mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-09-14 13:41:32 +08:00
320c935bfb
* shadcn * fix build error * fix readme * fix CI error * improvements * fixes * stronger types * fix interleave * fixes * on arg * minor fixes * fix lock
502 lines
14 KiB
TypeScript
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: {},
|
|
});
|