@dloizides/design-tokens
v1.11.0
Published
Framework-agnostic design-token SOURCE for the dloizides.com portfolio. One typed token set per app (aml / erevna / katalogos), seeded byte-for-byte from each app's CURRENT values, emitted in three forms from one source: CSS custom properties (:root) for
Downloads
632
Maintainers
Readme
@dloizides/design-tokens
Framework-agnostic design-token source for the dloizides.com portfolio — the cross-stack unifier (task #78). One typed token set per app, seeded byte-for-byte from each app's current values, emitted in three forms from one source.
#1 rule — behaviour-preserving extraction, not a redesign. Every preset mirrors the app's existing look. Adopting this package changes nothing visually. Runtime tests assert the RN output deep-equals the app's current
DEFAULT_THEME_CONFIGand the AML CSS reproducesui.css's:rootverbatim.
Values only — no components. (Shared React components live in @dloizides/ui-layout,
ui-forms, ui-feedback, ui-icons, ui-tables.)
The four output forms
One DesignTokens object per app feeds four pure emitters:
| Form | Emitter | Consumer stack | Output |
|------|---------|----------------|--------|
| (a) CSS custom properties | toCssVars(tokens, names, opts?) | Stack-A vanilla-JS (AML console) | a :root { --…: …; } string (+ optional dark-mode block) |
| (b) RN-web theme object | toRnTheme(tokens) | RN-web (erevna, katalogos) | a TenantThemeConfig-compatible object |
| (c) Tailwind preset | toTailwindPreset(tokens) | DOM-React (SyncfusionThemeStudio) | { theme: { extend: { colors, borderRadius, boxShadow, fontFamily } } } |
| (d) Shared UI kit theme | toUiTheme(tokens, mode?) | RN-web on the @dloizides/ui-* kit (ichnos, kefi) | a UiTheme for ui-feedback's UiProvider |
import {
AML_TOKENS, EREVNA_TOKENS, KATALOGOS_TOKENS, PRESETS,
toCssVars, toRnTheme, toTailwindPreset, toUiTheme,
AML_CSS_VAR_NAMES, DEFAULT_CSS_VAR_NAMES,
} from '@dloizides/design-tokens';
// (a) AML console — reproduces wwwroot/shared/ui.css :root exactly
const css = toCssVars(AML_TOKENS, AML_CSS_VAR_NAMES);
// (b) RN-web — drop in as the tenant-theme fallback (deep-equals the current default)
const rnDefault = toRnTheme(EREVNA_TOKENS);
// (c) DOM-React ThemeStudio — spread into tailwind.config.presets
const tw = toTailwindPreset(KATALOGOS_TOKENS);
// (d) Shared UI kit — hand straight to <UiProvider theme={…}>
const uiTheme = toUiTheme(MY_TOKENS);toUiTheme is a LOSSLESS carrier
toUiTheme exists because the naive projection an app writes by hand —
value => ({ '100': v, '500': v, '700': v }) — collapses a scale to one value, silently
discarding the deeper brand step and every status background tint. A provider fed that way
cannot reproduce the palette it was built from, which is what stops a shared component
(StatCard, ThemedTextInput, Field) from rendering on-brand in a consuming app.
Every token lands in an addressable slot, so an app can project its whole palette back out of the live provider rather than keeping a static parallel copy:
| Token | Slot |
|-------|------|
| primary / secondary / accent | palette.primary / .secondary / .accent at UI_MAIN_STEP |
| onBrand | palette.onBrand |
| light.surfaceMuted (or dark.*, per mode) | palette.surfaceMuted |
| semantic.success … | semantic.success at UI_MAIN_STEP |
| semantic.successBg … | semantic.success at UI_TINT_STEP |
The UiTheme shape is declared structurally in this package (as RnThemeConfig declares
TenantThemeConfig), so the zero-dependency / zero-peer guarantee holds: the token source
never depends on the kit it feeds.
The per-app presets
Each preset is seeded from that app's current source of truth:
| Preset | Seeded from | Notes |
|--------|-------------|-------|
| EREVNA_TOKENS | erevna-web/src/theme/presets/default.ts + theme/utils/palette.ts | primary #005f73, secondary #94d2bd, accent #008d5c; full light+dark mode |
| KATALOGOS_TOKENS | katalogos-web/src/theme/presets/default.ts | currently identical to Erevna; kept separate so they can diverge |
| AML_TOKENS | PROOViD/AMLService/.../wwwroot/shared/ui.css :root | brand #4f46e5; dark app-shell + light workspace + verdict palette + radii/shadows/fonts |
RN presets carry no typography/radii/shadows/extensions — the RN theme has no
centralized tokens for those today, so they are omitted (nothing to emit → zero change). The AML
preset carries the richer superset (dark app-shell, ink scale, verdict palette, radii, shadows,
fonts) in its extensions group.
FINREG module colours (FINREG_MODULE_COLORS, 1.5.0)
FINREG is a multi-module product (Payments / Accounting / CRM). Its interim EDA logo binds a
colour to each module — the three orbits are the three modules — so colour reads as module
identity rather than as one app's brand primary. Those tokens therefore sit beside the
per-app presets, not inside one:
| Module | base (fill) | on white | onWhite (AA text/icon) | on white |
|--------|---------------|----------|--------------------------|----------|
| Payments (cyan) | #4FD8F0 | 1.69:1 ✗ | #0C7D91 | 4.82:1 ✓ |
| Accounting (magenta) | #E85FBF | 3.08:1 ✗ | #D11E9B | 4.81:1 ✓ |
| CRM (violet) | #7B8FF7 | 2.95:1 ✗ | #4763F4 | 4.82:1 ✓ |
The base hexes are a decided design input (the logo), transcribed verbatim. They are light
fills — none clears the WCAG AA 4.5:1 text floor on white — so anything painting a module colour
as text or an icon on white uses onWhite, the darkened same-hue companion. The onWhite
values were derived (lightest shade of the hue that still clears AA, carried a little past the
floor) and both halves are pinned by finregModules.test.ts with their measured ratios, so a
drift below AA fails the build. This is the SOURCE of these hexes — consumers read the token, they
do not re-declare the literal (no-raw-color-literal).
Motion tokens (DEFAULT_MOTION, 1.7.0)
Motion is uniform across every portal, so — unlike colour — there is ONE shipped default rather
than one per preset. DEFAULT_MOTION carries durations (ms) and easings:
| Duration | ms | Easing | control points | css |
|----------|----|--------|----------------|-----|
| instant | 0 | standard | [0.2,0,0,1] | cubic-bezier(0.2,0,0,1) |
| fast | 140 | decelerate | [0,0,0,1] | cubic-bezier(0,0,0,1) |
| base | 200 | accelerate | [0.3,0,1,1] | cubic-bezier(0.3,0,1,1) |
| slow | 300 | | | |
instant (0) is the reduced-motion collapse target. Each easing carries both a web css
cubic-bezier(...) string and the bezier [x1,y1,x2,y2] control points, so @dloizides/ui-motion
can build a matching RN Easing.bezier(...) and web + native animate on the same curve. motion is
an OPTIONAL field on DesignTokens (a preset MAY override it), so existing presets/emitters are
byte-identical.
The token shape (aligned to TenantThemeConfig)
The core fields intentionally mirror @dloizides/tenant-theme-web's TenantThemeConfig
(primary, secondary, accent, semantic, light, dark, typography, branding) so a
per-tenant theme override layers cleanly on top of a preset — the tenant blob and the preset speak
the same field names. Fields the core cannot express (a dark app-shell, an ink scale, sanctions
verdict triads) live in an optional extensions group and are documented as Stack-A supersets,
not part of the TenantThemeConfig contract.
Three additions (1.2.0) are core rather than extensions, because they are needed by any app with a status badge or a filled brand button — not by one stack:
| Field | Why it needs its own slot |
|-------|---------------------------|
| light.surfaceMuted / dark.surfaceMuted | The inset surface (table header rows, chips, wells) recedes; surface and surfaceElevated both lift. Neither substitutes. |
| onBrand | The foreground on a primary fill. A contrast partner of primary, so it sits beside it, not in a mode block. Defaults to white; a pale primary must set it. |
| semantic.successBg / warningBg / errorBg / infoBg | A status tint is hand-picked for contrast, not derivable from its base — so it cannot be computed and must be carried. |
All optional, and every emitter emits them only when a preset declares them, so presets that predate 1.2.0 produce byte-identical output.
The cross-stack naming contract (#78b)
Different stacks use different CSS-variable names for the same token. toCssVars takes a
name map (CssVarNames) that says which CSS variable each token field emits as; a field mapped
to undefined is skipped. Two maps ship:
DEFAULT_CSS_VAR_NAMES— canonical kebab names (--primary,--background,--radius, …), with a dark-mode block. Adopt these for a fresh consumer.AML_CSS_VAR_NAMES— the AML console's existingui.cssnames, so nothing there changes.
AML name-mapping table (core → ui.css variable)
| Token field | AML CSS var | Note |
|-------------|-------------|------|
| primary | --brand | |
| secondary, accent | (skipped) | AML is single-brand; ui.css never reads them |
| semantic.success/warning/error | (via --ok/--warn/--bad) | emitted from the verdict extension |
| semantic.info | (skipped) | no AML variable |
| light.background | --bg | |
| light.surface | --surface | |
| light.surfaceElevated | (via --surface-2) | emitted from workspace.surfaceAlt |
| light.text | (via --ink) | emitted from ink.base |
| light.textSecondary | (via --ink-soft) | emitted from ink.soft |
| light.border | --line | |
| light.divider | (skipped) | AML reuses --line |
| dark.* | (skipped) | AML has no light/dark toggle; its dark shell is the shell extension in :root |
| radii.{xs,sm,md,lg,pill} | --r-xs, --r-sm, --r, --r-lg, --r-pill | |
| shadows.{sm,md,pop} | --sh-1, --sh-2, --sh-pop | |
| typography.fontFamily / monoFamily | --font / --mono | |
| extensions.shell.* | --shell, --shell-2, --shell-line, --on-shell, --on-shell-dim | |
| extensions.brand.* | --brand-press, --brand-soft, --brand-ring | |
| extensions.ink.* | --ink, --ink-soft, --ink-faint, --ink-2 | |
| extensions.workspace.* | --surface-2, --line-strong | |
| extensions.verdict.* | --ok*, --warn*, --bad* (base/soft/ink) | |
Open naming decisions (need a human call before wider adoption)
These were resolved conservatively to guarantee zero change, but a human should confirm the intended long-term semantics:
- AML
secondary/accent— AML has a single brand. The preset mirrors--brandinto both so the core shape is complete, butui.cssreads neither. If AML ever gains a real secondary brand, set it here. - AML
dark= the app-shell. AML has a dark shell over a light workspace, not a light/dark mode toggle. The preset seedsdarkfrom the shell so the shape is complete, and the AML name map omitsdark*so no dark-mode block is emitted (the shell is emitted from theshellextension in:root). If a real AML dark mode is introduced, this needs revisiting. semantic.info— AML has noinfocolour; it mirrors--brand. Confirm before relying oninfoin AML.- RN
radii/shadows. The RN apps have no centralized radius/shadow tokens today, so the RN presets omit them. If RN adopts a radius/shadow scale later, decide whether it should share AML's values or define its own.
Adoption (separate tasks — this package ships values only)
This package does not modify any consuming app. Adoption is tracked separately:
- AML: replace the hand-written
:rootinui.csswithtoCssVars(AML_TOKENS, AML_CSS_VAR_NAMES)output. - erevna/katalogos: import
toRnTheme(EREVNA_TOKENS)as thedefaultThemeConfigfallback. - ThemeStudio: spread
toTailwindPreset(...)intotailwind.config'spresets.
Scripts
npm run build (tsup → CJS+ESM+d.ts), npm test (jest, 100% coverage), npm run lint,
npm run typecheck. Publish via ./publish.ps1 patch|minor|major.
