@hex-core/payload
Pure-function builders for the canonical LLM context markdown.
Install
0.4.1Runtime dependencypnpm add @hex-core/payloadWhat it does
`@hex-core/payload` is the pure-function builder for the canonical LLM context blob. Pass it a theme + components (+ optional recipes, density), get markdown back.
It exists so non-MCP-client surfaces — Next.js Server Components, generator scripts, CI fixtures, CLI tools — can render the canonical payload without spawning a subprocess and speaking JSON-RPC over stdio. The MCP server (`@hex-core/mcp@0.4.0+`) is now a thin transport shell that wraps this package.
The package also bundles the registry data (185 items + 24 recipes as of payload 0.4.1) inside its tarball, so loaders work without filesystem coupling to the consumer's repo. This is what unblocked the studio's `/copy` route from re-implementing the markdown format locally.
Public API
buildAppContext
function buildAppContext(input: {
theme: { requested: string; resolved: Theme | null };
components: { slug: string; item: RegistryItem | null }[];
recipes: { slug: string; recipe: Recipe | null }[];
density?: "compact" | "comfortable" | "spacious";
overrides?: Record<string, string>;
}): string;Build the full canonical markdown blob: install commands, `globals.css`, `tailwind.config.ts`, components, recipes, and the LLM context prompt. `theme.resolved` is `null` when the requested slug was unknown — the markdown surfaces the fallback so the LLM knows the user's intent vs. the active theme. `recipes` is required (pass `[]` if you have none); both `recipe` and `item` slots may be `null` when a slug doesn't resolve. The documented input above is the canonical authoring surface; the published `AppContextInput` carries a few extra optional metadata fields on `AppContextTheme` (tags / designBrief / attribution / brand / category) that are normally populated upstream rather than authored by hand.
import { buildAppContext, loadRegistryItem } from "@hex-core/payload";
import { defaultTheme } from "@hex-core/tokens";
const components = ["button", "card", "input"].map((slug) => ({
slug,
item: loadRegistryItem(slug),
}));
const markdown = buildAppContext({
theme: { requested: "default", resolved: defaultTheme },
components,
recipes: [],
density: "comfortable",
});
// Render markdown in a Server Component, write to disk, etc.buildFigmaTokens
interface FigmaTokensInput {
theme: { requested: string; resolved: FigmaTokensTheme | null };
}
function buildFigmaTokens(input: FigmaTokensInput): string;Build a deterministic markdown payload describing the chosen theme as a Figma Variables REST POST body. Pasting the JSON block into Figma's plugin or the REST endpoint produces a populated variable kit (one collection, two modes, one variable per token). `theme.resolved` is `null` when the requested slug was unknown — the markdown calls that out so designers know the fallback happened.
import { buildFigmaTokens, getTheme } from "@hex-core/payload";
const slug = "midnight";
const markdown = buildFigmaTokens({
theme: { requested: slug, resolved: getTheme(slug) ?? null },
});
console.log(markdown.slice(0, 80));
// → "## Figma Variables payload — midnight\n..."buildFigmaPayload
interface FigmaTokensTheme {
name: string;
displayName: string;
tokens: { light: Record<string, TokenValue>; dark: Record<string, TokenValue>; };
}
interface FigmaPayloadResult {
payload: FigmaVariablesPayload;
skipped: Array<{ key: string; type: TokenValue["type"] }>;
}
function buildFigmaPayload(theme: FigmaTokensTheme): FigmaPayloadResult;Lower-level companion to `buildFigmaTokens` — returns the raw `FigmaVariablesPayload` object plus a `skipped` list of tokens whose type Figma can't represent as a primitive Variable (`shadow`, `gradient`, `cubicBezier`). Caller surfaces `skipped` so designers see what didn't transfer; a silent zero-value FLOAT fall-through would be worse. Iterates the union of light + dark keys so dark-only tokens land in the kit.
import { buildFigmaPayload } from "@hex-core/payload";
import { defaultTheme } from "@hex-core/tokens";
const { payload, skipped } = buildFigmaPayload(defaultTheme);
console.log(payload.variables.length);
// → number of tokens emitted as Figma Variables
if (skipped.length > 0) {
console.warn("Figma can't represent:", skipped);
// → [{ key: "shadow-md", type: "shadow" }, ...]
}loadRegistryItem / loadRegistry
function loadRegistryItem(slug: string): RegistryItem | null;
function loadRegistry(): RegistryIndex;
interface RegistryIndex {
name: string;
version: string;
description: string;
homepage: string;
items: Array<{
name: string;
displayName: string;
description: string;
category: string;
subcategory?: string;
tags: string[];
internalDeps: string[];
tokenBudget?: number;
}>;
}Read from the bundled registry snapshot. `loadRegistryItem(slug)` returns the FULL `RegistryItem` (or `null` for an unknown slug) — that's the function you reach for when you need props/variants/AI hints. `loadRegistry()` returns the slim index (`items[]` are summaries, not full RegistryItems) — use it to enumerate the catalog or drive a search/picker UI. Server-only: both perform synchronous filesystem reads, so don't import them from a Client Component.
import { loadRegistry, loadRegistryItem } from "@hex-core/payload";
const idx = loadRegistry();
console.log(idx.items.length); // 185 (at @0.4.1)
const button = loadRegistryItem("button");
if (button) {
console.log(button.ai.whenToUse);
}loadRecipes / loadRecipe
function loadRecipes(): RecipeIndex;
function loadRecipe(slug: string): Recipe | null;
interface RecipeIndex {
name: string;
version: string;
items: Array<{
slug: string;
title: string;
summary: string;
tags: string[];
components: string[]; // flat slug list, derived from steps
tokenBudget?: number;
}>;
}Same split as the component loaders. `loadRecipes()` returns the slim `RecipeIndex` — each `items[]` entry carries a derived `components` slug list for catalog UIs. `loadRecipe(slug)` returns the FULL `Recipe`, whose enumerable structure is `recipe.steps.map((s) => s.component)` (the recipe object itself has no `components` field). Pass loaded recipes into `buildAppContext`'s required `recipes` field to bundle them into the LLM payload (use `[]` when you have none).
import { loadRecipes, loadRecipe } from "@hex-core/payload";
// Index path: pull the components list straight off the index entry.
const idx = loadRecipes();
const auth = idx.items.find((i) => i.slug === "auth-form");
console.log(auth?.components); // ["button", "input", "label", ...]
// Full-recipe path: components live under steps, not directly on Recipe.
const full = loadRecipe("auth-form");
console.log(full?.steps.map((s) => s.component));getTheme / listThemes
function getTheme(name: string): Theme | undefined;
function listThemes(): readonly {
name: string;
displayName: string;
description: string;
category?: string;
tags?: string[];
brand?: string;
}[];Re-exports of the theme catalog from `@hex-core/tokens`. `listThemes()` is metadata-only — entries carry `name` / `displayName` / `description` (plus optional `category` / `tags` / `brand`) so a picker UI can render labels without paying for token data. To retrieve the full strict `Theme` object call `getTheme(name)`. Convenient when you already have `@hex-core/payload` installed and don't want to add a second dependency just to look up a theme by name.
import { getTheme, listThemes } from "@hex-core/payload";
const theme = getTheme("midnight") ?? getTheme("default");
console.log(listThemes().map((t) => t.name));resolveSpec (with ResolveResult / ComponentMatch / RecipeMatch / ResolverOptions)
interface ComponentMatch {
component: string;
displayName: string;
score: number;
matchReason: string[];
whenToUse: string;
whenNotToUse: string;
relatedComponents: string[];
tokenBudget?: number;
}
interface RecipeMatch {
slug: string;
title: string;
summary: string;
score: number;
matchReason: string[];
}
interface ResolveResult {
brief: string;
components: ComponentMatch[];
recipes: RecipeMatch[];
}
interface ResolverOptions {
limit?: number;
registry?: RegistryIndex;
recipes?: RecipeIndex;
}
function resolveSpec(brief: string, options?: ResolverOptions): ResolveResult;Deterministic brief-to-shortlist resolver. Pass a natural-language description ("settings page with profile, billing, notifications"); get back a ranked list of components + recipes with per-match `score` + `matchReason` + the AI-hint context (`whenToUse` / `whenNotToUse` / `relatedComponents`). Same logic the MCP `resolve_spec` tool wraps. Deterministic: same brief + same registry snapshot always produces the same output, so agents can cite a slug with confidence and CI can regression-test against fixed briefs. Pass `options.limit` to cap matches per category; pass `options.registry` / `options.recipes` to score against an alternate snapshot.
import { resolveSpec } from "@hex-core/payload";
const result = resolveSpec("confirm-destructive dialog with checkbox and dual-action footer");
for (const m of result.components.slice(0, 3)) {
console.log(`${m.score.toFixed(2)} ${m.component} — ${m.matchReason[0]}`);
}
// → 0.92 dialog — "destructive" matched whenToUse
// → 0.81 button — "dual-action footer" matched whenToUse
// → 0.74 checkbox — explicit slug matchWorkflows
Render the canonical payload from a Next.js Server Component
This is what the studio's `/copy` page does. No subprocess, no JSON-RPC, no MCP transport — direct function call.
// app/llm-context/page.tsx
import { buildAppContext, loadRegistryItem } from "@hex-core/payload";
import { defaultTheme } from "@hex-core/tokens";
export default function LlmContextPage() {
const components = ["button", "card", "input", "dialog"].map((slug) => ({
slug,
item: loadRegistryItem(slug),
}));
const markdown = buildAppContext({
theme: { requested: "default", resolved: defaultTheme },
components,
recipes: [],
});
return <pre>{markdown}</pre>;
}Generate a Figma tokens file in a CI script
Run on every theme change to keep design and code in sync.
// scripts/sync-figma-tokens.ts
import { buildFigmaTokens, getTheme } from "@hex-core/payload";
import { writeFileSync } from "node:fs";
const requested = process.env.THEME ?? "default";
const theme = getTheme(requested);
if (!theme) throw new Error("Unknown theme");
const markdown = buildFigmaTokens({
theme: { requested, resolved: theme },
});
writeFileSync("./design/figma-tokens.md", markdown);
console.log("Wrote figma-tokens.md");Compatibility
- Pure ESM. Server-only — `loadRegistry*` and `loadRecipe*` perform synchronous `fs` reads. Do not import from a Client Component.
- Bundles `registry/` data into the published tarball. Works in fresh `npm install` outside the hex-core monorepo.
- Depends on `@hex-core/registry@^0.3.2`, `@hex-core/tokens@^1.3.2`, and `@hex-core/themes@^0.2.0`. Re-exports `RegistryItem`, `Recipe`, `RegistryIndex`, `RecipeIndex` and the Figma + resolver types directly; the `Theme` type comes from `@hex-core/registry` (the doc's `Theme` references above point there — `@hex-core/payload` does not re-export it).
See also
Verified against @hex-core/components@1.14.0 · @hex-core/tokens@1.3.7 · @hex-core/themes@0.2.3 · @hex-core/registry@0.6.0 · @hex-core/payload@0.4.1