@cueplusplus/theme-tools
v1.1.0
Published
Build a CUE++ theme package from DTCG sources: theme.css, manifest.json, variants.css and the typed index, with the contrast report as the gate.
Downloads
606
Readme
@cueplusplus/theme-tools
cue-theme: DTCG colour sources in, a publishable CUE++ theme package out — with the WCAG contrast
report as the gate rather than as a footnote. For anyone building a palette of their own; the
presets in this repository are built by it too.
Install
The @cueplusplus scope is public on npm and resolves there by default, so there is nothing
to configure and no credential to supply — it installs like any other package.
pnpm add -D @cueplusplus/theme-tools @cueplusplus/tokensA build-time dependency: nothing it writes imports it. @cueplusplus/tokens is the one peer it
declares (>=0.9.0 <1) — the axes and the tier-1 primitives a theme source aliases — declared as a
peer so the theme you build is resolved against the same token layer your app's stylesheets came
from. @cueplusplus/theme-base comes along as a dependency: the manifest contract, the resolver and
the contrast report are its, and this package is their first consumer.
Quick start
Three commands, and the middle one is the whole job.
pnpm add -D @cueplusplus/theme-tools @cueplusplus/tokens @cueplusplus/theme-cue
pnpm exec cue-theme init acme --name acme --package @acme/cue-theme-acme --from @cueplusplus/theme-cue
cd acme && pnpm exec cue-theme buildinit scaffolds a package that builds before it is edited: two DTCG sources (colour and type only —
no geometry token may appear in a theme), a theme.config.ts, a seeded README.md and
CHANGELOG.md, and a package.json with the publish shape already in it,
prepack: "cue-theme check" included. --from starts the palette from an installed theme's
manifest rather than from the blank base.
build writes the five files a theme package ships — theme.css, manifest.json, variants.css,
index.js, index.d.ts — and check rebuilds all five in memory and fails if anything on disk
differs, which is the same question CI and prepack ask. Both take --config <file> and
--out <dir>; the config is theme.config.{ts,mts,js,mjs} in the current directory unless
--config says otherwise. A .ts config is loaded with Node's type stripping, and on Node 22 the
command re-execs itself with the flag rather than making you pass it.
The config is one call, and it is validated rather than trusted:
// theme.config.ts
import { defineTheme } from "@cueplusplus/theme-tools";
export default defineTheme({
name: "acme",
package: "@acme/cue-theme-acme",
mode: "complete", // or "delta": theme.css then holds only what differs from the base
sources: { dark: "src/acme.dark.tokens.json", light: "src/acme.light.tokens.json" },
});The contrast gate is not advisory. A required pair that fails its floor stops the build unless
the config names that exact pair in knownContrastFailures with the reason it ships that way; a
waiver that names a pair the report does not fail stops the build too, because a list that can
only rot in one direction always does.
cue-theme build also checks that the installed @cueplusplus/theme-base satisfies the range your
package's peerDependencies declares. Building against a base outside that range would resolve your
deltas against the wrong palette, and every file after that point would be wrong in a way no later
comparison could see.
The same build, programmatically
The CLI is a thin shell over the root export, for a generator or a test that wants the files without
touching a disk. buildTheme returns them, plus the manifest behind them, the contrast report and
the warnings the caller should print; it throws with the unwaived failures and the stale waivers
listed when the gate refuses.
import { readFile } from "node:fs/promises";
import { buildTheme, defineTheme } from "@cueplusplus/theme-tools";
const config = defineTheme({
name: "acme",
package: "@acme/cue-theme-acme",
mode: "complete",
sources: { dark: "src/acme.dark.tokens.json", light: "src/acme.light.tokens.json" },
});
const { files, manifest, report, warnings } = await buildTheme(config, {
configDir: process.cwd(),
packageJson: JSON.parse(await readFile("package.json", "utf8")),
});
console.log(files["theme.css"], manifest.contrast, report.failures, warnings);The pieces underneath are exported too, for a tool that wants one of them on its own:
readThemeSources (the config's sources, sorted into what each may own), buildManifest (a
manifest and its contrast report), partitionWaivers (waived / unwaived / stale), and the
four emitters themeCss, variantsCss, indexJs and indexDts.
What it ships
| Entry | What it is |
| --- | --- |
| cue-theme (bin) | init, build, check — the CLI above |
| @cueplusplus/theme-tools | defineTheme, buildTheme, buildManifest, readThemeSources, partitionWaivers, themeCss, variantsCss, indexJs, indexDts, and the ThemeConfig / ResolvedThemeConfig / ContrastWaiver / BuildContext / BuiltTheme / ThemePackageJson / ThemeSources types |
One entry point and one binary; there is no subpath. ESM only, Node 22 or newer, types beside the entry.
Where the rest is
- The theming guide — https://ui.cueplusplus.com/docs/theming. The token vocabulary, the
density and mode axes, the shipped presets, and
createTheme()from@cueplusplus/ui/theming— the in-app generator for a one-off palette, wherecue-themeis for one you publish. docs/CONSUMING.md— registry access in full, the peer matrix per subpath, and the table of what a failed install means.@cueplusplus/theme-base— the contract every theme this builds satisfies, and the resolver behindbuildTheme.CHANGELOG.md, in this package — one entry per release, addressed to you rather than to the diff, with a Migrating section on anything that needs an edit. Read it before an upgrade; the version number alone does not say what moved.
