Custom Catalogs
This guide explains how to create a custom Freesail catalog — a package that bundles a JSON schema describing UI components with their concrete React implementations. Agents use the schema to know what components exist; the React code renders them in the browser.
Quick Start
Section titled “Quick Start”npx freesail new catalogThis scaffolds a complete catalog package using the inclusion model. You own every file and can modify them freely.
Generated Structure
Section titled “Generated Structure”{name}-catalog/ package.json tsconfig.json src/ {name}-catalog.json # Generated — full resolved catalog (do not edit directly) freesailconfig.json # Catalog metadata (catalogId, title, description) index.ts # Exports CatalogDefinition includes/ catalog.include.json # Declare which packages to import from generated-includes.ts # Auto-generated bridge (do not edit) components/ components.json # Custom component schemas components.tsx # Custom component implementations functions/ functions.json # Custom function schemas functions.ts # Custom function implementationsCatalog ID
Section titled “Catalog ID”The catalogId in the generated catalog JSON is derived from the npm package name’s org scope. During scaffolding, you can provide your own org name, or accept the generated placeholder (e.g. @catamaran-4f8a2c). The resulting catalogId will look like https://acme.local/catalogs/weather-catalog.json.
To override the derived catalogId, edit src/freesailconfig.json:
{ "catalog": { "catalogFile": "weather-catalog.json", "catalogId": "https://mycompany.com/catalogs/weather-catalog.json", "title": "Weather Catalog", "description": "Weather UI components" }}Build Pipeline
Section titled “Build Pipeline”The generated package.json includes:
{ "scripts": { "prepare:catalog": "freesail prepare catalog", "prebuild": "freesail prepare catalog && freesail validate catalog", "build": "tsc" }}freesail prepare catalog— readscatalog.include.json, merges imported and local schemas, and writes{name}-catalog.jsonandgenerated-includes.tsfreesail validate catalog— checks that every JSON-declared component/function has a matching implementation
Both run automatically before each npm run build.
Importing from a Catalog Package
Section titled “Importing from a Catalog Package”The inclusion model lets you pull components and functions from any installed catalog package into your own. This is the primary way to reuse the standard Freesail components.
npx freesail include catalog --package @freesail/standard-catalogThis command:
- Reads all components and functions from the installed package’s catalog JSON
- Writes them into
src/includes/catalog.include.json - Re-runs
freesail prepare catalog
Edit catalog.include.json afterwards to keep only what you need:
{ "includes": { "@freesail/standard-catalog": { "catalogPath": "dist/standard-catalog.json", "components": ["Card", "Button", "Text", "Column", "Row"], "functions": ["formatString", "required", "formatDate"] } }}You can import from multiple packages by running freesail include catalog once per package, or by editing catalog.include.json directly.
Step 1: Define Custom Schemas
Section titled “Step 1: Define Custom Schemas”Add custom component schemas in src/components/components.json:
{ "components": { "StatusCard": { "type": "object", "allOf": [ { "$ref": "#/$defs/ComponentCommon" }, { "$ref": "#/$defs/CatalogComponentCommon" }, { "type": "object", "description": "A card displaying a status with a title, message, and severity level.", "properties": { "component": { "const": "StatusCard" }, "title": { "$ref": "#/$defs/DynamicString", "description": "Card heading" }, "message": { "$ref": "#/$defs/DynamicString", "description": "Body text" }, "severity": { "type": "string", "enum": ["info", "warning", "error", "success"], "description": "Visual severity level" } }, "required": ["component", "title"] } ], "unevaluatedProperties": false } }}Add custom function schemas in src/functions/functions.json:
{ "functions": { "truncate": { "type": "object", "description": "Truncates a string to maxLength characters.", "properties": { "call": { "const": "truncate" }, "args": { "type": "object", "properties": { "value": { "$ref": "#/$defs/DynamicValue" }, "maxLength": { "type": "number" } }, "required": ["value"] }, "returnType": { "const": "string" } }, "required": ["call", "args"], "unevaluatedProperties": false } }}After editing, run npx freesail prepare catalog to regenerate the resolved catalog JSON.
Key rules:
componentskeys are the component names agents will use (e.g."component": "StatusCard").descriptionfields are included in the agent’s system prompt — write them clearly.- Use
allOfwith$ref: "#/$defs/ComponentCommon"for consistent component structure. - Do not edit
{name}-catalog.jsondirectly — it is regenerated byfreesail prepare catalog.
Step 2: Implement Components (components/components.tsx)
Section titled “Step 2: Implement Components (components/components.tsx)”The scaffolded file imports included components from generated-includes.ts. Add your custom components alongside:
import React, { type CSSProperties } from 'react';import type { FreesailComponentProps } from '@freesail/react';import { includedComponents } from '../includes/generated-includes.js';
export function StatusCard({ component, children }: FreesailComponentProps) { const title = (component['title'] as string) ?? ''; const message = (component['message'] as string) ?? ''; const severity = (component['severity'] as string) ?? 'info';
const colors: Record<string, string> = { info: 'var(--freesail-info, #3b82f6)', warning: 'var(--freesail-warning, #f59e0b)', error: 'var(--freesail-error, #ef4444)', success: 'var(--freesail-success, #22c55e)', };
const style: CSSProperties = { padding: '16px', borderRadius: '8px', border: `1px solid ${colors[severity] ?? colors['info']}`, };
return ( <div style={style}> <strong>{title}</strong> {message && <p style={{ margin: '8px 0 0' }}>{message}</p>} {children} </div> );}
export const myappCatalogComponents = { ...includedComponents, StatusCard,};Conventions:
- Export each component as a named
export function. - Cast props with
as string— all values arrive asunknown. - Use CSS custom properties (
var(--freesail-*)) for theming. - The map keys must exactly match the component names in the JSON schema.
FreesailComponentProps reference
Section titled “FreesailComponentProps reference”| Prop | Type | Purpose |
|---|---|---|
component | A2UIComponent | All resolved props the agent sent for this component instance |
children | ReactNode | Rendered child components (for containers) |
meta | ComponentMeta | Metadata object with getBinding(propName) for accessing raw data bindings |
scopeData | unknown | Current item data when inside a dynamic list template |
dataModel | Record<string, unknown> | Full surface data model (read-only snapshot) |
onAction | (name, context) => void | Dispatch a named action to the agent |
onDataChange | (path, value) => void | Write a value to the local data model (two-way binding) |
onFunctionCall | (call) => void | Execute a client-side function call |
Two-way binding (input components)
Section titled “Two-way binding (input components)”For components that let users enter data, use the meta object to resolve the bound data model path, then call onDataChange on every change:
export function MyInput({ component, meta, onDataChange }: FreesailComponentProps) { const value = (component['value'] as string) ?? ''; const boundPath = meta.getBinding('value')?.path ?? `/input/${component.id}`;
return ( <input value={value} onChange={(e) => onDataChange?.(boundPath, e.target.value)} /> );}Step 3: Add Custom Functions (functions/functions.ts)
Section titled “Step 3: Add Custom Functions (functions/functions.ts)”The scaffolded file re-exports included functions. Add custom functions alongside:
import type { FunctionImplementation } from '@freesail/react';import { includedFunctions } from '../includes/generated-includes.js';
const truncate: FunctionImplementation = (value: unknown, maxLength: unknown) => { const str = String(value ?? ''); const max = Number(maxLength ?? 100); return str.length > max ? str.slice(0, max) + '…' : str;};
export const myappCatalogFunctions = { ...includedFunctions, truncate,};Remember to also declare the function in src/functions/functions.json (see Step 1).
Step 4: Wire Up index.ts
Section titled “Step 4: Wire Up index.ts”The scaffolded index.ts is ready to use:
import type { CatalogDefinition } from '@freesail/react';import { myappCatalogComponents } from './components/components.js';import { myappCatalogFunctions } from './functions/functions.js';import catalogSchema from './myapp-catalog.json';
export const MyappCatalog: CatalogDefinition = { namespace: catalogSchema.catalogId, schema: catalogSchema, components: myappCatalogComponents, functions: myappCatalogFunctions,};
formatStringis required. Bothfreesail prepare catalogandfreesail validate catalogwill warn if it is missing. Import it from@freesail/standard-catalogviacatalog.include.json, or implement it yourself.
Step 5: Register with FreesailProvider
Section titled “Step 5: Register with FreesailProvider”Pass your catalog to FreesailProvider via the catalogs prop:
import { ReactUI } from 'freesail';import { MyappCatalog } from 'myapp-catalog';
function App() { return ( <ReactUI.FreesailProvider theme={ReactUI.defaultLightTokens} catalogs={[MyappCatalog]} > {/* your app */} </ReactUI.FreesailProvider> );}Validation
Section titled “Validation”Run validation manually at any time:
npx freesail prepare catalog # Merge schemas and regenerate catalog JSONnpx freesail validate catalog # Check implementations match the schemaBoth accept --dir <path> to target a catalog in a different directory.
Validation checks:
- Every component key in the catalog JSON has a matching entry in the components map.
- Every function key in the catalog JSON has a matching implementation.
formatStringis present in the catalog (warns if missing).- Required schema fields (
catalogId,title) are set. - Warns if
catalogIduses a.localplaceholder domain.
CatalogDefinition API Reference
Section titled “CatalogDefinition API Reference”| Property | Type | Required | Description |
|---|---|---|---|
namespace | string | ✅ | The catalogId URI — must match schema.catalogId |
schema | object | ✅ | The parsed JSON schema object |
components | Record<string, ComponentType<FreesailComponentProps>> | ✅ | Component name → React component map |
functions | Record<string, FunctionImplementation> | ✅ | Function name → implementation map (must include formatString) |