Files
Chris Tate 320c935bfb shadcn (#115)
* shadcn

* fix build error

* fix readme

* fix CI error

* improvements

* fixes

* stronger types

* fix interleave

* fixes

* on arg

* minor fixes

* fix lock
2026-02-17 01:28:04 -06:00

502 lines
14 KiB
TypeScript

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { z } from "zod";
// =============================================================================
// Shared 3D schemas
// =============================================================================
const vec3 = z.array(z.number());
const animation3D = z
.object({
rotate: vec3.nullable(),
})
.nullable();
const transform3DProps = {
position: vec3.nullable(),
rotation: vec3.nullable(),
scale: vec3.nullable(),
};
const material3DProps = {
color: z.string().nullable(),
metalness: z.number().nullable(),
roughness: z.number().nullable(),
emissive: z.string().nullable(),
emissiveIntensity: z.number().nullable(),
wireframe: z.boolean().nullable(),
opacity: z.number().nullable(),
};
const mesh3DProps = {
...transform3DProps,
...material3DProps,
args: z.array(z.number()).nullable(),
animation: animation3D,
};
/**
* json-render + AI SDK Example Catalog
*
* Components for rendering data dashboards generated by the ToolLoopAgent.
* Data flows in through tools (weather, GitHub, crypto, HN), not user actions.
*/
export const explorerCatalog = defineCatalog(schema, {
components: {
// From @json-render/shadcn (used as-is)
Stack: shadcnComponentDefinitions.Stack,
Card: shadcnComponentDefinitions.Card,
Grid: shadcnComponentDefinitions.Grid,
Heading: shadcnComponentDefinitions.Heading,
Separator: shadcnComponentDefinitions.Separator,
Accordion: shadcnComponentDefinitions.Accordion,
Progress: shadcnComponentDefinitions.Progress,
Skeleton: shadcnComponentDefinitions.Skeleton,
Badge: shadcnComponentDefinitions.Badge,
Alert: shadcnComponentDefinitions.Alert,
// Chat-specific components (different schemas or fully custom)
Text: {
props: z.object({
content: z.string(),
muted: z.boolean().nullable(),
}),
description: "Text content",
example: { content: "Here is your data overview." },
},
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
detail: z.string().nullable(),
trend: z.enum(["up", "down", "neutral"]).nullable(),
}),
description:
"Single metric display with label, value, and optional trend indicator",
example: {
label: "Temperature",
value: "72F",
detail: "Feels like 68F",
trend: "up",
},
},
Table: {
props: z.object({
data: z.array(z.record(z.string(), z.unknown())),
columns: z.array(
z.object({
key: z.string(),
label: z.string(),
}),
),
emptyMessage: z.string().nullable(),
}),
description:
'Data table. Use { "$state": "/path" } to bind read-only data from state.',
example: {
data: { $state: "/stories" },
columns: [
{ key: "title", label: "Title" },
{ key: "score", label: "Score" },
],
},
},
Link: {
props: z.object({
text: z.string(),
href: z.string(),
}),
description: "External link that opens in a new tab",
example: { text: "View on GitHub", href: "https://github.com" },
},
// Charts
BarChart: {
props: z.object({
title: z.string().nullable(),
data: z.array(z.record(z.string(), z.unknown())),
xKey: z.string(),
yKey: z.string(),
aggregate: z.enum(["sum", "count", "avg"]).nullable(),
color: z.string().nullable(),
height: z.number().nullable(),
}),
description:
'Bar chart visualization. Use { "$state": "/path" } to bind read-only data. xKey is the category field, yKey is the numeric value field.',
},
LineChart: {
props: z.object({
title: z.string().nullable(),
data: z.array(z.record(z.string(), z.unknown())),
xKey: z.string(),
yKey: z.string(),
aggregate: z.enum(["sum", "count", "avg"]).nullable(),
color: z.string().nullable(),
height: z.number().nullable(),
}),
description:
'Line chart visualization. Use { "$state": "/path" } to bind read-only data. xKey is the x-axis field, yKey is the numeric value field.',
},
// Interactive
Tabs: {
props: z.object({
defaultValue: z.string().nullable(),
tabs: z.array(
z.object({
value: z.string(),
label: z.string(),
}),
),
}),
slots: ["default"],
description: "Tabbed content container",
},
TabContent: {
props: z.object({
value: z.string(),
}),
slots: ["default"],
description: "Content for a specific tab",
},
// Educational / Rich content
Callout: {
props: z.object({
type: z.enum(["info", "tip", "warning", "important"]).nullable(),
title: z.string().nullable(),
content: z.string(),
}),
description:
"Highlighted callout box for tips, warnings, notes, or key information",
example: {
type: "tip",
title: "Did you know?",
content: "The sun is about 93 million miles from Earth.",
},
},
Timeline: {
props: z.object({
items: z.array(
z.object({
title: z.string(),
description: z.string().nullable(),
date: z.string().nullable(),
status: z.enum(["completed", "current", "upcoming"]).nullable(),
}),
),
}),
description:
"Vertical timeline showing ordered events, steps, or historical milestones",
example: {
items: [
{
title: "Discovery",
description: "Initial breakthrough",
date: "1905",
status: "completed",
},
],
},
},
PieChart: {
props: z.object({
title: z.string().nullable(),
data: z.array(z.record(z.string(), z.unknown())),
nameKey: z.string(),
valueKey: z.string(),
height: z.number().nullable(),
}),
description:
'Pie/donut chart for proportional data. Use { "$state": "/path" } to bind read-only data. nameKey is the label field, valueKey is the numeric value field.',
},
// Interactive / Input
RadioGroup: {
props: z.object({
label: z.string().nullable(),
value: z.string().nullable(),
options: z.array(
z.object({
value: z.string(),
label: z.string(),
}),
),
}),
description:
'Radio button group for single selection. Use { "$bindState": "/path" } for two-way binding. Use for multiple-choice questions, settings, or any single-select input.',
example: {
label: "Choose one",
value: { $bindState: "/answer" },
options: [
{ value: "a", label: "Option A" },
{ value: "b", label: "Option B" },
],
},
},
SelectInput: {
props: z.object({
label: z.string().nullable(),
value: z.string().nullable(),
placeholder: z.string().nullable(),
options: z.array(
z.object({
value: z.string(),
label: z.string(),
}),
),
}),
description:
'Dropdown select input. Use { "$bindState": "/path" } for two-way binding. Use when there are many options and a dropdown is more compact than radio buttons.',
example: {
label: "Country",
value: { $bindState: "/selectedCountry" },
placeholder: "Select a country",
options: [
{ value: "us", label: "United States" },
{ value: "uk", label: "United Kingdom" },
],
},
},
TextInput: {
props: z.object({
label: z.string().nullable(),
value: z.string().nullable(),
placeholder: z.string().nullable(),
type: z.enum(["text", "email", "number", "password", "url"]).nullable(),
}),
description:
'Text input field. Use { "$bindState": "/path" } for two-way binding. Use for free-text entry like names, emails, search, etc.',
example: {
label: "Your name",
value: { $bindState: "/userName" },
placeholder: "Enter your name",
type: "text",
},
},
Button: {
props: z.object({
label: z.string(),
variant: z
.enum(["default", "secondary", "destructive", "outline", "ghost"])
.nullable(),
size: z.enum(["default", "sm", "lg"]).nullable(),
disabled: z.boolean().nullable(),
}),
description:
"Clickable button. Use with on.press to trigger actions like setState, pushState, etc. Can be used for quiz submissions, form actions, navigation, and more.",
example: {
label: "Submit",
variant: "default",
size: "default",
disabled: null,
},
},
// =========================================================================
// 3D Scene Components (React Three Fiber)
// =========================================================================
// Containers
Scene3D: {
props: z.object({
height: z.string().nullable(),
background: z.string().nullable(),
cameraPosition: vec3.nullable(),
cameraFov: z.number().nullable(),
autoRotate: z.boolean().nullable(),
}),
slots: ["default"],
description:
"3D scene container with orbit controls. All 3D components (Sphere, Box, lights, etc.) must be children of a Scene3D. height is a CSS value like '500px'.",
example: {
height: "500px",
background: "#000010",
cameraPosition: [0, 25, 45],
cameraFov: null,
autoRotate: null,
},
},
Group3D: {
props: z.object({
...transform3DProps,
animation: animation3D,
}),
slots: ["default"],
description:
"3D group for positioning, rotating, and animating children together. Use to create orbits: position a planet inside a Group3D and animate the group's rotation.",
example: {
position: null,
rotation: null,
scale: null,
animation: { rotate: [0, 0.005, 0] },
},
},
// Geometry primitives
Box: {
props: z.object(mesh3DProps),
description:
"3D box/cube mesh. args: [width, height, depth]. Supports on.press for click interaction.",
example: {
position: [0, 0, 0],
color: "#4488ff",
args: [1, 1, 1],
},
},
Sphere: {
props: z.object(mesh3DProps),
description:
"3D sphere mesh. args: [radius, widthSegments, heightSegments]. Use higher segment counts (32+) for smooth spheres.",
example: {
position: [0, 0, 0],
color: "#4B7BE5",
args: [1, 32, 32],
},
},
Cylinder: {
props: z.object(mesh3DProps),
description:
"3D cylinder mesh. args: [radiusTop, radiusBottom, height, radialSegments].",
example: {
position: [0, 0, 0],
color: "#88aa44",
args: [1, 1, 2, 32],
},
},
Cone: {
props: z.object(mesh3DProps),
description: "3D cone mesh. args: [radius, height, radialSegments].",
example: {
position: [0, 0, 0],
color: "#ff8844",
args: [1, 2, 32],
},
},
Torus: {
props: z.object(mesh3DProps),
description:
"3D torus (donut) mesh. args: [radius, tube, radialSegments, tubularSegments].",
example: {
position: [0, 0, 0],
color: "#aa44ff",
args: [1, 0.4, 16, 100],
},
},
Plane: {
props: z.object(mesh3DProps),
description:
"3D flat plane mesh. args: [width, height]. Useful for ground planes or flat surfaces.",
example: {
position: [0, -1, 0],
rotation: [-Math.PI / 2, 0, 0],
color: "#334455",
args: [10, 10],
},
},
Ring: {
props: z.object(mesh3DProps),
description:
"3D flat ring mesh. args: [innerRadius, outerRadius, thetaSegments]. Great for orbit path indicators.",
example: {
position: [0, 0, 0],
rotation: [-Math.PI / 2, 0, 0],
color: "#ffffff",
opacity: 0.2,
args: [14.8, 15.2, 64],
},
},
// Lights
AmbientLight: {
props: z.object({
color: z.string().nullable(),
intensity: z.number().nullable(),
}),
description:
"Ambient light that illuminates all objects equally. Use for base scene illumination.",
example: { color: null, intensity: 0.3 },
},
PointLight: {
props: z.object({
position: vec3.nullable(),
color: z.string().nullable(),
intensity: z.number().nullable(),
distance: z.number().nullable(),
}),
description:
"Point light that emits from a position in all directions. Use for suns, lamps, etc.",
example: { position: [0, 0, 0], intensity: 2 },
},
DirectionalLight: {
props: z.object({
position: vec3.nullable(),
color: z.string().nullable(),
intensity: z.number().nullable(),
}),
description:
"Directional light like sunlight. Position sets direction, not location.",
example: { position: [5, 10, 5], intensity: 1 },
},
// Helpers (drei)
Stars: {
props: z.object({
radius: z.number().nullable(),
depth: z.number().nullable(),
count: z.number().nullable(),
factor: z.number().nullable(),
fade: z.boolean().nullable(),
speed: z.number().nullable(),
}),
description:
"Starfield background for space scenes. Renders thousands of tiny points around the scene.",
example: { count: 5000, fade: true },
},
Label3D: {
props: z.object({
text: z.string(),
position: vec3.nullable(),
rotation: vec3.nullable(),
color: z.string().nullable(),
fontSize: z.number().nullable(),
anchorX: z.enum(["left", "center", "right"]).nullable(),
anchorY: z.enum(["top", "middle", "bottom"]).nullable(),
}),
description:
"Text label rendered in 3D space. Always faces the camera (billboard). Use for labeling objects in a scene.",
example: {
text: "Earth",
position: [15, 2, 0],
color: "#ffffff",
fontSize: 0.8,
},
},
},
actions: {},
});