@aplica/aplica-theme-engine
v3.13.7
Published
Configurable core engine for theme generation, design tokens, and multi-platform build output.
Maintainers
Readme
Aplica Theme Engine
@aplica/aplica-theme-engine is the reusable engine package behind Aplica's theme generation and token build pipeline.
It lets a consumer project keep its own theme configuration, generate local data/, and build local dist/ without copying the engine internals into each repository.
The preferred CLI command is theme-engine. The legacy aplica-theme-engine name remains available as a compatibility fallback.
Model Themes
The example themes under config/ are the active Aplica model-theme set.
They are used as:
- authored examples of valid theme config
- control fixtures for preview and regression testing
- reference scenarios for interaction decomposition, accessibility, overrides, gradients, and typography choices
Detailed maintainer-facing guides live in:
What this package does
- generates brand, mode, surface, semantic, and foundation token data from consumer-owned theme config
- supports workspace-level color-text generation, including the legacy 3-part general color contract and the expanded 4-part contract with
txt - generates Tokens Studio / Figma scaffolding files such as
data/$themes.jsonanddata/$metadata.jsonfrom the active workspace structure - builds multi-platform output such as JSON, typed JSON, ESM, CommonJS, CSS, and type declarations
- exposes a CLI plus stable config helpers for consumer projects
- keeps a legacy compatibility workspace for core development and transition scenarios
Interaction Decomposition And Surface Presets
In 3.9.0, interaction structure is split between workspace scope and theme scope:
aplica-theme-engine.config.mjs- owns shared public structure such as
legacyStructureandghost.enabled
- owns shared public structure such as
- theme files under
config/- own per-theme authored values such as decomposition method, direction, and state levels
This split is required for contract safety:
- shared generated layers such as
mode,surface, andsemanticcannot expose different public interaction structures per theme in the same workspace - because of that,
legacyStructureandghost.enabledmust stay global - theme files should only declare authored values, not workspace-wide public structure switches
Workspace config example:
generation: {
interaction: {
legacyStructure: false,
ghost: {
enabled: true
}
}
}Theme config example:
options: {
interaction: {
decomposition: {
method: 'dilution',
dilution: {
direction: 'high',
target: 'canvas',
fallback: 'ambient-neutral'
}
},
surfaces: {
solid: {
levels: {
action: 0.9,
active: 0.8,
focus: 1.2
}
},
ghost: {
levels: {
normal: 'transparent',
action: 0.95,
active: 0.9,
focus: 1.1
}
}
}
}
}Theme config example with grouped overrides:
options: {
interaction: {
decomposition: {
method: 'system-scale'
},
surfaces: {
solid: {
levels: {
action: 120,
active: 140,
focus: 50
}
},
ghost: {
levels: {
normal: 'transparent',
action: 40,
active: 60,
focus: 20
}
}
},
groups: {
function: {
surfaces: {
ghost: {
decomposition: {
method: 'dilution',
dilution: { direction: 'low' }
},
levels: {
normal: 'transparent',
action: 1.0,
active: 1.1,
focus: 0.25
}
}
}
},
feedback: {
decomposition: {
method: 'dilution',
dilution: { direction: 'low' }
}
}
}
}
}What this means:
system-scale- the legacy interaction behavior, now with an explicit name
- state values are palette levels such as
action: 120
dilution- state values are per-state dilution factors such as
action: 0.9 normalis always the authored base color- values below
1move toward the configured direction - values above
1invert the configured direction and use the excess intensity on the opposite extreme
- state values are per-state dilution factors such as
direction: 'high'- prefers the darker direction according to the engine's scale semantics
direction: 'low'- prefers the lighter direction according to the engine's scale semantics
target: 'fixed'- keeps the legacy dilution target behavior
lowaims at white andhighaims at black
target: 'canvas'- makes dilution quadrant-aware
- the dilution target becomes the resolved interaction canvas for the active
modeandsurfaceType
target: 'anchor'- makes dilution aim at an authored chromatic destination instead of only white/black or the canvas
anchor.sourcecan use:palettehextoken
anchor.canvasAware: true- keeps that chromatic anchor quadrant-sensitive
- light canvases pull it lighter
- dark canvases pull it darker
fallback: 'legacy'- if a canvas-aware result collapses too close to the canvas, the engine falls back to the fixed white/black behavior
fallback: 'ambient-neutral'- if a canvas-aware result collapses too close to the canvas, the engine reuses the nearest visible
brand.ambient.neutraltone from the same quadrant
- if a canvas-aware result collapses too close to the canvas, the engine reuses the nearest visible
legacyStructure: true- keeps the current public interaction structure
legacyStructure: false- enables explicit public
solidandghostgroups
- enables explicit public
ghost.normal: 'transparent'- means the visible surface is the declared canvas, so
txtOnis validated on the effective composited background, not on a raw opaque token
- means the visible surface is the declared canvas, so
groups.function/groups.feedback- allow local decomposition and state overrides for one interaction family without forcing the same behavior onto the other
groups.<group>.surfaces.<preset>- is the most specific interaction override layer
- merge order is:
- theme default
- preset default
- group default
- group + preset override
baseAdaptation- is the systemic opt-in for adapting authored base surfaces by quadrant
- it is meant for cases where
interaction.*.normalorproduct.*.*.defaultcannot stay fixed acrosslight/darkandpositive/negative - it does not replace authored
action,active, orfocuslogic - it only adapts:
- interaction
normal - product
default
- interaction
baseAdaptation.interaction.<group>.<preset>- adapts one interaction base branch such as
feedback.solid.normal
- adapts one interaction base branch such as
baseAdaptation.product- adapts product
defaultsurfaces across quadrants without introducing manual overrides
- adapts product
Backward compatibility rules:
- themes with no
options.interactionkeep the current behavior - legacy
options.interfaceFunctionPaletteLevelsstill works system-scaleis the implicit defaultsolid/ghostonly become public generated groups when the workspace explicitly setsgeneration.interaction.legacyStructure: false
Accessibility rules:
txtOnis always validated against the effective visible background of the interaction statetxtremains the readable family-colored text for the declared ambient canvas- the interaction contract is validated in all four quadrants:
light-positivelight-negativedark-positivedark-negative
- canvas-aware dilution is now validated as a general capability of
method: 'dilution'- it is not limited to
ghost solidand any grouped override can use the sametarget/fallbackcontract
- it is not limited to
- quadrant-aware base adaptation is also validated
- interaction base states and product defaults can now be tested as first-class quadrant-sensitive surfaces
Important Color Contract Modes In 3.6.x
In the active 3.6.x line, the Theme Engine treats general readable-color generation as a workspace-level decision configured in aplica-theme-engine.config.mjs.
The active controls live under:
generation.colorText.generateTxtgeneration.colorText.txtBaseColorLevelgeneration.colorText.fallbackBaseColorLevelgeneration.colorText.textExposure
Example:
import { defineThemeEngineConfig } from '@aplica/aplica-theme-engine/config';
export default defineThemeEngineConfig({
generation: {
colorText: {
generateTxt: false,
txtBaseColorLevel: 140,
fallbackBaseColorLevel: 130,
textExposure: {
feedback: true,
interfaceFunction: false,
product: false
}
}
}
});What each key means:
generateTxtfalsekeeps the smaller legacy general color contracttrueenables the expanded contract withtxt
txtBaseColorLevel- preferred starting level when the engine is generating
txt
- preferred starting level when the engine is generating
fallbackBaseColorLevel- preferred starting level when
generateTxtis off and the engine still needs to derive a readable fallback text candidate
- preferred starting level when
textExposure- controls which readable text families rise into
mode,surface,semantic, and simplifiedfoundation.txt - default is
feedbackonly interfaceFunctionandproductare opt-in
- controls which readable text families rise into
Default guidance for the current release line:
- older or migrated workspaces should stay on
generateTxt: falseunless you are intentionally moving them to the expanded readable-text contract - new starter/model scaffolds now also default to
includePrimitives: falseso the first generated workspace stays lighter and easier to validate
Which Config Owns What
For tutorials and integrations, keep this split explicit:
aplica-theme-engine.config.mjs- owns workspace-wide generation behavior
- owns
generation.colorText.generateTxt - owns
generation.colorText.txtBaseColorLevel - owns
generation.colorText.fallbackBaseColorLevel - owns
generation.colorText.textExposure - owns
generation.interaction.legacyStructure - owns
generation.interaction.ghost.enabled
theme-engine/config/*.config.mjs- own theme-authored input
- colors
- mapping
- typography
- per-theme overrides
txtOnStrategyand other theme-local optionsoptions.interaction.decomposition.*options.interaction.surfaces.solid.levelsoptions.interaction.surfaces.ghost.levels
This distinction is important:
- the theme file describes the theme itself
- the workspace config describes how the engine should generate the workspace contract
- mixed public interaction structures inside one workspace would break shared
mode/surface/semanticcontracts, so the engine treats those switches as workspace-scoped on purpose
Use these modes:
generateTxt: false:- keeps the legacy 3-part general color contract
backgroundtxtOnborder
generateTxt: true:- activates the expanded 4-part contract
backgroundtxtOnbordertxt
When generateTxt: true, the systemic contract is:
backgroundtxtOnbordertxt
This remains an important paradigm shift, not just an extra field:
txtOnstill means text or icon color used directly on the colored surfacetxtis now a separate generated text color that keeps the same semantic family while remaining readable on the declaredtheme.color.{mode}.brand.ambient.contrast.base.positive.backgroundcanvas of the active filetxtis accessibility-normalized, just like the rest of the generated color systemmode,surface,semantic, brand, grayscale, and primitive outputs participate in the updated quartet whenever the workspace enablesgenerateTxt
Important Text Contract Shift In 3.6.x
The 3.6.x line also changes how the Theme Engine generates and exposes text tokens.
txtOnkeeps the same meaning and is not being redefined by this work- first-level readable aliases such as
info,warning,primary, orpromonow resolve from text sources, not fromsurface.* - those simplified aliases represent the corresponding default readable value of that family
- structured text states are now exposed upward through
brand,mode,surface, andsemantic - generated foundations expose only the simplified first-level readable aliases in
foundation.txt
That means the engine now exposes text behavior intentionally:
- feedback text can expose
normal,action,active, andfocus - interface function text can expose
normal,action,active, andfocus - product text can expose its intensity ladder through
txt - generated foundations now flatten the readable default values into first-level aliases such as
foundation.txt.info,foundation.txt.primary, andfoundation.txt.promo - by default only
feedbackreadable text is promoted upward;interfaceFunctionandproductremain opt-in throughgeneration.colorText.textExposure
Use this rule of thumb:
- use
txtOnwhen text sits directly on the colored surface - use
txtor the structured text states when you need that semantic family on an established ambient or contrast background
If you need the new txt token to start from a different preferred level than border, use the workspace config:
generation.colorText.txtBaseColorLevelwith default readable-text search starting at140generation.colorText.fallbackBaseColorLevelwith fallback readable search starting at130whengenerateTxtis off and the engine must choose a readable text candidate without emittingtxt
Those controls let the workspace steer the preferred source levels, while the engine still enforces accessibility on the final generated value.
The package's authored model for this release line is theme_samples/aplica-themes, which now demonstrates:
- the
generation.colorTextworkspace contract - the optional
background / txtOn / border / txtquartet - the readable-text shift from
surface.*totxt.* - the simplified
foundation.txt.*aliases - optional readable-text promotion through
generation.colorText.textExposure - the split between workspace-level generation config and theme-level authored config
Breaking Upgrade Notes For 3.6.x
Treat the full 3.6.x line as a breaking upgrade line.
Even though the package version is in the 3.6.x range, this release changes the generated contract deeply enough that existing consumers should assume migration work is required.
If you already use @aplica/aplica-theme-engine, review these points before adopting 3.9.0:
3.9.0adds authored interaction decomposition and expanded surface presets:system-scale,dilution, optional publicsolid/ghost, and state-level authored control throughoptions.interaction3.9.0preserves the legacy public interaction structure by default;solid/ghostonly appear when the workspace explicitly setsgeneration.interaction.legacyStructure = false3.9.0keepsoptions.interfaceFunctionPaletteLevelsworking as a legacy-compatible input and normalizes it into the new interaction contract internally3.9.0addstheme-engine preview, which generates a static visual HTML preview for semantic colors,txtOn,txt, borders, typography, and elevation from the current workspacedist/3.8.4finishes the negative readable-text fix in the final distributable outputs:interface.function.*.txt,interface.feedback.*.txt,interface.function.disabled.*.txt, and negativeproduct.*.txtnow resolve against the correct mirrored canvas, and the package now audits all emitteddist/json/**.txtleaves againstcolor.brand.ambient.contrast.base.positive.background3.8.4also addsmode.productBySurfaceto preserve product polarity throughmode -> surface -> semantic, including freshly bootstrapped workspaces that createdata/mode/*.jsonfrom scratch3.8.3fixes the remaining ambient surface-polarity gap:surface negativenow resolves the wholebrand.ambientfamily from the inverse mode canvas, andambient.contrast.*.negative.txt/ambient.neutral.*.txt/ambient.grayscale.*.txtare all generated against the declaredcontrast.base.positive.backgroundcanvas of the final file3.8.2fixesbrand.brandingpolarity insurface/semantic:negativenow resolves brandingtxtOn,border, andtxtfrom the inverse mode branch instead of flattening to the same output aspositive3.8.2does not force authored branding backgrounds to differ by surface; if the source theme uses the sametheme.color.light|dark.brand.branding.*.background, the generatedbackgroundmay still match across positive/negative3.8.1corrects the accessibility target for ambienttxt:txtOnstill validates on the level's ownbackground, whileambient.*.txtnow follows the declared contrast canvas contract instead of the local level background3.8.0introducestheme-engineas the preferred CLI alias while keepingaplica-theme-engineavailable as a compatibility fallback- the generated general color shape is now controlled globally in
aplica-theme-engine.config.mjsthroughgeneration.colorText.generateTxt - when
generateTxtis enabled, the generated general color shape is no longerbackground / txtOn / border; it isbackground / txtOn / border / txt 3.7.1fixes the ambienttxtinversion regression in the expanded quartet path, so positive and negative ambient readable-text branches are resolved against the correct background context instead of collapsing to one mode-wide candidate3.7.2fixesbrand.brandingpropagation so mode-awareborderandtxtvalues now survive the trip fromtheme.color.*throughmode,surface,semantic, and finaldist/outputs instead of flattening too early insurface.brand.branding3.7.4fixes structured readable-text inversion forfeedbackandinterfaceFunction, validates those generated readable branches against their real positive/negative ambient backgrounds, and stops generating publicproduct.*.txttokens whengeneration.colorText.textExposure.productis off3.7.5removes invalidtxt: nullplaceholders from disabled product readable-text branches, which fixes Tokens Studio / Figma loading errors in consumer workspaces that enabledgenerateTxtglobally but intentionally keptproductreadable text off- readable text aliases are no longer allowed to come from
surface.*; they now come fromtxt.* - flat readable text aliases now mean the family's default readable text value, equivalent to
txt.normal foundation.txtno longer behaves like the older nested readable-text surface; it is now simplified to first-level aliases such asfoundation.txt.info,foundation.txt.primary, andfoundation.txt.promo- only
feedbackreadable text is promoted by default;interfaceFunctionandproductwill not appear unless the workspace explicitly enables them throughgeneration.colorText.textExposure - when
generation.colorText.textExposure.productisfalse, publicproductbranches now stay on the smallerbackground / txtOn / bordercontract inmode,surface, andsemantic - existing consumers should expect diffs not only in
data/semantic/default.json, but also indata/brand/*,data/mode/*,data/surface/*,data/foundation/*, and the deriveddist/output - converted legacy consumers now preserve the source workspace
generation.colorTextcontract automatically during migration, so parity and rebuild behavior no longer silently fall back to the no-txtdefaults when the source fixture already publishes the expanded quartet
Recommended upgrade checklist for existing consumers:
- Rebuild the workspace with
theme-engine build. - Run
theme-engine validate:data. - Review the generated diffs in
data/anddist/instead of assuming path compatibility. - Move any old text-generation controls out of theme-level
options.*and intoaplica-theme-engine.config.mjsundergeneration.colorText.*. - Audit any consumer code, token references, Figma mappings, or foundation aliases that depended on the old 3-part color contract or on old readable-text paths.
- If you need readable text for
interfaceFunctionorproduct, enable them intentionally throughgeneration.colorText.textExposure. - Use
config/aplica-blue-sky.config.mjsandtheme_samples/aplica-themesas the reference model for the new authored contract.
Fallbacks and non-changes to keep in mind:
txtOnis unchanged; this work does not redefine text-on-surface behaviortxtis different fromtxtOn: it is the readable family-colored text meant for the declared ambient contrast background, not for the token's own local block background- when
generateTxt: true, the newtxtsearch starts from level140, then walks to darker levels until one passes accessibility; if none passes, the engine falls back to black - when
generateTxt: false, readable fallback text starts fromfallbackBaseColorLevel(130by default), continues through darker levels until one passes, and falls back to black if the scale cannot satisfy accessibility - when
generation.colorText.textExposureis omitted, the default remainsfeedback: true,interfaceFunction: false,product: false - if you do not opt into extra readable-text families, the generated contract stays intentionally smaller at the
foundation.txtlayer
Legacy and migration expectations in the current package line:
- the internal compatibility fixture and the validated legacy migration path now both keep
generateTxt: falseby default - the package smoke test, compatibility suite, and legacy parity suite all validate that smaller contract path
- the migrator now writes a portable
theme-engine/lib/schema-loader.mjsinto converted consumers when they need local schema runtime support - legacy duplicated
dist/css/semantic/*files are still reported during parity analysis, but they are treated as expected report-only drift instead of blocking failures
Preferred model
The recommended setup is:
consumer-project/
aplica-theme-engine.config.mjs
theme-engine/
config/
global/
foundations/
my-brand.config.mjs
schemas/
transformers.config.mjs
data/
dist/The consumer owns the configuration and generated artifacts. The package owns the engine logic, schemas, defaults, and CLI.
Install
npm install @aplica/aplica-theme-engineFor CLI usage, prefer theme-engine. The previous aplica-theme-engine command still works as a fallback while downstream projects migrate.
To bootstrap a new workspace without installing the package globally, use:
npx @aplica/aplica-theme-engine@latest initThen add consumer scripts like:
{
"scripts": {
"tokens:build": "theme-engine build",
"tokens:build:all": "theme-engine build:all",
"tokens:themes": "theme-engine themes:generate",
"tokens:dimension": "theme-engine dimension:generate",
"tokens:sync": "theme-engine sync:architecture",
"tokens:foundations": "theme-engine foundations:generate",
"tokens:figma": "theme-engine figma:generate",
"tokens:validate": "theme-engine validate:data",
"tokens:preview": "theme-engine preview",
"tokens:ai": "theme-engine ai:init",
"tokens:ai:guidance": "theme-engine ai:guidance"
}
}Recommended meaning:
tokens:build: preferred full pipeline for consumer projectstokens:build:all: rebuilddist/from the currentdata/onlytokens:themes: regenerate brand theme filestokens:dimension: regenerate dimension outputstokens:sync: propagate structure into mode, surface, semantic, and foundationtokens:foundations: regenerate foundation aliases and stylestokens:figma: regenerate Tokens Studio / Figma scaffolding filestokens:validate: validate generateddata/against the active schemastokens:preview: generate a static visual preview indist/preview/tokens:ai: inject the distributed AI guidance templates into the current workspacetokens:ai:guidance: compose a portable Markdown AI guidance bundle intodist/
AI Guidance Distribution
To ship consumer-owned AI usage guidance alongside compiled token artifacts, add an ai.guidance block to aplica-theme-engine.config.mjs and run theme-engine ai:guidance.
import { defineThemeEngineConfig } from '@aplica/aplica-theme-engine/config';
export default defineThemeEngineConfig({
ai: {
guidance: {
enabled: true,
output: './dist/AI_GUIDANCE.md',
header: {
title: 'AI Guidance',
intro: 'Prefer compiled outputs in `dist/` over guessed token names.',
metadata: {
audience: 'AI coding tools',
contract: 'portable dist bundle'
}
},
sources: [
'./docs/context/ai-ui/UI_TOKEN_CONSUMPTION_CONTRACT.md',
'./docs/context/tokens/token-usage-for-components-and-figma.md',
{
path: './docs/context/ai-ui/TYPOGRAPHY_AND_ELEVATION_STYLE_USAGE.md',
order: 30
}
]
}
}
});Notes:
- the feature is opt-in and does nothing unless
ai.guidance.enabledistrue - source Markdown files stay consumer-owned and can live anywhere inside the workspace
- output defaults to
./dist/AI_GUIDANCE.md - source order follows the array order unless you provide an explicit numeric
order - generated output is deterministic and safe for CI use
Typed JSON Output
The active package line also supports an additive typed JSON output for downstream engineering targets that need token metadata preserved in the generated artifact.
This is exposed as the built-in jsonTyped platform in theme-engine/transformers.config.mjs.
Example:
import { defineTransformersConfig } from '@aplica/aplica-theme-engine/transformers/config';
export default defineTransformersConfig({
layers: {
semantic: {
enabled: true,
platforms: ['json', 'jsonTyped', 'esm', 'js', 'css']
}
},
output: {
directories: {
json: './dist/json',
jsonTyped: './dist/json-typed',
esm: './dist/esm',
js: './dist/js',
css: './dist/css/semantic'
}
},
formatOptions: {
jsonTyped: {
typeKey: 'type',
valueKey: 'value',
descriptionKey: 'description',
includeDescription: true
}
}
});What jsonTyped does:
- preserves token metadata in the generated payload
- emits nested token objects with
typeandvalue - can optionally include
descriptionandpath - is additive, so it does not change the existing
jsoncontract
Important behavior:
jsonTypedpreserves the current token semantics instead of forcing a single scalar type for every token- that means
valueis still token-dependent - for example:
- colors remain strings such as
#265ed9 - spacing and radii remain dimension strings such as
12px - many
fontWeightsare numeric, but some can still be emitted as strings when the active token contract uses a named value
- colors remain strings such as
So the recommended consumer assumption is:
typeis the stable discriminatorvalueshould be interpreted according to that token type
Current boundary:
- consumers can enable the built-in
jsonTypedplatform per layer - consumers can customize the output directory and metadata key names for
jsonTyped - consumers still cannot register an entirely new output platform or arbitrary renderer through config alone
Recommended use cases:
- Dart / Flutter token ingestion
- typed mobile pipelines
- engineering tooling that needs token type metadata instead of plain resolved values
Reference:
Quick start
Start with:
theme-engine initThat command opens two onboarding paths:
Load starter template: scaffolds a ready-to-run consumer workspace with one example theme, default package schemas, and starter defaults tuned for lower memory usageCreate using the wizard: scaffolds the consumer workspace and also generatestheme-engine/schemas/architecture.mjsfrom guided schema answers
Starter-template specifics in the current package line:
- the starter theme is scaffolded with
includePrimitives: false - the command prints an English welcome banner with:
- package version
- package license
- key docs entry points
- an optional support / coffee link
This keeps the starter workspace lighter by default while still leaving includePrimitives available as an authored theme option when a consumer really needs primitive output.
After init, run:
theme-engine buildIf you want the package to scaffold a consumer-owned architecture.mjs schema for you, run:
theme-engine schemas:helperThat helper asks structural questions and writes a starter schema, typically in theme-engine/schemas/architecture.mjs.
If you also want the package to inject the distributed AI guidance surfaces for the current workspace, run:
theme-engine ai:initThat command injects the current distributed guidance templates into the consumer workspace, including:
docs/context/aplica-ui-integration.md.cursor/rules/.claude/skills/.github/instructions/
theme-engine build is the preferred consumer command. It runs the full pipeline:
ensure:datathemes:generatedimension:generatesync:architecturefoundations:generatefigma:generatebuild:all
If you want to validate the generated data/ contract before or after a build, run:
theme-engine validate:dataThat command validates the generated token layers in data/ against the active workspace schemas and current generation contracts.
If you want a quick visual check of the generated colors and helper styles, run:
theme-engine previewThat command writes a static preview bundle to:
dist/preview/index.htmldist/preview/preview-data.jsdist/preview/preview-app.jsdist/preview/preview-app.css
The preview reads the current workspace dist/ output and the active workspace schema snapshot, and lets you inspect:
- semantic brand, interface, product, and text tokens
background,border,txtOn, andtxt- typography helper classes
- elevation helper classes
light/darkpositive/negative
In the active 3.9.0 line, the preview is schema-aware:
- semantic ordering and grouping follow the active architecture schema and workspace interaction contract
- public
solid/ghostrendering follows the current workspace structure instead of assuming only the legacy grouping - foundation preview sections are resolved from generated foundation output instead of relying only on a fixed hardcoded subset
- the UI now includes a
Viewtoggle:Detailedkeeps the full section-by-section explorerSummaryrenders a condensed quartet table focused onbackground,txtOn,border,txt, and contrast so QA can compare color behavior faster across quadrants
If you want to rebuild first and then generate the preview in one step, run:
theme-engine preview --buildIf you want the preview to stay available through a local static server so the HTML, CSS, JS, fonts, and helper styles behave the same way they do in a browser, run:
theme-engine preview --serveIn the current line, preview --serve also watches the active workspace dist/ output. When semantic JSON, helper CSS, foundations, or other preview-relevant dist artifacts change, the preview bundle is regenerated and the browser reloads automatically.
You can combine both when you want a fresh build and an immediately browsable preview:
theme-engine preview --build --serveFor the full setup, migration notes, and pre-publish validation flow, see Consumer Package Guide and Publish Checklist.
Core validation scripts
Inside this repository, the main maintainer checks are:
npm run test:compat- validates legacy workspace mode, consumer fixture mode, config loading, and Figma scaffolding behavior
npm run test:package-smoke- packs the current package, installs it into a temporary consumer workspace, and validates a real build flow
npm run test:legacy-parity- reruns the legacy migration parity suite against the validated internal references
These are maintainer-level validation scripts for the package repository itself, not scripts that downstream consumers need to copy verbatim.
Where to read next
Use the documentation in layers:
- Theme Engine Documentation Map: the official ordered public path in English
- Theme Engine - Mapa da Documentação: the same ordered public path in PT-BR
- Consumer Package Guide: the operational companion for real consumer setup, commands, and workspace ownership
- Context Index: maintainer and agent navigation
- Documentation System: how the documentation itself is organized and maintained
Recommended reading paths:
- new consumer onboarding:
README→#00→#01→#02→#03→Consumer Package Guide - system understanding:
#04→#06→#07→#09→#10 - theme authoring and discovery:
#02→#03→#11 - systemic color model changes:
README→docs/context/THEME_CONFIG_REFERENCE.md→docs/context/DYNAMIC_THEMES.md→docs/context/dynamic-themes-reference/COLOR-DECOMPOSITION-SPEC.md→#10
Historical material is preserved for traceability only:
docs/context/legacy/: internal historical context and transition notesdocs/legacy/: archived user-facing docs that no longer describe the current package model
AI UI reference program
The repository also maintains an internal AI-guided UI library integration program for teaching and validating how LLMs should apply Aplica tokens in real component implementations.
Start with:
Current validated and in-validation reference set:
ButtonDialogInputBadgeSelectCardTabs
This program is the maintainer-facing source of truth for:
- AI token-consumption rules
- sanctioned typography and elevation style usage
- component archetype guidance
- distributed skill/template behavior for other AI tools
Important boundary:
- this program helps us train and distribute better AI guidance
- the sandbox used to validate that guidance is internal to this repository
- it is not part of the published npm package contract
- downstream consumers are expected to use the package outputs and the distributed AI guidance, not this repo's sandbox implementation directly
- distributed skill surfaces should stay objective and point to the contract/archetypes instead of copying internal validation details inline
Legacy migration parity
The package also includes a legacy migrator focused on one critical guarantee:
- reproduced
data/outputs must match the legacy reference - reproduced
dist/outputs must match the legacy reference
This is the migration success rule. Metadata drift, validation artifacts, and wrapper/index drift may still appear in reports, but they do not fail migration parity by themselves.
Recommended real-project flow:
theme-engine migrate:legacy-consumer run --source <legacy-project>That command:
- analyzes the legacy project
- selects the migration profile you requested, or the recommended one when omitted
- converts the workspace
- runs the correct build command for the chosen profile
- compares converted
data/anddist/against the legacy reference
You can still run each stage separately:
theme-engine migrate:legacy-consumer analyze --source <legacy-project>
theme-engine migrate:legacy-consumer convert --source <legacy-project>
theme-engine migrate:legacy-consumer compare --source <legacy-project>For repeated test runs against the same target workspace, use --force with convert or run to replace the previous converted workspace.
The migrator currently supports two conversion profiles:
config-driven: for legacy repos that still have realconfig/andschemas/output-seeded: for legacy repos wheredata/anddist/are the reliable source of truth
If you omit --profile, the migrator analyzes the source project and uses the recommended profile for that layout.
Each converted consumer gets its own schema surface under theme-engine/schemas/:
- copied legacy schemas when they are portable
- inferred
architecture.mjswhen the legacy project has no schema source - package-backed bridge schemas for runtime helpers that must remain available in the converted workspace
Validated internal migration references cover both config-driven and output-seeded legacy profiles, and they currently achieve output parity.
Reports and converted workspaces are written under temp/outputs/legacy-migration/, leaving the source fixtures untouched.
Maintainers can rerun the full regression suite with:
npm run test:legacy-parityPublic consumer APIs
@aplica/aplica-theme-engine/config@aplica/aplica-theme-engine/transformers/config@aplica/aplica-theme-engine/helpers/dimension-scale@aplica/aplica-theme-engine/schemas/architecture@aplica/aplica-theme-engine/schemas/generation/architecture@aplica/aplica-theme-engine/schemas/semantic-token-descriptions@aplica/aplica-theme-engine/schemas/descriptions/semantic-token-descriptions@aplica/aplica-theme-engine/schemas/typography-scale@aplica/aplica-theme-engine/schemas/generation/typography-scale@aplica/aplica-theme-engine/schemas/typography-styles@aplica/aplica-theme-engine/schemas/generation/typography-styles@aplica/aplica-theme-engine/schemas/foundation-styles@aplica/aplica-theme-engine/schemas/generation/foundation-styles@aplica/aplica-theme-engine/schemas/foundation-style-defaults@aplica/aplica-theme-engine/schemas/defaults/foundation-style-defaults@aplica/aplica-theme-engine/schemas/foundation-token-descriptions@aplica/aplica-theme-engine/schemas/descriptions/foundation-token-descriptions@aplica/aplica-theme-engine/schemas/data
CLI commands
theme-engine buildtheme-engine build:themestheme-engine build:alltheme-engine build:semantictheme-engine build:foundationtheme-engine build:componentstheme-engine inittheme-engine consumer:inittheme-engine ai:inittheme-engine ai:guidancetheme-engine ai:bundletheme-engine ai:disttheme-engine ai:setuptheme-engine skillstheme-engine skills:inittheme-engine themes:generatetheme-engine themes:singletheme-engine themes:figmatheme-engine ensure:datatheme-engine dimension:generatetheme-engine sync:architecturetheme-engine sync:architecture:testtheme-engine sync:architecture:schematheme-engine schemas:helpertheme-engine schemas:inittheme-engine validate:datatheme-engine previewtheme-engine data:validatetheme-engine foundations:generatetheme-engine figma:generatetheme-engine migrate:legacy-consumertheme-engine legacy:migrate
Compatibility note:
theme-engine ai:initis the recommended AI guidance injection commandai:setup,skills, andskills:initare supported aliasestheme-engine ai:guidancegenerates a consumer-owned Markdown bundle indist/ai:bundleandai:distare supported aliases
Expected runtime behavior
theme-engine buildis a full generation + build pipeline for consumer projects.theme-engine figma:generateregenerates the Tokens Studio / Figma scaffolding files indata/from the active theme/foundation structure.theme-engine previewgenerates a static visual artifact indist/preview/from the currentdist/output. Usetheme-engine preview --buildwhen you want the command to rebuild before rendering the preview.theme-engine preview --servestarts a small local static server for the generated preview so scripts, fonts, and helper styles run in a browser context. Combine it with--buildwhen you want a fresh preview before serving.- inside the preview UI,
View > Summaryprovides a condensed color-comparison mode for quartet tokens (background,txtOn,border,txt) while preserving the existing detailed explorer view. data/$themes.jsonis merged structurally and preserves Figma-owned IDs and references when entries already exist.theme-engine build:allonly transforms the currentdata/intodist/. Use it whendata/is already prepared.- If the workspace has no consumer config yet, build commands fail fast with a clear message asking you to run
theme-engine init. - Semantic artifact names are derived from the active
layers.semantic.outputPatternin the consumer transformers config. Known file extensions in the pattern are normalized away before the build appends the platform extension. - Foundation artifacts keep a stable namespace contract per foundation brand:
dist/json/foundation/<foundationBrand>/foundation.jsondist/js/foundation/<foundationBrand>/foundation.cjsdist/esm/foundation/<foundationBrand>/foundation.mjs
- Font assets are consumer-owned. If
assets/fontsdoes not exist in the consumer workspace, the build warns and skips font copy/manifest generation instead of failing. - If
data/componentsdoes not exist,build:componentsis skipped with an informational message instead of failing. - Theme generation may emit accessibility warnings when a palette does not satisfy AAA contrast. When the theme/config allows AA fallback, the engine logs the fallback and continues. Treat those messages as a theme-quality review signal, not as a package crash.
- If gradients are enabled in config but
themes:generatehas not populated brand gradients yet,sync:architecturewarns that gradients were not propagated. Run the full pipeline or generate themes before syncing architecture.
Consumer vs core scripts
- Consumer projects should prefer the public CLI commands such as
theme-engine build. - The
package.jsonscripts inside this repository exist mainly for core development, fixture validation, and release checks.
Safety model
- Consumer-configured workspace paths are validated to stay inside the active workspace root.
- Consumer
.mjsconfig files are treated as trusted executable code from the consumer project. - The core package does not require consumers to edit engine internals to define their themes.
Schema overrides
- Package schemas are the default source of truth.
- A consumer can override them by setting
paths.schemasDirinaplica-theme-engine.config.mjs, typically to./theme-engine/schemas. - The engine resolves schemas from the consumer workspace first and falls back to the package defaults.
- Compatibility tests now cover this override path, so custom consumer schemas are part of the supported contract.
- Generated output schemas now live in
schemas/data/and can be validated throughtheme-engine validate:data. - The legacy migrator also preserves this rule: every converted workspace must expose its own schema surface, either copied from the legacy consumer or inferred from legacy outputs.
Repository modes
This repository still supports two modes:
- legacy compatibility workspace at the repository root
- preferred consumer workspace through the package model
New integrations should follow the consumer workspace model first.
More docs
License
Copyright 2018-2026 Poe Bellentani
Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
Trademarks: “Aplica” and “Aplica Design” are trademarks of Poe Bellentani; registration is pending at INPI (Brazil). The Apache License does not grant rights to use these names as product or service branding beyond customary attribution.
Support
If this project helped you, consider supporting it on Ko-fi. Donations sustain the open-source tooling behind Aplica Design and the YouTube channel — they do not imply commercial support or an additional license.
