import { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/react/schema"; 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: { // Layout Stack: { props: z.object({ direction: z.enum(["horizontal", "vertical"]).nullable(), gap: z.enum(["sm", "md", "lg"]).nullable(), wrap: z.boolean().nullable(), }), slots: ["default"], description: "Flex layout container", example: { direction: "vertical", gap: "md", wrap: null }, }, Card: { props: z.object({ title: z.string().nullable(), description: z.string().nullable(), }), slots: ["default"], description: "Card container with optional title and description", example: { title: "Weather", description: "Current conditions" }, }, Grid: { props: z.object({ columns: z.enum(["1", "2", "3", "4"]).nullable(), gap: z.enum(["sm", "md", "lg"]).nullable(), }), slots: ["default"], description: "Responsive grid layout container", example: { columns: "3", gap: "md" }, }, // Typography Heading: { props: z.object({ text: z.string(), level: z.enum(["h1", "h2", "h3", "h4"]).nullable(), }), description: "Section heading", example: { text: "Data Explorer", level: "h1" }, }, Text: { props: z.object({ content: z.string(), muted: z.boolean().nullable(), }), description: "Text content", example: { content: "Here is your data overview." }, }, // Data display Badge: { props: z.object({ text: z.string(), variant: z .enum(["default", "secondary", "destructive", "outline"]) .nullable(), }), description: "Status badge", example: { text: "Live", variant: "default" }, }, Alert: { props: z.object({ variant: z.enum(["default", "destructive"]).nullable(), title: z.string(), description: z.string().nullable(), }), description: "Alert or info message", }, Separator: { props: z.object({}), description: "Visual divider", }, 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", }, Progress: { props: z.object({ value: z.number(), max: z.number().nullable(), }), description: "Progress bar", }, Skeleton: { props: z.object({ width: z.string().nullable(), height: z.string().nullable(), }), description: "Loading placeholder", }, // 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.", }, }, Accordion: { props: z.object({ items: z.array( z.object({ title: z.string(), content: z.string(), }), ), type: z.enum(["single", "multiple"]).nullable(), }), description: "Collapsible accordion sections for organizing detailed content", example: { items: [{ title: "Overview", content: "A brief introduction." }], type: "multiple", }, }, 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: {}, });