Skip to main content
Version: Next

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/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.