@hex-core/registry
Zod schemas + TypeScript types for components, themes, and recipes.
Install
0.5.1Runtime dependencypnpm add @hex-core/registryWhat it does
`@hex-core/registry` is the schema package — Zod schemas plus the TypeScript types they generate. It defines the shape every other hex-core package speaks: `Theme`, `TokenValue`, `RegistryItem`, `Recipe`, the strict variants of each.
Most consumers never `import` from this package directly; they use it transitively through `@hex-core/components`, `@hex-core/tokens`, `@hex-core/themes`, or `@hex-core/payload`. You install it directly when you need to type a function parameter as `Theme`, validate user input against `strictThemeSchema`, or build a tool that produces registry items.
The 0.2.0 release tightened `Theme.tokens.{light,dark}` from `Record<string, unknown>` to the strict `Record<string, TokenValue>` shape. Type-version skew between sibling packages is now caught at `tsc` time instead of at runtime.
Public API
Theme
export interface Theme {
name: string;
displayName: string;
description: string;
tokens: {
light: Record<string, TokenValue>;
dark: Record<string, TokenValue>;
};
brand?: string;
category?: ThemeCategory;
tags?: string[];
designBrief?: string;
attribution?: ThemeAttribution; // see Theme classification surface entry below
}The canonical Theme shape. Every theme object the design system consumes — `defaultTheme`, `midnightTheme`, anything you author — must satisfy this type.
import type { Theme } from "@hex-core/registry";
const myTheme: Theme = {
name: "brand",
displayName: "Brand",
description: "Brand description",
tokens: { light: {/* ... */}, dark: {/* ... */} },
};TokenValue / TokenSet
export interface TokenValue {
value: string;
description?: string;
type:
| "number" | "color" | "dimension" | "font" | "fontWeight"
| "duration" | "cubicBezier" | "shadow" | "gradient"
| "radius" | "spacing" | "opacity";
}
export type TokenSet = Record<string, TokenValue>;A token is a `value` (HSL triplet, CSS length, easing function, etc.) plus a `type` discriminant. Discriminating by `type` lets transformers emit the right wrapper — `hsl()` for colors, raw value for everything else.
import type { TokenValue } from "@hex-core/registry";
const primary: TokenValue = { value: "240 5.9% 10%", type: "color" };
const radius: TokenValue = { value: "0.5rem", type: "radius" };RegistryItem
export interface RegistryItem {
$schema: string; // schema URL (defaulted)
name: string;
displayName: string;
description: string;
category:
| "component" | "lib" | "hook" | "primitive"
| "block" | "example" | "theme" | "ai";
subcategory?: string;
version: string; // semver (defaulted to "0.0.1")
framework: "react" | "vue" | "svelte"; // defaulted to "react"
props: PropDef[];
variants: VariantDef[];
slots: SlotDef[];
files: RegistryFile[]; // required — source files shipped by hex add
dependencies: { npm: string[]; internal: string[]; peer: string[] };
tokensUsed: string[];
cssVariables?: Record<string, { light: string; dark: string }>;
examples: Example[];
ai: AIHints;
tags: string[];
}Schema for one entry in `registry/items/<slug>.json`. Components, the CLI's `hex add`, and the MCP server's `get_component` tool all serialize to this shape. The `ai` block is what makes hex-ui LLM-native — `whenToUse`, `commonMistakes`, `accessibilityNotes`.
import { registryItemSchema } from "@hex-core/registry";
const parsed = registryItemSchema.parse(rawJson);
// → typed RegistryItem with full validationstrictThemeSchema / strictTokenSetSchema
import { strictThemeSchema, strictTokenSetSchema } from "@hex-core/registry";
import type { z } from "zod";
declare const strictThemeSchema: z.ZodType<Theme>;
declare const strictTokenSetSchema: z.ZodType<TokenSet>;Zod schemas for runtime validation. Use these at trust boundaries — parsing user-submitted theme overrides, validating an MCP tool input, ingesting a third-party token set. `strictTokenSetSchema` (added 0.3.x alongside the existing `strictThemeSchema`) covers the inner `Record<string, TokenValue>` shape on its own when you only need to validate one palette.
import { strictThemeSchema } from "@hex-core/registry";
const result = strictThemeSchema.safeParse(input);
if (!result.success) {
throw new Error("Invalid theme: " + result.error.message);
}
const theme = result.data;Theme classification surface
import {
themeAttributionSchema,
themeCategorySchema,
type ThemeAttribution,
type ThemeCategory,
} from "@hex-core/registry";
type ThemeCategory =
| "ai" | "dev-tools" | "backend" | "productivity"
| "design" | "fintech" | "ecommerce" | "media" | "automotive";
interface ThemeAttribution {
source: string; // e.g. "voltagent/awesome-design-md"
license: string; // SPDX identifier
url: string; // upstream URL
brand?: string; // brand name if applicable
}The metadata layer added in 0.3.x to support the `@hex-core/themes` branded preset catalog. `category` slots themes into the studio's grouped picker; `attribution` carries the upstream-source / license / brand context (full-replacement on `extendTheme`, never deep-merged). Both are optional fields on `themeSchema` — first-party themes (`midnight`, `ember`) leave them undefined.
import { themeCategorySchema, themeAttributionSchema } from "@hex-core/registry";
const cat = themeCategorySchema.parse("fintech"); // → "fintech"
const att = themeAttributionSchema.parse({
source: "voltagent/awesome-design-md",
license: "MIT",
url: "https://github.com/voltagent/awesome-design-md",
brand: "Stripe",
});HeavyPeer / heavyPeerSchema
import { heavyPeerSchema, type HeavyPeer } from "@hex-core/registry";
interface HeavyPeer {
name: string; // npm package name (e.g. "@tanstack/react-table")
version: string; // semver range
bundleKbGzip?: number; // estimated install cost surfaced by the CLI prompt
reason?: string; // human-readable why-this-peer note
}
// Components declare heavy peers via dependencies.heavyPeer[] in their schema:
declare const dependencySchema: z.ZodObject<{
npm: string[];
internal: string[];
peer: string[];
heavyPeer?: HeavyPeer[];
}>;Schema for peer dependencies the CLI's heavy-peer prompt surfaces during `hex add`. When a component declares `heavyPeer: [{ name, version, bundleKbGzip?, reason? }]`, the CLI consolidates them across the install set and asks the user once before continuing — avoids the "why did I just bundle 90KB of `react-day-picker`" surprise. Re-introduced in registry@0.3.4 (the type was deferred from the 0.3.3 surface). Pairs with `@hex-core/cli@0.6.0`'s alias resolver + drift checks.
import { heavyPeerSchema } from "@hex-core/registry";
const peer = heavyPeerSchema.parse({
name: "@tanstack/react-table",
version: "^8.0.0 || ^9.0.0",
bundleKbGzip: 28,
reason: "Headless table primitives for sortable / filterable rows.",
});Recipe schema (recipeSchemaDefinition + types)
import {
recipeSchema,
recipeSchemaDefinition,
recipeStepSchema,
recipeChecklistItemSchema,
recipeIndexItemSchema,
type Recipe,
type RecipeStep,
type RecipeChecklistItem,
type RecipeIndex,
type RecipeIndexItem,
SLUG_REGEX,
internalDepToSlug,
} from "@hex-core/registry";
interface Recipe {
slug: string;
title: string;
summary: string;
tags: string[];
brief: string;
steps: RecipeStep[];
checklist: RecipeChecklistItem[];
example?: string;
tokenBudget?: number;
}Recipe schemas were promoted out of `@hex-core/components` into their own subpath (`@hex-core/registry/recipes`). `Recipe` is the multi-component blueprint shape (e.g. "auth-form", "settings-page"); `RecipeStep` lists components with role + reason; `RecipeChecklistItem` carries severity (`blocker`/`warn`/`nit`) for post-install validation. `RecipeIndexItem` is the slim version surfaced in the index for catalog UIs. `SLUG_REGEX` is the canonical slug validator (`/^[a-z][a-z0-9-]*$/`); `internalDepToSlug` translates an internal dep path stored in registry items (`primitives/<slug>/<slug>`, `components/<slug>/<slug>`, `blocks/<slug>/<slug>`) into the bare component slug; returns `null` for non-component paths like `lib/utils`.
import { recipeSchema, internalDepToSlug } from "@hex-core/registry";
const recipe = recipeSchema.parse(rawJson);
console.log(recipe.steps.map((s) => s.component));
// → ["button", "input", "label", ...]
const slug = internalDepToSlug("primitives/button/button"); // → "button"componentSchemaDefinition
import {
componentSchemaDefinition,
type ComponentSchemaDefinition,
} from "@hex-core/registry";
declare const componentSchemaDefinition: z.ZodType<ComponentSchemaDefinition>;The typed contract for the `<component>.schema.ts` files components ship — the structural definition (props, variants, slots, AI hints) separate from the wire-format `registryItemSchema`. Components author against this; the build pipeline serializes to `registryItemSchema` (the `registry/items/<slug>.json` shape) for distribution.
import { componentSchemaDefinition } from "@hex-core/registry";
import { buttonSchema } from "@hex-core/components/schemas";
const def = componentSchemaDefinition.parse(buttonSchema);
console.log(def.variants);Workflows
Validate a user-uploaded theme JSON file
Users sometimes ship custom themes as JSON. Parse with `strictThemeSchema` before handing the result to any transformer.
import { strictThemeSchema } from "@hex-core/registry";
import { themeToCss } from "@hex-core/tokens";
export async function compileUploadedTheme(json: unknown) {
const result = strictThemeSchema.safeParse(json);
if (!result.success) {
return { ok: false, errors: result.error.issues };
}
return { ok: true, css: themeToCss(result.data) };
}Build a typed registry-item generator
Authoring a custom component? Type your scaffold against `RegistryItem` so missing fields surface at `tsc` time.
import type { RegistryItem } from "@hex-core/registry";
export function makeRegistryItem(name: string): RegistryItem {
return {
$schema: "https://hex-ui.dev/schemas/registry-item.json",
name,
displayName: name,
description: "TODO",
category: "component",
version: "0.0.1",
framework: "react",
props: [],
variants: [],
slots: [],
files: [],
examples: [],
ai: {
whenToUse: "",
whenNotToUse: "",
commonMistakes: [],
relatedComponents: [],
accessibilityNotes: "",
tokenBudget: 0,
},
dependencies: { npm: [], internal: [], peer: [] },
tokensUsed: [],
tags: [],
};
}Compatibility
- Depends only on `zod@^4.3.6`. No React, no DOM, no Node-specific APIs.
- Compiled to ESM with `.d.ts` types. Tree-shakable — importing only `Theme` doesn't pull the Zod runtime.
- Strict-mode schema versioning: 0.3.x line is mostly back-compatible at the type level for Theme / TokenSet / RegistryItem — the additions (`themeAttribution`, `themeCategory`, recipe split, `componentSchemaDefinition`) are net-new optional fields. Two fields are now **required**, though: `Theme.description` (replaces the removed `colorScheme` discriminator) and `RegistryItem.files` (the source files `hex add` ships). Older payloads that omitted either must backfill before validating against the 0.3.x schemas.
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