Skip to content

@tabula-css/preset

Assembles the project's Tailwind v4 entry stylesheet: @import "tailwindcss" source(none) plus @theme plus @source inline() over the whole closed vocabulary.

Install

@tabula-css/preset is a runtime dependency — part of the root README's quickstart:

bash
npm install @tabula-css/merge @tabula-css/preset

Overview

@tabula-css/preset is a pure library: every export is a ResolvedModel → string function, with no filesystem access — writing the result to .tabula/source.css is @tabula-css/cli's job, not this package's. It takes the resolved token model from @tabula-css/tokens and produces the single stylesheet a project's build imports: Tailwind scanning turned off (source(none)), the token theme (delegated to emitThemeCss() from @tabula-css/tokens), one @utility block per registered class (in logical longhands only, so every emitted property stays a canonical slot), and @source inline(...) declarations enumerating exactly the closed vocabulary — which is what makes closure (T3, see Concepts) hold: an unregistered class literally cannot produce CSS, because source(none) has told Tailwind to scan nothing else.

Exports

buildPresetCss()

ts
export function buildPresetCss(model: ResolvedModel): string;

Assembles the complete Tailwind v4 entry stylesheet for a resolved model — the exact string tabula build writes to .tabula/source.css. In order: the @import "tailwindcss" source(none); line (plus, under the base profile level, a @theme block resetting the stock text-*/font-*/leading-*/tracking-* scales so only the profile's own partial-typography utilities exist); the embedded token theme; one @utility block per candidate class; the @source inline(...) vocabulary closure; the declared variant-chain rules in an appended @layer utilities block (via buildChainLayer(), omitted when the model declares no chains); and the prose-quarantine exclusion CSS in its own layer.

ts
import { writeFileSync } from 'node:fs';
import { buildProfile } from '@tabula-css/tokens';
import { buildPresetCss } from '@tabula-css/preset';

const { model } = buildProfile(tokensJson, configJson);
writeFileSync('.tabula/source.css', buildPresetCss(model));

buildUtilities()

ts
export function buildUtilities(model: ResolvedModel): string;

Renders just the @utility <class> { … } blocks — one per candidate, in the model's rank order — without the surrounding import/theme/source-inline scaffolding. Each declaration prefers the candidate's var-indirected emitted form over its literal value, so the emitted CSS stays themable.

buildSourceInline()

ts
export function buildSourceInline(model: ResolvedModel): string[];

Renders the @source inline(...) lines covering the full vocabulary, by compressing the candidate class list (compressClasses()) and rendering it (renderSourceInline()). Returned as an array of lines rather than a single string, matching how buildPresetCss() joins it into the larger document.

buildChainLayer()

ts
export function buildChainLayer(model: ResolvedModel): string;

Renders the declared variant products (the v0.2 variant-closure fix) as a single appended @layer utilities { … } block — one literal CSS rule per entry in model.chainCandidates, its declaration body byte-identical to the base utility's (the same rendering path as buildUtilities()). Rules are emitted in global (effectiveRank, class) order, and consecutive rules sharing a media condition are grouped into one @media block, so document order realizes the merge's total order exactly. Returns the empty string when the model declares no chains. Chain rules are deliberately not listed in @source inline — a chain listed there would make Tailwind generate a stock-shaped (0,2,0) duplicate that contradicts the profile's (0,1,0) shape; emitting them as literal rules is what forces the correct cascade order by construction.

compressClasses() / expandSpec() / expandSpecs() / renderSourceInline()

ts
export function compressClasses(classes: readonly string[]): string[];
export function expandSpec(spec: string): string[];
export function expandSpecs(specs: readonly string[]): string[];
export function renderSourceInline(specs: readonly string[], maxLineLength?: number): string[];

The @source inline spec synthesis this package's closure guarantee rests on. compressClasses() brace-folds a class list that shares a first segment (bg-surface, bg-surface-raisedbg-{surface,surface-raised}) into a smaller set of Tailwind brace-expansion specs — sorted, so the spec list is a pure function of the class set — while leaving anything unsafe to fold (negated classes like -m-md, single-segment names) enumerated verbatim. expandSpec()/expandSpecs() are the exact inverse, exported specifically so the round-trip property expandSpecs(compressClasses(v)) === v (as a set) is testable without a real Tailwind build in the loop. renderSourceInline() wraps a spec list into @source inline("…"); lines, breaking at maxLineLength (default 96) so a large vocabulary stays readable in the generated file.

ts
import { compressClasses, renderSourceInline } from '@tabula-css/preset';

renderSourceInline(compressClasses(['bg-surface', 'bg-surface-raised', 'p-md']));
// → ['@source inline("bg-{surface,surface-raised} p-md");']

See also

  • @tabula-css/tokens — produces the ResolvedModel this package renders.
  • @tabula-css/registry — derives registry.json from the same Tailwind build this stylesheet produces.
  • @tabula-css/cli — writes buildPresetCss()'s output to .tabula/source.css as part of tabula build.
  • Concepts § closure (T3) — why source(none) plus @source inline is what makes an unregistered class emit no CSS.

Released under the MIT License.