Skip to content

@xmachines/play-catalog

Documentation / @xmachines/play-catalog

Framework-agnostic UI schema with Zod validation for XMachines Play Architecture

Type-safe catalog defining component contracts without framework dependencies.

Overview

@xmachines/play-catalog provides defineCatalog() and defineComponents() for creating type-safe component catalogs with Zod schema validation. It enables business logic to define component props declaratively while maintaining zero framework dependencies — components can be React, Vue, Svelte, or any other framework.

Per RFC Play v1, this package implements:

  • Strict Separation (INV-02): Business logic has zero framework imports
  • Catalog validates prop types at state entry (build-time + runtime)

Installation

Terminal window
npm install zod@^4.3.6
npm install @xmachines/play-catalog

Current Exports

  • defineCatalog
  • defineComponents
  • types: Catalog, InferComponentProps

Peer dependencies:

  • zod ^4.3.6 — Schema validation (you control the version)

Quick Start

import { z } from "zod";
import { defineCatalog } from "@xmachines/play-catalog";
import type { InferComponentProps } from "@xmachines/play-catalog";
// 1. Define catalog with Zod schemas (business logic layer)
const catalog = defineCatalog({
LoginForm: z.object({
error: z.string().optional(),
}),
Dashboard: z.object({
userId: z.string(),
username: z.string(),
stats: z.object({
logins: z.number(),
lastLogin: z.date(),
}),
}),
});
// 2. TypeScript infers prop types from schemas
type DashboardProps = InferComponentProps<typeof catalog, "Dashboard">;
// => { userId: string; username: string; stats: { logins: number; lastLogin: Date } }
// 3. Use in state machine meta.view
const machine = setup({...}).createMachine({
states: {
dashboard: {
meta: {
route: "/dashboard",
view: {
component: "Dashboard", // Type-checked against catalog
props: {
userId: "user123",
username: "Alice",
stats: {
logins: 42,
lastLogin: new Date(),
},
},
},
},
},
},
});

API Reference

defineCatalog()

Create type-safe component catalog from Zod schemas:

const catalog = defineCatalog({
ComponentName: z.object({
/* props schema */
}),
});

Parameters:

  • schemas: Record<string, ZodObject> - Map of component names to Zod schemas

Returns: Frozen catalog object (immutable in development)

Example:

const catalog = defineCatalog({
UserProfile: z.object({
userId: z.string(),
avatar: z.string().url().optional(),
bio: z.string().max(500).optional(),
}),
SettingsForm: z.object({
theme: z.enum(["light", "dark"]),
notifications: z.boolean(),
}),
});

InferComponentProps<Catalog, Component>

Utility type extracting prop types from catalog:

type Props = InferComponentProps<typeof catalog, "ComponentName">;

Example:

const catalog = defineCatalog({
Dashboard: z.object({
userId: z.string(),
data: z.array(z.number()),
}),
});
type DashboardProps = InferComponentProps<typeof catalog, "Dashboard">;
// => { userId: string; data: number[] }
// Use in component definition
function Dashboard(props: DashboardProps) {
// props.userId is string
// props.data is number[]
}

defineComponents()

Validate React component implementations match catalog (compile-time):

const components = defineComponents(catalog, {
ComponentName: ComponentImplementation,
});

Compile-time validation:

  • Component names must exist in catalog
  • Component prop types must match schema-inferred types

Example:

import { defineComponents } from "@xmachines/play-catalog";
const catalog = defineCatalog({
LoginForm: z.object({ error: z.string().optional() }),
Dashboard: z.object({ userId: z.string() }),
});
const components = defineComponents(catalog, {
LoginForm: ({ error }) => {
// error is string | undefined (inferred from schema)
return <form>{error && <p>{error}</p>}</form>;
},
Dashboard: ({ userId }) => {
// userId is string (inferred from schema)
return <div>User: {userId}</div>;
},
// @ts-expect-error - "InvalidComponent" not in catalog
InvalidComponent: () => <div />,
});

Examples

Progressive Validation

import { z } from "zod";
import { defineCatalog } from "@xmachines/play-catalog";
// Start simple
const catalog = defineCatalog({
UserCard: z.object({
name: z.string(),
}),
});
// Add validation rules
const enhancedCatalog = defineCatalog({
UserCard: z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().max(150),
}),
});
// Complex nested schemas
const advancedCatalog = defineCatalog({
UserCard: z.object({
name: z.string().min(1),
email: z.string().email(),
profile: z.object({
avatar: z.string().url().optional(),
bio: z.string().max(500).optional(),
links: z.array(z.string().url()).max(5).optional(),
}),
preferences: z.object({
theme: z.enum(["light", "dark", "auto"]),
language: z.string().length(2), // ISO 639-1
}),
}),
});

Machine Integration

import { setup } from "xstate";
import { defineCatalog } from "@xmachines/play-catalog";
import { z } from "zod";
const catalog = defineCatalog({
LoginForm: z.object({ error: z.string().optional() }),
Dashboard: z.object({
userId: z.string(),
notifications: z.number(),
}),
});
const machine = setup({
types: {
context: {} as { userId: string; errorMsg: string | null },
},
}).createMachine({
initial: "login",
context: { userId: "", errorMsg: null },
states: {
login: {
meta: {
view: {
component: "LoginForm", // Type-checked against catalog
props: (context) => ({
error: context.errorMsg ?? undefined,
}),
},
},
on: {
"auth.login": {
target: "dashboard",
actions: assign({ userId: ({ event }) => event.userId }),
},
},
},
dashboard: {
meta: {
view: {
component: "Dashboard",
props: (context) => ({
userId: context.userId,
notifications: 5, // Hardcoded for example
}),
},
},
},
},
});

Architecture

This package enforces Strict Separation (INV-02):

  1. Zero Framework Dependencies:

    • Catalog definition has no React/Vue/Svelte imports
    • Business logic stays framework-agnostic
    • Same catalog works with any renderer
  2. Validation at State Entry:

    • Zod validates props when actor enters state
    • Invalid props trigger validation error (catchable)
    • Build-time + runtime safety
  3. Compile-Time Type Safety:

    • TypeScript infers prop types from Zod schemas
    • Component implementations type-checked at compile time
    • Refactoring propagates through catalog and components

Benefits:

  • Swap React for Vue without changing business logic
  • Test catalog validation without framework overhead
  • Framework updates don’t affect business logic

License

Copyright (c) 2016 Mikael Karon. All rights reserved.

This work is licensed under the terms of the MIT license.
For a copy, see https://opensource.org/licenses/MIT.

Type Aliases

Functions