@hex-core/payload
Pure-function builders for the canonical LLM context markdown.
Install
0.3.0Runtime 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 (164 items as of payload 0.3.0) 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); // 164 (at @0.3.0)
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.12.0 · @hex-core/tokens@1.3.6 · @hex-core/themes@0.2.2 · @hex-core/registry@0.5.1 · @hex-core/payload@0.3.0