@candide/design-tokens

中文文档
0.8.0发布于 2026/10/30UNLICENSED零依赖近 30 天下载 2250

Candide design tokens for Babylonstoren and The Newt. DTCG source, rendered CSS output.

原始使用文档
文档目录13 节
  1. What this produces
  2. Layout
  3. Token format
  4. What the normaliser does
  5. Types
  6. Viewport
  7. Tiers
  8. Source defects
  9. Tokens Studio setup
  10. Themes
  11. Commands
  12. CI
  13. Known gaps

x-design-tokens

Design tokens for Babylonstoren (bt) and The Newt (tn). Figma is the source of truth. Designers publish from Tokens Studio, this repository renders CSS. AI agents: read AGENTS.md first.

Part of Candide ATMOS, epic EP-1300. This repository is EP-1360. The design is in the Slite specification, sections 8 and 11 to 14.

What this produces

Every build makes two CSS files per brand from the same values. The wrapper is the only difference between them, and it decides which one wins on the page.

FileWrapperShips inRead by
dist/<brand>/tokens.css@layer x-tokens { ... }the npm packagethe shop build, as a baked default
dist/<brand>/runtime.cssbare :root { ... }the GCS bucketthe shop server, pasted into every page

Unlayered CSS beats every @layer, whatever the order and whatever the specificity. That is the platform guarantee the runtime override rests on. A change to a value reaches production without a shop deploy. A change to a token name is a code change, because Tailwind and Angular Material are generated from the names at build time.

Layout

tokens/$metadata.json   the set order Tokens Studio applies
tokens/$themes.json     two themes, one per brand, in one group
tokens/core/tier1.json  brand agnostic ramps: neutrals, utility colours, spacing, borders,
                        letter spacing, font weights, icon and control sizes
tokens/brands/bt/       tier1.json  the brand's own ramps, type scale, radii, shadows
                        tier2.json  the semantic layer
                        tier3.json  the control layer: buttons, forms, links
tokens/brands/tn/       the same three files
contract.json           the legal --x-* names and their types, core tier 1 plus tier 2 and 3
terrazzo.config.ts      one build per BRAND and MODE, reading .tokens-dtcg/
scripts/                normalise, build, emit, and the four checks
.tokens-dtcg/           gitignored, written by every build, never edited by hand

Every set comes straight from a Figma variable collection. Nothing here is hand authored.

Brand tier 1 is private to a brand. Babylonstoren has a blue ramp and Helvetica Neue; The Newt has cream and green ramps and Avenir Next. Naming one of those gives you something that exists in one brand and not the other, so brands/<brand>/tier1 is not in the contract.

Core tier 1 is. Both brands load the identical core/tier1 file, so its spacing scale, borders, letter spacing, font weights, sizes, neutrals and utility colours carry the same names and the same values by construction. It holds 84 tokens, and zero differ between the brands. Excluding them would leave the spacing scale unnameable, which is not a safety property, only a mistake.

So the contract is core/tier1 plus tier2 and tier3, 345 names.

Token format

Two tools read these files and they disagree about how a value is written.

Tokens Studio writes strings. That is the source form in tokens/, because designers own these files and the Figma plugin is what edits them.

{ "$type": "fontSizes", "$value": "14px" }
{ "$type": "color", "$value": "#202e68" }

Terrazzo 2.7.1 requires the DTCG 2025.10 object form. It rejects the string form and exits 1.

{ "$type": "dimension", "$value": { "value": 14, "unit": "px" } }
{ "$type": "color", "$value": { "colorSpace": "srgb", "components": [0.1255, 0.1804, 0.4078] } }

Tokens Studio cannot read the object form. Its plugin prints [object Object] for every value, so the designer sees an empty token set.

scripts/normalise-tokens.mjs resolves this. It converts the source into the object form under .tokens-dtcg/, and every build and check runs it first. The conversion goes one way only. Nothing writes back to tokens/, so the plugin stays the single editor of the source.

What the normaliser does

StepWhy
String values to DTCG objectsTerrazzo rejects strings
Tokens Studio types to DTCG typessee below
Font families gain a fallback stackFigma stores a bare family name, so Helvetica Neue builds as helvetica-neue-lt-pro, "Helvetica Neue", sans-serif, with the Adobe Fonts kit name first
-mobile styles folded into the base, per modesee Viewport
Same name aliases across tiers collapsedsee Tiers
Illegal names and dangling aliases droppedsee Source defects

Types

Tokens Studio groups the plugin's token list by its own type vocabulary, which is finer than DTCG's, and it shows no Dimension section at all. A token typed dimension therefore appears nowhere in the plugin, however correct the CSS it produces.

A Figma import types almost everything dimension, so the source carries the finer type and the build maps it down.

Source $typePlugin sectionBuilt as
spacingSpacingdimension
sizingSizingdimension
borderRadiusBorder Radiusdimension
borderWidthBorder Widthdimension
fontSizesFont Sizedimension
lineHeightsLine Heightdimension
letterSpacingLetter Spacingdimension
fontWeightsFont WeightfontWeight
fontFamiliesFont FamilyfontFamily
textCaseText Casestring
colorColorcolor

Shadow parts are typed sizing. Figma models a shadow as five separate variables rather than one composite, and Tokens Studio has no type for a shadow part, so sizing is the nearest bucket that keeps them visible.

npm run check:source fails on any token typed dimension or text, because both are invisible: the plugin does not list dimension, and Terrazzo silently emits nothing for text. Re-importing from Figma reintroduces both, so the check is what stops half the library quietly disappearing.

contract.json records the built type, not the source type, because the contract is what the shop lints against.

Letter spacing is a percent without the sign. Figma variables cannot hold %, and Tokens Studio silently skips a "10%" value on push, which is how button-lg lost its letter spacing in Figma. So letter spacing is written as a bare number of percent, "10" for 10%, which is also what the Figma text styles set. The normaliser reads a bare letterSpacing number as percent and builds 0.1em. Values with a unit, like "1px", are left alone.

Font weight is a unit trap. Figma stores a weight as a FLOAT, and the import option that usefully turns spacing into dimensions also turns weight 400 into 400px. That is invalid CSS and a browser drops the declaration. The source types weights as fontWeights, and the normaliser strips a trailing px from any weight, so a re-import cannot bring it back.

Viewport

Figma bakes the viewport into the token name. The unsuffixed style is desktop, and -mobile is the variant. This repository does the opposite, building one permutation per mode and diffing them into a media query.

The normaliser folds one into the other. On a mobile build each x-mobile value overwrites x; on a desktop build the -mobile tokens are simply dropped. Either way the CSS carries one name per property, so a consumer writes --x-typography-body-default-font-size and never thinks about viewport.

A -mobile token that aliases its own desktop twin is a no-op and is skipped. If it aliases a different property of that twin, that is a mis-wire in Figma and the source check fails.

Tiers

In Figma the tiers are separate collections, so tier 2 can hold border.width.sm that references tier 1's border.width.sm. Flattening the collections into one namespace turns that into a token that points at itself.

The normaliser collapses those: when a token in a higher tier aliases a token of the identical name in a lower tier, the higher one is removed and the lower one's value stands. That is what the Figma reference meant.

Source defects

Figma is the source of truth, so defects are fixed in Figma, never patched here. npm run check:source names each one and fails.

Two kinds are also dropped from the build, so one bad variable cannot take down a whole brand's stylesheet:

  • An illegal name. A Figma variable called disabled 2 would emit --x-...-disabled 2, which is not a legal custom property and breaks the CSS rule around it.
  • A dangling alias. A reference to a variable the brand does not define has no correct value, so shipping nothing beats shipping a wrong one.

Tokens Studio setup

Point the Figma plugin at this repository under Settings, Sync, GitHub.

FieldValue
Repositorycandide-com/x-design-tokens
Brancha tokens/<name> branch, so the preview workflow fires on the push
File pathtokens
Token formatW3C DTCG

The tokens file path and the tokens/ branch prefix are unrelated. One is a folder, the other is a branch name.

Themes

$themes.json holds two themes, Babylonstoren and The Newt, in one group called Brand. A group is exclusive, so a designer picks one and the plugin turns the other off. Without a group the themes are independent switches and both brands can be on at once, which resolves to whichever sits later in tokenSetOrder.

Viewport is not a theme, because Figma carries it in the token name rather than in a separate collection.

selectedTokenSets lists only the enabled sets. Tokens Studio omits the rest and treats absent as disabled. Writing them out explicitly means the plugin's copy and the file say the same thing but do not match, so it offers to push a config change every time someone opens it, forever.

npm run check:sets fails when a theme has no group, when the themes span more than one group, when a theme lists a set as "disabled", or when a theme's enabled sets differ from what the build compiles.

Both $themes.json and $metadata.json are written by the plugin. Do not hand-edit them beyond what the checks require.

Commands

npm run validate        # check the sets, the source, the schema, build, check the contract
npm run build           # four Terrazzo builds, then emit the two CSS shapes
npm run check:sets      # sets are wired, present, ordered, and covered by the themes
npm run check:source    # names and aliases in the Figma source are sound
npm run check           # DTCG schema and lint, for all four brand and mode combinations
npm run check:contract  # every contract name is present in every build, with a valid value

To regenerate contract.json after design adds or removes a token:

node scripts/check-contract.mjs --write

CI

WorkflowRuns onDoes
validateevery pull request, every push to mainnpm run validate. Publishes nothing.
previewa push to a tokens/** branchoverwrites <brand>/preview.css, 15 second cache, then posts or updates one status comment on the pull request. A shop server reads the file where DESIGN_TOKENS_URL points at it.
preview reseta tokens/** pull request is merged or closedrebuilds <brand>/preview.css from main, but only if the preview still shows a commit of that pull request.
releaseevery push to main, and a v* tagOn main: works out the next version, commits it, tags it, then publishes. On a tag: publishes that tag. Publishing is npm, then the immutable version copy, then <brand>/production.css, 60 second cache.
rollbackmanual dispatchrestores production.css from an immutable copy.

A merge to main is a production release. scripts/next-version.mjs builds main and compares it with the last released npm package:

Change since the last releaseRelease
nothingnone
a valuepatch
a new nameminor
a removed or retyped namenone, the job fails until a person sets the major version in package.json

A version that a person sets in package.json in the pull request wins over the computed one, as long as it is higher than the last release. If main moves while the job runs, the push of the version commit fails and nothing is published. The next merge releases both changes.

Preview carries --x-tokens-version: "0.0.0-preview.<short sha>", so a page names the commit it is running. After a reset, the sha is the main commit.

The two preview workflows share one concurrency group, preview-channel, so they never write preview.css at the same time.

The bucket workflows read these repository variables and one secret. Set them under Settings, Secrets and variables, Actions.

NameKindExample
BUCKETvariablegs://some-bucket
PREFIXvariabledesign-tokens
PREVIEW_SHOP_URL_BTvariable, optionalthe Babylonstoren shop that reads the preview, linked in the status comment
PREVIEW_SHOP_URL_TNvariable, optionalthe same for The Newt
GCP_SA_KEYsecretthe service account JSON

The bucket has to be publicly readable. The shop server fetches the CSS over plain HTTPS with no credentials, so a private bucket breaks the runtime path in production while every CI job still passes.

Known gaps

  1. No shop environment reads preview.css yet. The shop server injects the bucket CSS only where DESIGN_TOKENS_URL is set, and no review, stage or production environment sets it yet. Until one does, check a preview on a local shop with DESIGN_TOKENS_URL pointed at the brand's preview.css.
  2. Preview is shared. There is one preview.css per brand, so two open token branches overwrite each other, and the last push wins. The status comment on each pull request says which commit was published, and preview reset never wipes a newer push from another branch.
  3. Fonts come from Adobe Fonts kits, which publish them under their own names: helvetica-neue-lt-pro (kit ban3byn), adobe-caslon-pro and avenir-next-lt-pro (kit pah0waz). Figma has to keep the real family names, so the normaliser puts the kit name first and the Figma name after it as a fallback. The shop loads the kits; this package only names the families.

版本与发布记录

@candide/design-tokens 共发布 14 个版本,首次发布于 2026/09/22;过去 12 个月发布了 14 个版本。

版本发布日期距今
0.8.0latest2026/10/3010 天前
0.7.12026/10/2911 天前
0.7.02026/10/2713 天前
0.6.22026/10/2614 天前
0.6.12026/10/2614 天前
0.6.02026/10/2614 天前

关于 @candide/design-tokens 的常见问题

@candide/design-tokens 是什么?

Candide design tokens for Babylonstoren and The Newt. DTCG source, rendered CSS output.

如何安装 @candide/design-tokens?

在项目目录运行 npm install @candide/design-tokens,或使用 pnpm add / yarn add / bun add。

@candide/design-tokens 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/candide__design-tokens 是否存在。

@candide/design-tokens 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@candide/design-tokens 可以商用吗?

@candide/design-tokens 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@candide/design-tokens 的下载量有多少?

根据 npm 官方下载统计,@candide/design-tokens 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,250 次,日均约 75 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@candide/design-tokens 还在维护吗?

最新版本 0.8.0 发布于 2026/10/30(10 天前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/09/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。