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. 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.
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"
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;
diagnostics: Diagnostic[];
}
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.
- ❌ Don't import from
@terrazzo/parserdirectly unless you need features core doesn't expose. - ❌ Don't ship
Projectobjects to the browser — they carry full raw-AST references. UsethemesResolvedprojections instead.