@bitbaum/design-tokens

中文文档
1.5.2发布于 2026/11/03MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 6834

Design SSOT for OrangeCat, Loki and Solon — tokens, self-hosted faces, the Tailwind preset that maps them, and next/font/local calls for every face the fleet's apps use.

原始使用文档
文档目录10 节
  1. Why
  2. Retheming the entire stack
  3. Install
  4. Optional cross-product composition grammar
  5. The contract
  6. Typefaces
  7. Faces for generated images ( next/og )
  8. Faces for Next apps ( next/font/local )
  9. Why a serif
  10. Data maps

@bitbaum/design-tokens

The design single source of truth for OrangeCat, Loki and Solon.

One file — tokens.css — defines every colour, typeface, weight, tracking, radius and shared primitive the three products use. They import it. Nothing is copied.

Why

These tokens used to be hand-copied into three repos, and they drifted. Solon's globals.css carried a comment claiming its tokens matched OrangeCat's while sharing zero names or values with it — so the three products looked like three different companies. Copies drift; imports cannot.

Retheming the entire stack

  1. Edit the block marked ▼▼▼ THE KNOBS ▼▼▼ in tokens.css for a retheme, or update the optional composition.css contract for interface improvements.
  2. Bump package.json's version and set the matching explicit .github/release-request.txt; submit a PR and pass package CI.
  3. A successful main-branch push CI triggers tag-approved-release.yml. It tags only the exact latest verified SHA and never rewrites an existing version tag.
  4. The tag workflow explicitly dispatches publish.yml; npm publishing is still guarded by NPM_PUBLISHING and requires trusted-publishing bootstrap. If that variable is off, a Git tag exists but no npm release is claimed.
  5. Upgrade consumers to the confirmed Git tag, published npm version, or an exact source-commit SHA; run their frozen-lockfile CI.

The one-time release request does not automatically approve future version bumps: a request must match the manifest version.

Changing the display typeface is one line:

--font-display: "Instrument Serif", Georgia, serif;
--font-display-weight: 400;   /* the face's weight lives WITH the face */
--tracking-display: -0.015em; /* and so does its tracking */

Install

// package.json
"@bitbaum/design-tokens": "^1.2.0"
// app/layout.tsx — import BEFORE the app's own globals.css
import "@bitbaum/design-tokens/tokens.css";
import "./globals.css";
// tailwind.config.js (v3)
module.exports = {
  presets: [require("@bitbaum/design-tokens/tailwind-preset")],
  content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
};

Tailwind v4 apps consume tokens.css through @theme; the variable names are identical, so both paths stay in sync.

Optional cross-product composition grammar

The brand tokens above intentionally belong to OrangeCat/Loki/Solon. Their shared palette should never be imposed on unrelated products or customer websites.

For spacing, control sizing, visual rhythm and interaction decisions, composition.css is an opt-in, namespaced layer. It defines no fonts, brand colours, body style or global reset.

// App Router — load AFTER the app's CSS reset and BEFORE product CSS modules.
// The shared classes intentionally use :where() specificity, so importing
// them before a later universal '*' padding reset would erase their padding.
import './globals.css';
import '@bitbaum/design-tokens/composition.css';
<main className="bb-frame">
  <div className="bb-stack" style={{ '--bb-stack-gap': '1.5rem' }}>
    <div className="bb-measure">A product-specific headline and description</div>
    <div className="bb-actions">
      <a className="bb-button bb-button--primary" href="/start">Get started</a>
      <a className="bb-button bb-button--secondary" href="/method">Read the method</a>
    </div>
  </div>
</main>

Map only the small seam of semantic CSS roles once in each app:

:root {
  --bb-action-bg: var(--color-action);
  --bb-action-fg: var(--color-on-action);
  --bb-control-bg: var(--color-surface);
  --bb-control-fg: var(--color-text);
  --bb-line: var(--color-border);
  --bb-muted: var(--color-text-muted);
  --bb-focus: var(--color-focus);
}

The exported grammar includes bb-frame, bb-measure, bb-section, bb-stack, bb-cluster, bb-grid, bb-actions, bb-choice-row, bb-list, bb-keyline, bb-tabular, bb-control, bb-button, and bb-skip.

It guarantees 44px controls, visible focus, responsive equal-cell choices, and reduced-motion support. Apps own typography, colours, spacing adjustments, components and their content. Do not render all information as cards: the grammar deliberately offers whitespace and hairline lists.

Release contract: the new composition subpath first appears in source version 1.2.0; v1.1.0 does not include it. The verified tagging pipeline only tags a version explicitly requested in .github/release-request.txt after main CI is green. Verify the v1.2.0 Git ref and the registry separately before claiming either is published. Until then, use a verified exact commit SHA. Existing tokens.css consumers do not change appearance by opting out.

The contract

Components declare intent, never mechanics.

DoDon'tWhy
className="font-display"font-['Instrument_Serif']the face is not a component's business
className="font-display"font-display font-boldthe display face has one weight; a synthesized bold looks cheap
className="bg-surface-base"bg-[#1c1c1c]retheming must not require finding hex codes
className="text-accent"text-[#ff5c00]one action colour, one definition
bg-accent text-on-accentbg-accent text-whitewhite on the accent is 3.10:1 and fails AA; the paired ink is 6.10:1
className="tabular"—numbers that change in place must not jitter
font-display at ≥text-2xlfont-display text-lgbelow 24px the high-contrast serif reads lighter than the body sans and hierarchy inverts
className="wordmark"font-display on the logouppercase + open tracking is the one small size a display serif survives

Enforce it in each repo's design gate so drift fails the build rather than shipping quietly.

Typefaces

Self-hosted (latin + latin-ext), no runtime requests to Google:

RoleFaceWeightsCarries
displayInstrument Serif400 onlyheadlines, wordmarks
sansInter400–700 variableall UI and body text
monoIBM Plex Mono400, 500addresses, signatures, hashes, tallies

All three are SIL Open Font License.

Faces for generated images (next/og)

Given no fonts, next/og's ImageResponse draws with the Geist Regular it bundles, and for any character no font covers it asks fonts.googleapis.com (and cdn.jsdelivr.net for emoji) while the image is drawn: a build that prerenders an image, or a server that draws one, then depends on Google, and offline an emoji fails the whole image. Pass the faces from here instead:

import { ogFonts, ogText } from '@bitbaum/design-tokens/og';

return new ImageResponse(<Card title={ogText(post.title)} />, {
  ...size,
  fonts: await ogFonts(),
});
  • ogFonts() is Geist 400, the face next/og would have used, so the image is unchanged. ogFonts('inter:400,600') or ogFonts('inter') (every weight shipped) names others; each font is named after its family, and a fontFamily no font has falls back to the first.
  • ogText(text, ...specs) leaves out what none of the faces draws: a word missing a letter (another script) whole, an emoji or symbol on its own. Use it on text the image does not control, such as a name or a title from a database.
  • For such text, pass Inter after Geist: ogFonts('geist', 'inter:400') and ogText(text, 'geist', 'inter:400'). Satori draws with Geist and takes what Geist lacks (Greek, Vietnamese, ₿, ✓, many arrows) from Inter, without a fetch; what Geist draws looks as before.
  • The renderer reads neither WOFF2 nor variable fonts, so these are static instances stored as WOFF in JS modules (og/faces/): any runtime (edge or Node) and any bundler imports them, and a route loads only the faces it names. Shipped: Geist 400, Inter 400 and 600. A face it lacks is added to OG_FACES in scripts/og-fetch-fonts.mjs and released, never fetched.

Faces for Next apps (next/font/local)

next/font/google downloads its faces during next build, so a build fails whenever Google Fonts does not answer. The faces the fleet's apps use ship in fonts/<face>/ instead, each beside its OFL.txt, and fonts/faces.json lists every file with the subset, style, weight range, unicode-range and SHA-256 Google serves it under:

Face (slug)WeightsStylesSubsets
Inter (inter)100–900 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, greek, greek-ext, vietnamese
Space Grotesk (space-grotesk)300–700 variablenormallatin, latin-ext, vietnamese
IBM Plex Mono (ibm-plex-mono)400, 500, 600normallatin, latin-ext, cyrillic, cyrillic-ext, vietnamese
Fraunces (fraunces)100–900 variablenormal, italiclatin, latin-ext, vietnamese
Cormorant Garamond (cormorant-garamond)300–700 variablenormal, italiclatin, latin-ext, cyrillic, cyrillic-ext, vietnamese
DM Sans (dm-sans)100–1000 variablenormallatin, latin-ext
Geist (geist)100–900 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, vietnamese
Geist Mono (geist-mono)100–900 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, symbols2, vietnamese
JetBrains Mono (jetbrains-mono)100–800 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, greek, vietnamese
Montserrat (montserrat)100–900 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, vietnamese

next/font takes only literal options in the calling module, and one call carries one unicode-range, so an app gets its font module from a command rather than from a helper:

pnpm exec design-tokens-next-fonts src/app/fonts.ts \
  'sans=inter;variable=--font-inter' \
  'display=space-grotesk;weight=500,700;variable=--font-space-grotesk'
// app/layout.tsx
import { display, sans } from './fonts';
<html className={`${sans.variable} ${display.variable}`}>

Keys after the face: weight= (default: the whole range), style= (default normal), variable=, subsets= (default: all shipped), preload= (default latin; none), display= (default swap), hinting=on (the TrueType-hinted files in fonts/<face>/hinted/, which Google serves a current Chrome on Windows or Linux; the default is the unhinted ones next/font/google downloaded). It writes one localFont call per subset, in Google's order: the same files, weights, styles, ranges and preloads next/font/google produced, formatted with the app's own prettier. The first lines of the file are the command that wrote it; add --check to fail when the file and the package disagree. Two things differ from Google, and neither changes a rendered page: the family is named after the export (Turbopack ignores a font-family declaration in .variable), and the fallback face is measured from the file instead of Google's table.

node scripts/fetch-fonts.mjs (maintainers only, never in a build) is how the files got here: it asks Google exactly what next/font/google asks, with its user agent.

Why a serif

Every AI and infrastructure company competes inside the same dark-canvas grotesque convention, where the winner is whoever has the largest design budget. A high-contrast serif exits that comparison: it reads institutional rather than startup, which is exactly right for a stack whose product is legitimacy — signed votes, verifiable treasuries, an append-only audit trail.


Part of bitbaum — AI-native products on open infrastructure, built in Zürich. Every package here lists the apps that use it: which apps use design-tokens.

Data maps

--map-scale-1 … --map-scale-7 are the one sequential scale for every choropleth (viridis, dark to light; hex, so an SVG fill takes them as is). Luminance rises at every step, so the scale survives colour-vision deficiency and greyscale; test/contrast.test.js keeps it that way. --map-no-data and --map-boundary follow the theme. An area more than one authority claims is hatched, never given a colour of its own: lines of --map-hatch-width every --map-hatch-gap at --map-hatch-angle, in the text colour. Colour is never the only carrier: every map has a list and a legend.

版本与发布记录

@bitbaum/design-tokens 共发布 8 个版本,首次发布于 2026/09/05;过去 12 个月发布了 8 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.5.2latest2026/11/035 天前
1.5.12026/11/035 天前
1.5.02026/11/035 天前
1.4.02026/11/035 天前
1.3.12026/11/035 天前
1.3.02026/11/035 天前

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

@bitbaum/design-tokens 是什么?

Design SSOT for OrangeCat, Loki and Solon — tokens, self-hosted faces, the Tailwind preset that maps them, and next/font/local calls for every face the fleet's apps use.

如何安装 @bitbaum/design-tokens?

在项目目录运行 npm install @bitbaum/design-tokens,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 design-tokens-next-fonts,可以用 npx design-tokens-next-fonts 直接运行。

@bitbaum/design-tokens 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bitbaum/design-tokens 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

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

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

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

@bitbaum/design-tokens 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

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

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

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

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