SIAX Technology (sax3l)

@siax/tokens (0.1.2)

Published 2026-09-09 22:31:06 +00:00 by admin

Installation

@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/
npm install @siax/tokens@0.1.2
"@siax/tokens": "0.1.2"

About this package

@siax/tokens

The single source of truth for SIAX design token values. Everything else in DES1GN — @siax/ui's CSS, Tailwind utilities, future native targets — should eventually be derived from this package, not the other way around.

Where the values actually come from

Canonical origin: sax3l/siax-io, app/globals.css (1866 lines, OKLCH, heavily commented), vendored verbatim at reference/siaxio-app-globals.css @ commit 905b425b3a1260f8d8c1570769a8a5ce50b62ef4.

This corrects the provenance recorded in the first version of this package. packages/ui/src/globals.css was cited as the source of truth; it is not — it is a reduced derivation of the canonical sheet. Measured against the canonical file, @siax/ui's 111-line sheet carries colour and radius only, and silently omits:

Missing from @siax/ui/src/globals.css Count Canonical location
--sidebar-* 8 × 2 themes globals.css:73-81, :110-117
--elevation-1..5 5 × 2 themes globals.css:1768-1791, :1797-1815
--depth-shadow, --depth-shadow-lifted, --lift-hard 3 × 2 themes globals.css:1611-1627, :1638-1648
--duration-* (6), --ease-* (5) 11 globals.css:1742-1755
--perspective, --spatial-ease, --spatial-duration 3 globals.css:1604-1632
--z-base/raised/dropdown/modal/toast 5 globals.css:1758-1762
data-universe accent system 7 universes × 2 themes globals.css:120-140
Fluid type scale (.fluid-*) 6 steps globals.css:210-240
Filmic layer (.filmic-*) 6 utilities globals.css:242-287

All of the above are now modelled here. The values @siax/ui did carry were re-verified against the canonical source and every one matched byte-for-byte — nothing had drifted, the sheet was merely incomplete.

Five colour tokens go the other way: success, success-foreground, warning, warning-foreground and overlay exist in @siax/ui but not in siax.io. They are kept (the admin consoles need status colours and a modal scrim) and are flagged as INTENDED_ADDITIONS in build.mjs so the verify pass reports them as deliberate rather than as drift.

What is DTCG, and why does this package use it

DTCG (Design Tokens Community Group) is the W3C community group format for design tokens: plain JSON where every token is an object with a $value and a $type key ({"$type": "color", "$value": "oklch(0.5 0.2 285)"}), and files use the .tokens.json extension. It went stable in October 2025. Before this, every design-tokens tool (Figma Tokens Studio, Style Dictionary, Specify, Superposition, …) invented its own JSON shape, and syncing between them meant writing a converter. DTCG gives all of them one shape to agree on.

Concretely, for SIAX this means:

  • Figma sync becomes possible. The Tokens Studio plugin reads and writes .tokens.json files directly. Point it at packages/tokens/tokens/ and a designer can push a token change straight into this repo as a PR, or pull this repo's tokens into Figma — no manual re-typing of hex/oklch values in either direction.
  • New output targets stop being a rewrite. Today the only consumer is CSS custom properties. When h0st/VehIQ need native tokens (iOS Color.xcassets/Android XML, or a Flutter ThemeData), that's a new Style Dictionary platform pointed at the same .tokens.json source — not a second hand-maintained token sheet that can drift from the CSS one.
  • Tokens become machine-checkable. pnpm --filter @siax/tokens test rebuilds and diffs the output against the canonical siax-io sheet, so a typo'd value fails a script instead of shipping unnoticed.

Why Style Dictionary v4

Style Dictionary is the transform engine: it reads .tokens.json, resolves references, and emits whatever target format you configure (CSS, JS, iOS, Android, …). Version 4 is the release that treats DTCG ($value/$type) as its native input format — earlier versions expected Style Dictionary's own pre-DTCG shape (value/type, no $). Using v4 means the token source can be pure DTCG with no Style-Dictionary-specific dialect mixed in, which is what keeps it Tokens-Studio-compatible.

Layout

packages/tokens/
  reference/
    siaxio-app-globals.css   VENDORED canonical source (sax3l/siax-io @ 905b425).
                             Read-only. `pnpm test` diffs against this.
  tokens/
    color/
      light.tokens.json      :root colours
      dark.tokens.json       .dark colours
    sidebar/
      light.tokens.json      --sidebar-* (8), light
      dark.tokens.json       --sidebar-* (8), dark
    elevation/
      light.tokens.json      --elevation-1..5, --depth-shadow*, --lift-hard, light
      dark.tokens.json       ditto, dark
    universe/
      README.md              the data-universe system explained
      <universe>/
        light.tokens.json    --accent, --ring
        dark.tokens.json     --accent, --accent-foreground, --ring
      … one directory per named universe (7)
    motion.tokens.json       --duration-*, --ease-*, --perspective, --spatial-*
    z-index.tokens.json      --z-*
    typography.tokens.json   --font-sans/-mono + the fluid type scale
    radius.tokens.json       --radius (0rem, DESIGN_GUARD: stays 0)
    marketing-only/
      filmic.tokens.json     NOT part of the platform set — see "Filmic" below
  build.mjs                  Style Dictionary v4 build + verification
  scripts/
    validate-tokens.mjs      structural lint (`pnpm lint`)
  dist/                      generated — do not hand-edit
    css/variables.css         platform layer: :root, .dark, 14 universe blocks
    css/filmic.css            marketing layer, separate on purpose
    js/tokens.js, .d.ts       typed objects: { light, dark, universes }

The universe accent system (data-universe, "Boken §2.3")

Eight universes exist. Seven are named and carry their own accent hue; the eighth, core, renders no attribute at all and therefore inherits the default SIAX purple. There is no universe/core/ directory by design — core is the absence of an override.

Universe Hue (H) Light --accent Dark --accent
core (no attribute) 285 oklch(0.45 0.23 285) oklch(0.65 0.25 285)
revops 150 oklch(0.45 0.12 150) oklch(0.70 0.14 150)
dev 240 oklch(0.45 0.19 240) oklch(0.68 0.15 240)
automation 60 oklch(0.47 0.11 60) oklch(0.75 0.14 60)
effekt 330 oklch(0.46 0.20 330) oklch(0.68 0.21 330)
optimering 200 oklch(0.45 0.09 200) oklch(0.72 0.12 200)
bilhandlare 30 oklch(0.48 0.14 30) oklch(0.72 0.15 30)
editorial 270 oklch(0.40 0.04 270) oklch(0.75 0.04 270)

Light overrides --accent and --ring; dark additionally overrides --accent-foreground. Nothing else — the @theme inline mapping cascades the rest. The attribute goes on a route/template wrapper, never on an individual component; the slug → universe mapping lives in siax-io's lib/site-config.ts (universeForPath).

Because --lift-hard and --filmic-glow-shadow are built on color-mix(… var(--accent) …), they re-tint per universe automatically. No other shadow token does.

#open-questions

What is "Boken"? Every universe rule in the canonical CSS cites it (app/globals.css:120: "Universum-accenter (Boken §2.3)"), and the citation is load-bearing: it is what locks the hue and permits only lightness to be revised. A repo-wide search of sax3l/siax-io @ 905b425 finds 29 references across 22 files, citing §1.5, §2.3, §4.4, §25, §27, §33, §37, §45.1/A2, §50.3, kap 12/36 and kap 40 — i.e. a numbered document of at least 50 chapters governing accent policy, section numbering, CTA copy, FAQ patterns, newsletter components and the design-guard baseline ratchet.

The document itself is not in the repository. find -iname '*boken*' returns nothing; no .md contains it; and no reference is ever a link — every citation is bare prose in a code comment. Its content is only ever visible second-hand, through the rules quoted at those 29 sites.

Status: OKÄNT. It could be a Notion/Drive document, a printed brand book, or Simon's own working notes. Until it is located and archived, §2.3 cannot be re-derived — so if a universe hue is ever questioned, the CSS values in this package are the only surviving record of it. Recommendation: locate "Boken", commit a copy (or an authoritative extract) to DES1GN, and link it from here.

Font-stack divergence. @siax/config/tailwind-preset.css declares --font-sans: var(--font-inter), 'Inter', ui-sans-serif, … (next/font wiring) while the canonical source declares --font-sans: 'Inter', system-ui, -apple-system, sans-serif. Both are tracked; neither was changed here. Reconciling them is a @siax/config decision, not a token-value question.

Filmic — why it is segregated, not deleted

tokens/marketing-only/filmic.tokens.json compiles to its own artifact, dist/css/filmic.css, exported as @siax/tokens/filmic. It is never concatenated into dist/css/variables.css, and --verify fails the build if a filmic token ever appears in both.

The call: marketing-site-only. Evidence, all from siax-io @ 905b425:

  1. The layer's own spec (docs/FILMIC-DESIGN-SYSTEM.md) states its purpose as "Låt video, ASCII och Simon bära det visuella" — a hero video, a WebGL/ASCII renderer, and the founder-presence system. An admin console has none of those.
  2. --filmic-overlay-gradient is literally a scrim for video.
  3. Real adoption is one call site: filmic-kicker in components/navigation/mega-menu.tsx:118. The other five utilities appear only in documentation.
  4. The source header calls them "opt-in" (app/globals.css:243) — never a baseline.
  5. They are stylistic duplicates of the platform scale, not additions to it: .filmic-display is a louder .fluid-title-hero, .filmic-lead a more generous .fluid-body. Shipping both to an admin OS would give two competing display scales with no rule for choosing between them.

Recorded counter-argument: the kicker idiom (mono, uppercase, wide tracking) genuinely is platform-wide — ContentHub eyebrows, section headings, card meta labels. If that gets tokenised for the platform it should be re-derived under a neutral name (eyebrow-*) in the main set, not by promoting this file; these values are tuned against a full-bleed video backdrop.

How the build works

node build.mjs (via pnpm --filter @siax/tokens build, or pnpm tokens:build from the repo root).

Style Dictionary resolves one merged token tree per build, and this system reuses the same token names under many different selectors — --accent exists in :root, in .dark, and in fourteen [data-universe] blocks with a different value each time. Merging them would make later files silently clobber earlier ones. So build.mjs runs Style Dictionary once per selector (16 platform passes + 1 filmic pass), then stitches the blocks together in the canonical cascade order. Same "build per theme, merge after" pattern Style Dictionary's docs recommend for multi-brand setups, extended from 2 passes to 17.

No colour-value transform is registered. Style Dictionary's built-in colour transforms go through culori and can re-round or reformat oklch() strings; this build needs byte-for-byte fidelity, so every $value is emitted exactly as authored. The only transforms are on the name (the DTCG path, kebab-joined) and two structural composers described below.

#spec-deviations

DTCG cannot losslessly express several things this design system actually uses. Where that happens the literal CSS is the $value, and the reason is recorded on the token. The complete list:

$type Form used Why not the spec form
color CSS string, "oklch(0.45 0.23 285)" See #why-string-color-values
shadow CSS string Multi-layer, plus inset, plus color-mix(in oklch, var(--accent) …). DTCG's shadow composite can express none of those and would silently drop layers.
gradient CSS string DTCG gradients are (color + position) stops with concrete colours; this one references var(--background) and color-mix().
duration CSS string, "0.25s" DTCG wants a (value + unit) object; string keeps byte fidelity.
dimension CSS string Needed for clamp(), em, ch, vw — none of which DTCG dimensions support.
fontFamily CSS string (whole stack) The source quotes some names and not others ('Inter', system-ui). Splitting to an array would require re-deriving quoting on output, which is not lossless.
cubicBezier spec-legal 4-number array The one composite kept in true DTCG form; build.mjs composes cubic-bezier(a, b, c, d).

For the fluid type scale, DTCG has no representation for clamp(min, Nvw + Mrem, max) at all. The literal is the $value, and the decomposition (min / preferredVw / preferredRem / max) lives in $extensions["io.siax.tokens"].fluid, which DTCG explicitly reserves for vendor data. build.mjs recomposes the clamp() from those parts and fails the build if it does not match the $value, so the documentation cannot silently disagree with the value.

#why-string-color-values

The DTCG color spec also defines an object form for $value ({"colorSpace": "oklch", "components": [...], "alpha": ...}). This package uses the plain CSS string form instead ("oklch(0.964 0.006 265)") for two reasons: it round-trips through Style Dictionary with zero risk of reformatting (see above), and Tokens Studio's own OKLCH support is newer and less consistent than its plain-string "custom color" handling. If/when the object form is broadly supported everywhere this package's output needs to travel, migrating is a source-file change only — every consumer of dist/ is unaffected either way.

Verifying the output matches the source

pnpm --filter @siax/tokens test     # node build.mjs --verify

The verifier does five independent things, and each one has been proven to fail on a deliberately injected fault:

  1. Custom properties, selector by selector. Both reference/siaxio-app-globals.css and the freshly built dist/css/variables.css are parsed into { selector -> { property -> value } } maps and diffed. The parser is a brace-balanced walk, not a flat regex: the canonical sheet declares :root three separate times (lines 16, 1601, 1741) and .dark three times (84, 1637, 1796), nests blocks inside @media/@layer/@supports/@keyframes, and uses attribute selectors — a naive /sel\s*\{([^}]*)\}/ mis-reads all of it. Repeated selectors are merged the way the cascade merges them.
  2. Font stacks are cross-checked against the canonical @theme inline block, since that is where the source declares them.
  3. The fluid scale is cross-checked against the literal font-size / line-height / letter-spacing inside each .fluid-* class body — in both directions, so inventing a letter-spacing for a step that has none upstream fails just as loudly as dropping one that does.
  4. The filmic layer is cross-checked the same way against the .filmic-* class bodies, and asserted to be absent from the platform artifact.
  5. Cascade order is asserted. Values are only half the contract; which block wins is the other half.

Anything not on the INTENDED_ADDITIONS allowlist fails the build.

Current status: 137 canonical declarations across 16 selectors reproduced value-for-value; 0 unintended differences; 26 intended, allowlisted additions (5 DES1GN-only colours × 2 themes, 2 font stacks, 14 fluid-scale properties). Run the command above for the itemised list.

Adversarial checks performed on the verifier itself

A verifier that always passes is worse than none. These faults were injected and all were caught:

Injected fault Caught as
Dark --accent L 0.65 → 0.66 VALUE MISMATCH
editorial dark chroma 0.04 → 0.05 VALUE MISMATCH on the universe selector
Fluid preferredVw 4 → 4.5 (desynced from its literal) build fails before emit
--lift-hard deleted from the dark elevation set in canonical source but MISSING from build
A filmic token moved into the platform set SEGREGATION BREACH (×2) + not-allowlisted
Pass order reversed CASCADE ORDER differs

Bug fixed in this pass

The previously committed dist/ was broken, in a way nothing tested for. Both generated headers contained the glob **/*.tokens.json inside a /** … */ block comment. **/ closes the comment:

  • dist/js/tokens.js was not valid JavaScript — node --check reports SyntaxError: Unexpected token '*', so import '@siax/tokens' would have thrown at load.
  • dist/css/variables.css leaked comment prose into the stylesheet ahead of :root, a parse error at the top of the sheet.

Neither surfaced because no consumer imports dist/ yet. The headers no longer emit */.

Adding or changing a token

  1. Edit the relevant tokens/**/*.tokens.json file — never dist/, it's regenerated. Never edit reference/ — it is the upstream copy.
  2. pnpm --filter @siax/tokens lint — structural DTCG check.
  3. pnpm --filter @siax/tokens build.
  4. pnpm --filter @siax/tokens test — the five-part verification above.

Changing a value that exists upstream will (correctly) fail step 4: the canonical source is siax-io, so the change belongs there first, followed by re-vendoring reference/siaxio-app-globals.css and bumping REFERENCE_COMMIT in build.mjs. That friction is the point.

Note on $description: Style Dictionary v4 treats {…} inside any token string, including $description, as a token reference and hard-fails when it cannot resolve it. Do not write CSS snippets or object shapes with braces in prose — use parentheses.

Figma (Tokens Studio) — future wiring

Not wired up yet, but the source is already in the right shape for it. When this is picked up:

  1. Install the Tokens Studio for Figma plugin.
  2. Point its "Sync providers" at this repo (Gitea git.siax.io/sax3l/DES1GN) using its Git/GitHub-compatible sync provider, file path packages/tokens/tokens/.
  3. Tokens Studio reads each *.tokens.json as a token set. Map them to Tokens Studio "themes": lightcolor/light + sidebar/light + elevation/light + radius + motion + z-index + typography; darkcolor/dark + sidebar/dark + elevation/dark. Each universe becomes an additional enabled-on-top set, mirroring how [data-universe] overrides only --accent/--ring in CSS.
  4. Designer edits in Figma become PRs against .tokens.json — reviewed like any other code change, then pnpm tokens:build regenerates dist/.

Still out of scope (nothing guessed)

  • --radius-sm/md/lg/xlcalc(var(--radius) ± Npx) expressions living in the @theme inline mapping layer (globals.css:170-173, @siax/config/tailwind-preset.css), not values. Since --radius is 0, two of them compute to negative radii that CSS clamps to 0.
  • Spacing scale.section-padding, .section-gap, .content-gap (globals.css:293-304) are clamp() literals in class bodies with no custom property behind them. Tokenisable by the same $extensions pattern used for the fluid scale; not done here because nothing asked for it and the naming (section-* vs a generic space-* scale) is a product decision.
  • Container widths.container-narrow/content/wide/full (680/900/1400/ 1800px, globals.css:634-656), same reasoning.
  • Animation keyframes — ~30 @keyframes blocks. These are behaviour, not values; the --duration-*/--ease-* scale they should be built from is now tokenised, but most utilities still inline cubic-bezier(0.32, 0.72, 0, 1) as a literal. Migrating them to var(--ease-snappy) is a follow-up in siax-io, not here.
  • The --color-* / @theme inline mapping — owned by @siax/config.

Status

Re-derived from the canonical siax-io source 2026-08-09. This package builds and self-verifies, but @siax/ui's globals.css has not yet been changed to consume it — a separate, already-assigned follow-up, kept separate so nothing breaks mid-migration. Right now @siax/tokens is a superset of @siax/ui/globals.css that additionally agrees with siax.io, checked by pnpm --filter @siax/tokens test.

Dependencies

Development Dependencies

ID Version
style-dictionary ^4.4.0
Details
npm
2026-09-09 22:31:06 +00:00
2174
UNLICENSED
latest
40 KiB
Assets (1)
Versions (3) View all
0.1.2 2026-09-09
0.1.1 2026-08-09
0.1.0 2026-08-09