Skip to main content
Version: 0.13

Core

Published as @unpunnyfuns/swatchbook-core. Framework-free DTCG loader. Parses token files via Terrazzo, resolves aliases, composes themes through a resolver or layered-axes config, and emits CSS variables + TypeScript types. No React, no Storybook dependency.

Install

npm install @unpunnyfuns/swatchbook-core

Functions

defineSwatchbookConfig(config)

Identity helper for a typed swatchbook.config.ts. No runtime effect — just TypeScript ergonomics. See the Config reference for every field's semantics.

import { defineSwatchbookConfig } from '@unpunnyfuns/swatchbook-core';

export default defineSwatchbookConfig({
resolver: 'tokens/resolver.json',
default: { mode: 'Light', brand: 'Default' },
cssVarPrefix: 'ds',
});

loadProject(config, cwd?)

Parse + resolve a project. Eagerly resolves every theme permutation, so downstream consumers can read themesResolved[name] directly without further I/O.

import { loadProject } from '@unpunnyfuns/swatchbook-core';

const project = await loadProject(config, process.cwd());
// project: { config, axes, presets, themes, themesResolved, graph, diagnostics }

resolveTheme(project, name)

Pick a single composed theme out of a project.

const { tokens } = resolveTheme(project, 'Light · Default');

Throws with a helpful "known themes" message if the name is unknown.

emitCss(themes, themesResolved, options?)

Concatenated stylesheet. With options.axes supplied (or when called through projectCss), emits :root for the default tuple plus one compound-selector block per non-default tuple.

const css = emitCss(project.themes, project.themesResolved, {
prefix: 'ds',
axes: project.axes,
});

Single-axis projects keep the familiar single-attribute shape — [data-<prefix>-theme="…"] with a prefix set (default swatch), [data-theme="…"] when you opt out with cssVarPrefix: ''.

projectCss(project)

emitCss with project defaults applied (prefix + axes). The form you almost always want.

emitTypes(project)

TypeScript source declaring the token-path union + SwatchbookTokenMap. The addon preset writes this to .swatchbook/tokens.d.ts so useToken() has typed paths.

permutationID(input)

Stringify a tuple to the form used as Theme.name and CSS data-attribute values:

permutationID({ mode: 'Dark', brand: 'Brand A' }); // → "Dark · Brand A"
permutationID({ theme: 'Light' }); // → "Light"

emitViaTerrazzo(project, options?)

Axis-aware wrapper around Terrazzo's programmatic build(). Derives compound-selector permutations from project.themes (or project.presets via selection), pins @terrazzo/plugin-css's variableName to the project's cssVarPrefix, and runs whatever additional Terrazzo plugins you pass — plugin-css-in-js, plugin-tailwind, plugin-swift, plugin-js, plugin-sass, plugin-vanilla-extract, plugin-token-listing.

import { loadProject, emitViaTerrazzo } from '@unpunnyfuns/swatchbook-core';
import cssInJsPlugin from '@terrazzo/plugin-css-in-js';
import tailwindPlugin from '@terrazzo/plugin-tailwind';

const project = await loadProject(config, cwd);
const files = await emitViaTerrazzo(project, {
cssOptions: { filename: 'tokens.css' },
plugins: [
cssInJsPlugin({ filename: 'tokens.js' }),
tailwindPlugin({ filename: 'tailwind.tokens.css' }),
],
selection: 'themes', // default — full cartesian fan-out for runtime CSS
});
// files → [{ filename, contents }, ...]

Options:

OptionWhat
selection'themes' (default, full cartesian), 'presets' (one entry per declared preset), or explicit Array<{ input, name? }>. Library consumers (MUI / Vuetify) usually want 'presets'.
cssOptionsExtra @terrazzo/plugin-css options (filename, include / exclude, transform, utility, colorDepth). permutations and variableName are managed internally — passing them here is a no-op.
pluginsAdditional Terrazzo plugins to run alongside plugin-css.

Requires a resolver-backed project. Layered (config.axes) and plain-parse paths don't populate parserInput; the wrapper throws with a clear error on those.

Not the addon's emission path. Storybook's runtime CSS still comes from projectCss with chrome aliases. emitViaTerrazzo is for library-level consumers driving their own build or for future display-side integrations.

Types

Config

interface Config {
/** Required for plain-parse and layered; optional when `resolver` is set. */
tokens?: string[];
resolver?: string; // mutually exclusive with `axes`
axes?: AxisConfig[]; // mutually exclusive with `resolver`
presets?: Preset[];
/** Partial tuple keyed by axis name — e.g. `{ mode: 'Dark', brand: 'Brand A' }`. Omitted axes fall back to each axis's own `default`. */
default?: Partial<Record<string, string>>;
/** Axis names to suppress from the toolbar and CSS emission; each is pinned to its default context. */
disabledAxes?: string[];
/** Map from block chrome roles (CHROME_ROLES) to target token paths — emits :root aliases redirecting --swatchbook-* chrome reads to consumer tokens. */
chrome?: Record<string, string>;
cssVarPrefix?: string;
outDir?: string;
}

AxisConfig

interface AxisConfig {
name: string;
description?: string;
contexts: Record<string, string[]>; // contextName → file paths / globs
default: string;
}

Preset

interface Preset {
name: string;
axes: Partial<Record<string, string>>; // axisName → contextName
description?: string;
}

Axis

interface Axis {
name: string;
contexts: string[];
default: string;
description?: string;
source: 'resolver' | 'layered' | 'synthetic';
}

Project

interface Project {
config: Config;
axes: Axis[];
/** Axis names suppressed via `config.disabledAxes` and validated against the resolver. */
disabledAxes: string[];
presets: Preset[];
/** Validated chrome-alias entries from `config.chrome`. Invalid entries are dropped and reported as diagnostics. */
chrome: Record<string, string>;
themes: Theme[];
themesResolved: Record<string, TokenMap>;
graph: TokenMap; // default theme's resolved tokens
/** Absolute paths of every file loaded while building the project (resolver + `$ref` targets, overlay files, or globbed plain-parse files). */
sourceFiles: string[];
/** Loader cwd — what all config-relative paths resolved against. */
cwd: string;
/** Retained Terrazzo parse output for `emitViaTerrazzo`. Present for resolver-backed projects; undefined for layered / plain-parse paths. */
parserInput?: ParserInput;
diagnostics: Diagnostic[];
}

ParserInput

Pass-through bag of the three values Terrazzo's programmatic build() needs. Consumers treat it as opaque — feed the whole object into build() (or into emitViaTerrazzo, which handles that for you).

interface ParserInput {
tokens: Record<string, TokenNormalized>; // unified pre-permutation token graph
sources: InputSourceWithDocument[]; // parser's input sources
resolver: Resolver; // the Terrazzo resolver object
}

SwatchbookIntegration

Contract for display-side integrations plugged into the addon via its integrations[] option. An integration names a virtual module; the addon's Vite plugin serves whatever render(project) produces.

interface SwatchbookIntegration {
name: string;
virtualModule?: {
virtualId: string; // e.g. 'virtual:swatchbook/tailwind.css'
render(project: Project): string; // produce the module body
};
}

See the Integrations section for factories that build these for Tailwind and CSS-in-JS.

Theme

interface Theme {
name: string;
input: Record<string, string>; // the axis tuple
sources: string[];
}

Diagnostic

interface Diagnostic {
severity: 'error' | 'warn' | 'info';
group: string;
message: string;
filename?: string;
line?: number;
column?: number;
}

Do / don't

  • ✅ Use this package at build time — Node scripts, SSR, Storybook presets. It has no DOM dependency.
  • ✅ Treat emitCss output as the source of truth for CSS vars; don't parallel-hand-write them.
  • ❌ Don't import from @terrazzo/parser directly unless you need features core doesn't expose.
  • ❌ Don't ship Project objects to the browser — they carry full raw-AST references. Use themesResolved projections instead.