@meetcleo/design-tokens
v0.3.0
Published
Canonical source of truth for Cleo's design tokens (DTCG JSON), plus a TypeScript reader.
Readme
@cleonardo/tokens
Canonical source of truth for Cleo's design tokens.
Every downstream consumer — the generator, the Ruby module, the User Prompt pilot, product surfaces — is (or will be) produced from what lives here.
Current scope
Colour primitives and semantic roles, plus a code-owned base-unit spacing API. Radii, typography, and other token types land in follow-up tickets.
Spacing
Use CleoDesignTokens.spacing(multiplier, options) for every spacing value. spacing is code-owned: it is not exported from Figma, included in token files, or changed by the Figma sync route.
It accepts "auto" / :auto or a finite positive or negative multiple of 0.25, using a fixed 4px base unit. Without unit, it returns a numeric pixel value for React Native. CSS callers opt into px, rem, or em; relative units use a 16px base.
CleoDesignTokens.spacing(1); // 4
CleoDesignTokens.spacing(1, { unit: 'px' }); // '4px'
CleoDesignTokens.spacing(1, { unit: 'rem' }); // '0.25rem'
CleoDesignTokens.spacing(-1.5, { unit: 'em' });// '-0.375em'
CleoDesignTokens.spacing('auto', { unit: 'rem' }); // 'auto'CleoDesignTokens.spacing(1) # 4
CleoDesignTokens.spacing(1, unit: :px) # "4px"
CleoDesignTokens.spacing(1, unit: :rem) # "0.25rem"
CleoDesignTokens.spacing(-1.5, unit: :em) # "-0.375em"
CleoDesignTokens.spacing(:auto, unit: :rem) # "auto"Only px, rem, and em are supported units; another unit raises a clear error. auto ignores a supported unit but unsupported units are still rejected.
Format
Two files, both keyed by lowerCamel dot paths. Primitives store hex values; semantic roles reference primitives. Both public readers return resolved hex strings.
tokens/color/primitives.json — the palette. A single-palette leaf carries a value:
"brown": { "800": { "$type": "color", "$value": "#47201C" } }tokens/color/semantic.json uses the root marker "$schema": "cleo-semantic-references/1". The Base binding sits on the leaf; other themes appear under $themes only where they genuinely differ:
"core": { "content": { "primary": {
"$type": "color",
"$ref": "brown.800",
"$themes": {
"roast": { "$ref": "brown.50" }
}
} } }$refis a primitive fetch key, resolved against the configured experience. It records intent — thatcore.content.primaryis meant to bebrown.800, not merely that it happens to equal#47201C.- A literal
$valuewithout$refmeans a palette gap: no primitive carries that value. See Known exceptions. - A theme absent from
$themesresolves to Base. A theme earns an entry only by differing from Base in its value or its$ref. - Neither
$refnor$valueon the leaf means the role exists in some themes but not Base — a Figma-side gap, reported by the transform rather than papered over. A themeless read raises.
The theme is an axis, not part of the key: core.content.primary is one role with per-theme values, not four keys. That's why 1880 leaf entries collapse to 473 roles plus 336 overrides.
Primitive experiences
When the primitive collection has multiple modes, Current supplies the existing leaf values, regardless of which mode Figma marks as its default. Other modes are normalised to lowerCamel names (Cleo Refresh → cleoRefresh) and stored as complete snapshots under $experiences:
"brown": { "50": {
"$type": "color",
"$value": "#F8F6F2",
"$experiences": { "cleoRefresh": { "$value": "#F9F9F8" } }
} }Semantic bindings are shared across experiences. A role referencing brown.50 automatically uses the Current or Refresh value from primitives.json. The reader resolves and caches each selected palette once, keeping subsequent fetches as lookup reads.
Only roles whose bindings differ need $experiences.cleoRefresh. An explicit experience snapshot replaces the whole semantic binding, including its theme overrides; it never merges with Current. This preserves cases where Refresh removes a Current theme override or changes the primitive reference. Literal palette gaps remain literal. Missing a semantic snapshot means reuse the shared references and literals against the selected primitive palette.
Every primitive still carries a complete value for each experience; missing primitive snapshots fail rather than falling back to Current. An experience is independent of the Base/Chat/Roast/Hype theme axis. Multiple primitive modes require Current; colliding normalised experience names and missing per-mode primitive values fail validation.
The transform expands semantic references before compatibility mapping, value diffs, removal checks and release notes, then compacts the written file. Reports still identify resolved entries as background.primary@experience:cleoRefresh@roast. The verifier resolves every binding against every available primitive experience, including inherited bindings.
The root schema distinguishes shared bindings from legacy complete snapshots. Both readers continue accepting the previous resolved format. Ship the new readers and reference-based data together; older readers cannot consume the new semantic JSON. Direct JSON imports are not a supported consumer API.
Readers default to Current. Runtime experience selection is documented under Consumers; token fetch keys and their generated unions remain experience-free.
Layout
packages/tokens/
tokens/
color/
primitives.json # the palette — resolved hex, no themes
semantic.json # the roles — Base on the leaf, overrides under $themes
scripts/
transform.mjs # CLI driver: reads the dump, writes the files
transform-core.mjs # Figma parsing, alias resolution and key derivation
semantic-format.mjs # compact references and lossless expansion for validation/diffs
spec/
synthesise-dump.mjs # builds dump fixtures from a pre-restructure token pair
fixtures/ # committed dumps + the value-equivalence expectations
fixtures/reader/ # small hand-written fixtures for the reader's own tests
lib/
cleo_design_tokens.rb # Ruby reader — requires the below, wires up `colors`
cleo_design_tokens/
tree_walker.rb # walks a token tree, yielding each leaf
lookup_builder.rb # flattens a tree into a frozen key -> value/SemanticEntry lookup
semantic_entry.rb # one role's Base value + theme overrides
bucket.rb # colors.primitives — one lookup, one `fetch`
semantic_bucket.rb # colors.semantic — adds the `theme:` axis
configuration.rb # fiber-local experience selection and shared immutable palette cache
errors.rb # token, theme, experience and collision errors
src/
CleoDesignTokens.ts # TypeScript reader
generated/
tokenKeys.ts # generated key unions — do not hand-editKeys
A key is the JSON path, dot-joined. Figma's authored casing is normalised to lowerCamel, and an all-caps segment lowercases whole:
| Figma | key segment |
| --- | --- |
| Credit Score | creditScore |
| DataVisPrimary | dataVisPrimary |
| ShimmerAlpha 2 | shimmerAlpha2 |
| UI | ui |
| EWA | ewa |
| 800 | 800 |
Space is the only character outside [A-Za-z0-9] anywhere in the tree. One helper (buildKey) produces $ref values, the collision check and the generated unions, so the three can never disagree.
The token type is not in the key — it's in the reader accessor (see Consumers), which is what leaves room for radii, typography and spacing to arrive as siblings later.
Rules
- Semantic roles alias primitives. Every role has a
$refon its Base value and on each override, except the known gaps. - All references resolve. No alias pointing at a variable that doesn't exist, or at anything outside the primitives collection.
- No key resolves from more than one place within its bucket.
primitives.jsonandsemantic.jsonhave separate key namespaces, so the same key in both is fine. Two Figma names in one collection that normalise to the same key is not.
Semantic namespaces
Top-level groups are core, extension, effects, feature, surface and ui. The four themes — base, chat, roast, hype — mirror Cleo's product personalities. They are not display modes: there is no light/dark axis here, and roast is already dark.
Updating from Figma
Designers own the tokens in Figma; this package mirrors them.
- The Figma plugin (COREEXP-323) writes
figma-exports/figma-dump.json. That folder is gitignored — the raw dump is input, not history. - Run
yarn tokens:checkfor a dry-run diff, oryarn tokens:transformto apply. - Review the change report and the resulting diff in
tokens/color/andsrc/generated/, then open a PR. That PR must carry thetoken-regenlabel —tokens/color/**andsrc/generated/**are gated as generated output (see CI), and without the label the gate fails it by design.
The input format
One file, figma-exports/figma-dump.json, verbatim from Figma's plugin API — nothing nested, hexed, filtered or resolved:
{
"$schema": "cleo-figma-dump/1",
"file": { "name": "Colour Modes and Themes" },
"collections": [
{ "id": "VariableCollectionId:1:3", "name": "Themes", "source": "local",
"defaultModeId": "1:0", "modes": [{ "modeId": "1:0", "name": "Light" }] }
],
"variables": [
{ "id": "VariableID:1:5", "name": "Base/Core/Content/Primary",
"collectionId": "VariableCollectionId:1:3", "resolvedType": "COLOR",
"valuesByMode": { "1:0": { "type": "VARIABLE_ALIAS", "id": "VariableID:9:7" } } }
]
}valuesByMode values are either { r, g, b, a } floats in 0–1 or { type: "VARIABLE_ALIAS", id }. Names keep / separators and Figma's casing. Aliases reference variables by id, and the dump must include every collection an alias reaches into.
This script owns every interpretation — name splitting, theme extraction, RGBA→hex, alpha, alias resolution, type filtering, and the collection→file mapping. The plugin shapes nothing, so alias handling exists in exactly one place.
Which collection becomes which file
Config, not hardcoded, because a second token type arrives later:
| Figma collection | Output | Types | Theme axis |
| --- | --- | --- | --- |
| Base Colors (fallback: Color Primitives) | tokens/color/primitives.json | COLOR | none; primitive modes select the experience palette |
| Themes | tokens/color/semantic.json | COLOR | first name segment, default Base |
| Color Roles (fallback: Modes) | tokens/color/semantic.json | COLOR | Figma modes named Base, Chat, Roast, Hype |
Each bucket selects the first available source: Base Colors before Color Primitives, and Themes before Color Roles before Modes. This keeps the original Colour Modes and Themes file exporting Themes while both its legacy and mode-based collections coexist. Unselected collections are ignored, including component-specific colour collections and spacing, radius, typography and border-width collections. Non-COLOR variables inside a selected collection are skipped and counted, not errors — Themes carries 18 FLOAT variables. Multiple collections with the selected name are rejected as ambiguous.
For mode-based semantic collections, every mode is resolved independently, for each primitive experience. The Base mode supplies each snapshot's leaf value even if Figma's default mode is different; overrides are emitted only when the value or primitive reference differs. A missing Base mode, duplicate theme names (case-insensitive), or a missing value for any declared mode fails validation.
Without an existing output baseline, paths are preserved apart from casing normalisation: content/primary emits content.primary. When syncing a multi-experience Color Roles collection into the published vocabulary, the CLI applies the compatibility rules below. Reader experience selection is documented under Consumers.
Migrating Color Roles without breaking published keys
The Color Roles collection covers only part of the existing vocabulary. When committed semantic output exists and the primitive collection has multiple experiences, the CLI keeps every existing Current semantic leaf and uses each new role's Token: <published.key> description to map its alternate snapshots onto that published key. An authored path that already exists uses that key directly. Missing or unknown mappings fail before writing.
If several roles share a copied description, exactly one must have the same final name segment as the target key. That role supplies the mapped Refresh value; the others retain their authored paths as additional keys. Other ambiguous mappings fail. In the Cleonardo 2.0 export, background.primary and background.dataVisPrimary supply the canonical mappings, while background.navbar and background.dataVisSenary are additional roles. The transform reports mapped, retained and additional role counts, plus any new semantic definitions that differ from the preserved Current values.
For legacy roles absent from Color Roles, the transform preserves Current and resolves their existing primitive references against each alternate palette. Reference-free literals remain literal, and theme overrides are recomputed relative to each experience's Base. A Current primitive change during this migration fails explicitly so the control palette cannot change accidentally. Current palette changes must be migrated separately.
The verifier requires every published key listed in scripts/semantic-contract.json and permits additional roles. It still validates all references, experiences, known gaps, canonical formatting and generated key unions. The normal removal guard remains enabled, including for alternate experiences; --allow-removals does not bypass the verifier's published-key contract.
Ordering
Keys are written in a canonical order — alphabetical by group, numeric-aware within a group, so palette scales read 50, 100, … 1000 rather than 100, 1000, …, 50. The order does not depend on the dump.
That matters because the dump carries Figma's own variable order. Ordering used to be inherited from it, which made output stable for identical input but not canonical across inputs: a designer reordering variables in Figma produced a whole-file diff with no value changes, burying the real ones. Leaves keep their authored key order ($type, $value, $ref, $themes, $experiences); $themes follows the configured theme order and $experiences uses numeric-aware name order.
Change policy
The transform is additive and mutative by default, but never destructive:
- Adds and changes — applied automatically. A changed theme override shows as a change on that role; a re-point at an identical hex still counts, since
$refis part of what's compared. - Removals — the transform refuses to write and exits 1, listing what would go. If the removal is intentional, re-run with
yarn tokens:transform -- --allow-removals.
Validation
Exits 2 and writes nothing on:
- Dangling alias id — the target isn't in the dump at all. Names the id and its collection.
- Dead reference — the target exists but sits outside the primitives collection.
- Semantic→semantic alias — resolution is single-hop; an alias points at a primitive.
- Ambiguous hex recovery — a role holds a literal that 2+ primitives share, so the transform can't tell which it meant. This exists so a missing
$refcan only ever mean "genuine palette gap". - Key collision — two names in one collection normalising to the same key.
Reported as warnings, not failures: palette gaps, roles missing from Base, aliases recovered by exact-hex match, skipped types, and ignored collections. All go to stderr, including the success line — CI has to capture it.
CI
.github/workflows/tokens.yml runs two jobs on every PR touching this package, both required to merge. verify covers the TypeScript side, in two layers, neither able to do the other's job:
- Generated files are generated (
.github/workflows/tokens.yml, layer 1).tokens/color/**andsrc/generated/**must be byte-identical to the PR's merge base, checked withgit diffrather than a fixture — a committed fixture would go red on the first real Figma change. This is what catches a hand-edit on one of the 13 primitives nothing references (alpha.dark.75,blue.300/500/900,green.300/500/600/800,orange.300/600/700/900,yellow.600): nothing insemantic.jsonpoints at them, so a value-comparison check has nothing to compare against. Skip it by applying thetoken-regenlabel — that's how a human who ranyarn tokens:transformand reviewed the diff (see Updating from Figma) tells the gate "yes, I regenerated these". It's a different label fromfigma-sync, kept separate so a reviewer can still tell an automated sync PR from a human one — butfigma-sync.ymlapplies both to its own PRs:token-regenis true there by construction (the PR's entire content is generated output), and it's what keeps layer 1 from failing if a human later pushes a follow-up commit to that PR branch (that push isn't made withGITHUB_TOKEN, so it does triggertokens.yml, unlike the PR's ownopenedevent — see below). - Invariants on the committed tree (
yarn tokens:verify, layer 2, every run). Layer 1 says "unchanged"; this says "correct". It rebuilds the canonical form of both JSON files andtokenKeys.tsand byte-compares, checks every$refresolves in every primitive experience, and checks the two allowlists below. It's the only gate that runs on afigma-syncPR — see below — where the files legitimately move, so layer 1 doesn't apply.
Both allowlists live as named consts at the top of scripts/verify-tokens.mjs, next to the why, mirroring Known exceptions:
PALETTE_GAP_ALLOWLIST— the one reviewed$ref-less semantic entry, pinned to its hex.MISSING_BASE_ALLOWLIST— the three roles Figma defines only underchat.
Editing an allowlisted token, or letting an allowlist entry go stale (the gap gets fixed in Figma but the exception isn't removed), fails tokens:verify either way.
A figma-sync PR is checked inside figma-sync.yml, not tokens.yml. GitHub does not fire pull_request workflow events for actions taken with GITHUB_TOKEN, so tokens.yml never runs on the PR's own opened event. figma-sync.yml runs tokens:verify, tokens:test and tokens:typecheck itself, against the tree the transform just wrote, before it opens the PR — a bad export fails the sync workflow rather than landing in review. That restriction doesn't cover what happens afterwards, though: a human pushing a follow-up commit to that PR branch uses their own credentials, which does trigger tokens.yml's synchronize event as normal — the token-regen label applied at creation (see above) is what keeps layer 1 from failing on that push.
ruby-test covers the Ruby reader (cleo_design_tokens, COREEXP-264) — bundle exec rspec against its own spec suite. Independent of verify: it doesn't touch tokens/color or src/generated, so it has no layer 1, and figma-sync.yml has no equivalent step, since a sync PR never touches lib/ or spec/.
Consumers
Configuring the NPM experience
import { CleoDesignTokens } from '@meetcleo/design-tokens';
const { fetch: fetchSemantic } = CleoDesignTokens.colors.semantic;
CleoDesignTokens.configure({ experience: 'refresh' });
fetchSemantic('core.content.primary'); // Resolves against Refresh primitives
fetchSemantic('core.content.primary', 'roast'); // Refresh's Roast override or Refresh Base
CleoDesignTokens.configure({ experience: 'current' }); // Reset on logout/account switchingexperience accepts 'current' (the default) or 'refresh'. The public refresh name selects the Figma Cleo Refresh snapshots stored as cleoRefresh. The application owns experiment bucketing; the package receives the chosen experience. Configuration is synchronous and applies to subsequent primitive and semantic fetches, including functions destructured before configuration. Spacing is unaffected.
Refresh requires synced snapshots on every primitive in the installed package. Semantic roles reuse shared bindings unless they carry an explicit Refresh replacement. Unsupported experiences, missing snapshots or incomplete values throw UnknownExperienceError. Both palettes are validated before switching, so a failed configuration leaves the previous selection intact. Successfully built palettes are cached, and resetting to Current restores the original values. The exported PRIMITIVES_LOOKUP and SEMANTIC_LOOKUP constants remain Current-only snapshots.
Already-resolved hex strings do not change. The mobile app must configure after resolving its bucket and then rebuild its colorRoles / theme-provider value. Configuration does not trigger React renders. App-init integration is a separate change.
This configuration is shared by all callers of the loaded NPM module instance. It is intended for the mobile app's active user; concurrent server users can use the RubyGem's scoped configuration below.
The readers (TypeScript: @meetcleo/design-tokens; Ruby: cleo_design_tokens) are the canonical way to read a value — never import tokens/color/*.json directly. They're namespaced by token type, then by layer, with the theme passed alongside the key:
CleoDesignTokens.colors.semantic.fetch("core.content.primary") # => "#47201C"
CleoDesignTokens.colors.semantic.fetch("core.content.primary", theme: :roast) # => "#F8F6F2"
CleoDesignTokens.colors.primitives.fetch("brown.800") # => "#47201C"Reach for colors.semantic by default. A colors.primitives call is reaching past the semantic layer into the raw palette — legitimate in a theme adapter that re-exports a palette tier, a smell anywhere else, and usually a sign that a semantic role is missing.
Configuring the Ruby experience
CleoDesignTokens.configure(experience: :refresh)
CleoDesignTokens.colors.semantic.fetch("core.content.primary")
CleoDesignTokens.colors.semantic.fetch("core.content.primary", theme: :roast)
CleoDesignTokens.configure(experience: :current)Symbols and strings are accepted for current and refresh; public refresh selects the stored cleoRefresh snapshots. Both primitive and semantic fetches read the selection at call time, including previously captured bucket objects and Method objects. Theme fallback stays inside the selected experience. Already-resolved hex strings and spacing values do not change; the exported lookup constants remain frozen Current snapshots.
The selection belongs to the current thread/fiber, not the whole Ruby process. New threads and fibers start on Current. For requests and jobs, use the block form once around rendering:
experience = bucket == :b_side ? :refresh : :current
CleoDesignTokens.configure(experience: experience) do
render_user_colours
endThe block returns its result and restores the previous selection on normal exit or exceptions, including nested scopes. Without a block, configuration persists in the current fiber until reset; use the block form for pooled request/job workers so the next user starts with the previous default. Application experiment bucketing and request/job middleware are separate integration work.
Selecting Refresh requires complete synced primitive snapshots in the installed gem; semantic bindings are shared unless explicitly overridden. Invalid experiences or missing/incomplete values raise CleoDesignTokens::UnknownExperienceError before changing either bucket. Successfully built palettes are immutable and shared through a mutex-protected cache; each caller's active selection remains isolated. The API works with Current-only data until Refresh is selected, at which point unavailable snapshots fail clearly.
src/generated/tokenKeys.ts type-checks the arguments on the TypeScript side and is generated, never hand-edited:
ColorPrimitiveKey— 106 membersColorSemanticKey— 475 membersColorTheme—'base' | 'chat' | 'roast' | 'hype'
Ruby — cleo_design_tokens
CleoDesignTokens.colors.semantic.fetch("core.content.primary") # => "#47201C"
CleoDesignTokens.colors.semantic.fetch("core.content.primary", theme: :roast) # => "#F8F6F2"
CleoDesignTokens.colors.primitives.fetch("brown.800") # => "#47201C"Add to a Gemfile:
gem "cleo_design_tokens", path: "path/to/cleonardo/packages/tokens"gem "cleo_design_tokens"The gem is public on RubyGems.org, so Bundler and Dependabot need no registry credentials.
colors returns a frozen Struct of primitives/semantic — lowercase accessors, not CleoDesignTokens::Colors::Semantic.fetch(...) module nesting, so the call text stays identical to the TypeScript reader. PRIMITIVES_LOOKUP/SEMANTIC_LOOKUP are built once at load, into frozen Hashes (values frozen too, SemanticEntry structs frozen too) — safe to read from multiple threads, no lazy ||= race. An unknown key raises CleoDesignTokens::UnknownTokenError rather than returning nil; an unknown theme: raises CleoDesignTokens::UnknownThemeError.
TypeScript — @meetcleo/design-tokens
import CleoDesignTokens from "@meetcleo/design-tokens";
CleoDesignTokens.colors.semantic.fetch("core.content.primary"); // => "#47201C"
CleoDesignTokens.colors.semantic.fetch("core.content.primary", "roast"); // => "#F8F6F2"
CleoDesignTokens.colors.primitives.fetch("brown.800"); // => "#47201C"yarn add @meetcleo/design-tokensThe package is published publicly to npmjs; no registry override or npmjs authentication is required to install it.
Theme is a plain second positional argument here, not the theme: keyword the Ruby reader uses — TypeScript has no equivalent that's as cheap as a positional param, and an options object (fetch(key, { theme })) would buy nothing. Imported as the CleoDesignTokens namespace, not a bare fetch — import { fetch } from "@meetcleo/design-tokens" would shadow the global fetch in that file.
Versioning
The release workflow writes the computed version into package.json before building either package. npm publishes that literal, and lib/cleo_design_tokens/version.rb reads it when the gem loads; the gemspec ships package.json alongside lib/ so that read works from an installed gem too.
The source manifest carries the manual release version. On the first release it is used as-is. On later releases, a manifest version higher than the previous tokens-v* tag is treated as an exact manual override (for a major release); otherwise the previous tag is bumped minor for colour changes or patch for other shipped changes. The release-only commit carries the computed package.json and is the target of that tag; it is not pushed to protected main.
Release
COREEXP-334: .github/workflows/tokens-release.yml publishes on every merge to main that changes a shipped file (tokens/color/**, lib/**, src/**, package.json, the gemspec). One trigger, one version bump, one set of release notes, covering both artefacts:
- Version and release notes come from
scripts/plan-release.mjs, diffing the previoustokens-v*tag against the tree onmain— reusingtransform-core.mjs'sflatten/diffFlat/renderDiffReport, the same functions behind the Figma-sync change report, so a colour change reads identically wherever it's described. A colour change (primitivesorsemantictouched) always bumps minor, never patch —meetcleo's Dependabot config ignores all patch updates, so a patch-versioned colour change would never open a pull request. - npm publishes the public
@meetcleo/design-tokenspackage to npmjs through trusted publishing, using a short-lived OIDC token rather than a repository secret; RubyGems does the same for its public package. The gem'sallowed_push_hostpoints to RubyGems.org. - Shared version, one bump. Both artefacts release under the same version in the same run — there's only one trigger and one colour diff to describe, and
CleoDesignTokens.colors.*is already meant to be one vocabulary across both languages (see Consumers); a version split would just be a second thing to keep in sync for no reader-visible benefit. After both publishes succeed, that version is tagged in the release-only commit.
tokens-v0.1.0 is the first real release, published this way.
Known exceptions
One palette gap. effects.background.glassMorphism has no $ref in any theme: its value (#00000033 — pure black at 20% alpha) matches no primitive. The closest is alpha.dark.20 (#0E060533), which is Cleo's brand-black with alpha rather than pure black. Fixing it needs a design decision: add a pure-black primitive, or realign to alpha.dark.20.
Three roles missing from Base. core.border.level0, core.border.level3 and core.border.level4 exist only in the chat theme. They emit $themes with neither a leaf $ref nor $value, so reading them without a theme raises. That's a gap in Figma, not in the transform — adding them to Base there fixes it.
Colour mapping to consumers
The mapping from these tokens to existing backend/native-app consumers lives in meetcleo/design-tokens on branch feature/COREEXP-320-colour-token-mapping (mapping/**, scripts/mapping.mjs). It is re-homed into this package by a follow-up ticket — not yet done as of this move.
History
Stood up as meetcleo/design-tokens (COREEXP-263). Moved here (COREEXP-321) because cleonardo is the declared design-system home. The original repo is archived; history is preserved there, read-only.
