@hex-core/registry

Zod schemas + TypeScript types for components, themes, and recipes.

Install

Version 0.5.1Runtime dependency
pnpm
pnpm add @hex-core/registry

What 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

ts
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.

tsx
import type { Theme } from "@hex-core/registry";

const myTheme: Theme = {
	name: "brand",
	displayName: "Brand",
	description: "Brand description",
	tokens: { light: {/* ... */}, dark: {/* ... */} },
};

TokenValue / TokenSet

ts
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.

tsx
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

ts
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`.

tsx
import { registryItemSchema } from "@hex-core/registry";

const parsed = registryItemSchema.parse(rawJson);
// → typed RegistryItem with full validation

strictThemeSchema / strictTokenSetSchema

ts
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.

tsx
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

ts
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.

tsx
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

ts
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.

tsx
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)

ts
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`.

tsx
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

ts
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.

tsx
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.

tsx
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.

tsx
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