Compare commits

..
Author SHA1 Message Date
Chris Tate e9d484f985 update route.ts to use path.join for font resolution and adjust next-env.d.ts import path 2026-02-27 12:58:34 -06:00
Chris Tate d15a287645 disable @next/next/no-img-element rule in ESLint configuration 2026-02-27 12:53:11 -06:00
Chris Tate 3259c0ac85 fix ci 2026-02-27 12:52:04 -06:00
Chris Tate cb18dc5e88 fix ci 2026-02-27 12:50:09 -06:00
Chris Tate e07a5a02f1 fixes 2026-02-27 12:47:34 -06:00
Chris Tate e42fe26064 fixes 2026-02-26 21:28:02 -06:00
Chris Tate df6316d076 image 2026-02-26 20:56:08 -06:00
455 changed files with 756 additions and 41343 deletions
+1 -7
View File
@@ -6,7 +6,6 @@
[
"@json-render/core",
"@json-render/react",
"@json-render/react-email",
"@json-render/react-pdf",
"@json-render/shadcn",
"@json-render/react-native",
@@ -16,12 +15,7 @@
"@json-render/redux",
"@json-render/jotai",
"@json-render/vue",
"@json-render/xstate",
"@json-render/image",
"@json-render/mcp",
"@json-render/svelte",
"@json-render/solid",
"@json-render/react-three-fiber"
"@json-render/xstate"
]
],
"linked": [],
-8
View File
@@ -1,8 +0,0 @@
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
}
}
}
-2
View File
@@ -4,7 +4,6 @@
node_modules
.pnp
.pnp.js
.pnpm-store/
# Local env files
.env*
@@ -28,7 +27,6 @@ out/
build
dist
*.tsbuildinfo
.svelte-kit/
# Debug
-9
View File
@@ -1,9 +0,0 @@
{
"servers": {
"json-render": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
}
}
}
-35
View File
@@ -73,41 +73,6 @@ Do **not** add `--port` flags -- portless handles port assignment automatically.
- Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)
- `AGENTS.md` (if workflow or conventions change)
## Releases
This monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
### Fixed version group
All public `@json-render/*` packages are in a **fixed** group (see `.changeset/config.json`). A changeset that bumps any one of them bumps all of them to the same version. You only need to list the packages that actually changed in the changeset front matter — the fixed group handles the rest.
### Preparing a release
When asked to prepare a release (e.g. "prepare v0.12.0"):
1. **Create a changeset file** at `.changeset/v0-<N>-release.md` following the existing pattern:
- YAML front matter listing changed packages with bump type (`minor` for feature releases, `patch` for bug-fix-only releases)
- A one-line summary, then `### New:` / `### Improved:` / `### Fixed:` sections describing each change
- Always list `@json-render/core` plus any packages with actual code changes
2. **Do NOT bump versions** in `package.json` files — CI runs `pnpm ci:version` (which calls `changeset version`) to do that automatically
3. **Do NOT manually write `CHANGELOG.md`** entries — `changeset version` generates them from the changeset file
4. **Add new packages to the fixed group** in `.changeset/config.json` if they should be versioned together with the rest
5. **Fill documentation gaps** — every public package should have:
- A row in the root `README.md` packages table
- A renderer section in the root `README.md` (if it's a renderer)
- An API reference page at `apps/web/app/(main)/docs/api/<name>/page.mdx`
- An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`
- An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)
- A skill at `skills/<name>/SKILL.md`
- A `packages/<name>/README.md`
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
### CI scripts
- `pnpm changeset` — interactively create a new changeset
- `pnpm ci:version` — run `changeset version` + lockfile update (CI only)
- `pnpm ci:publish` — build all packages and publish to npm (CI only)
<!-- opensrc:start -->
## Source Code Reference
+35 -245
View File
@@ -15,16 +15,8 @@ npm install @json-render/core @json-render/react-native
npm install @json-render/core @json-render/remotion
# or for PDF documents
npm install @json-render/core @json-render/react-pdf
# or for HTML email
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
# or for Vue
npm install @json-render/core @json-render/vue
# or for Svelte
npm install @json-render/core @json-render/svelte
# or for SolidJS
npm install @json-render/core @json-render/solid
# or for 3D scenes
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
```
## Why json-render?
@@ -34,7 +26,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur
- **Guardrailed** - AI can only use components in your catalog
- **Predictable** - JSON output matches your schema, every time
- **Fast** - Stream and render progressively as the model responds
- **Cross-Platform** - React, Vue, Svelte, Solid (web), React Native (mobile) from the same catalog
- **Cross-Platform** - React, Vue (web), React Native (mobile) from the same catalog
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
## Quick Start
@@ -95,7 +87,9 @@ const { registry } = defineRegistry(catalog, {
</div>
),
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>{props.label}</button>
<button onClick={() => emit("press")}>
{props.label}
</button>
),
},
});
@@ -115,26 +109,19 @@ function Dashboard({ spec }) {
## Packages
| Package | Description |
| --------------------------- | ---------------------------------------------------------------------- |
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
| `@json-render/react` | React renderer, contexts, hooks |
| `@json-render/vue` | Vue 3 renderer, composables, providers |
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
| `@json-render/solid` | SolidJS renderer with fine-grained reactive contexts |
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components) |
| `@json-render/react-native` | React Native renderer with standard mobile components |
| `@json-render/remotion` | Remotion video renderer, timeline schema |
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
| `@json-render/codegen` | Utilities for generating code from json-render UI trees |
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
| `@json-render/zustand` | Zustand adapter for `StateStore` |
| `@json-render/jotai` | Jotai adapter for `StateStore` |
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
| Package | Description |
|---------|-------------|
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
| `@json-render/react` | React renderer, contexts, hooks |
| `@json-render/vue` | Vue 3 renderer, composables, providers |
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
| `@json-render/react-native` | React Native renderer with standard mobile components |
| `@json-render/remotion` | Remotion video renderer, timeline schema |
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
| `@json-render/zustand` | Zustand adapter for `StateStore` |
| `@json-render/jotai` | Jotai adapter for `StateStore` |
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
## Renderers
@@ -163,7 +150,7 @@ const spec = {
// defineRegistry creates a type-safe component registry
const { registry } = defineRegistry(catalog, { components });
<Renderer spec={spec} registry={registry} />;
<Renderer spec={spec} registry={registry} />
```
### Vue (UI)
@@ -186,43 +173,6 @@ const { registry } = defineRegistry(catalog, {
// <Renderer :spec="spec" :registry="registry" />
```
### Svelte (UI)
```typescript
import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => /* Svelte 5 snippet */,
Button: ({ props, emit }) => /* Svelte 5 snippet */,
},
});
// In your Svelte component:
// <Renderer spec={spec} registry={registry} />
```
### Solid (UI)
```tsx
import { defineRegistry, Renderer } from "@json-render/solid";
import { schema } from "@json-render/solid/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
Button: (renderProps) => (
<button onClick={() => renderProps.emit("press")}>
{renderProps.element.props.label as string}
</button>
),
},
});
<Renderer spec={spec} registry={registry} />;
```
### shadcn/ui (Web)
```tsx
@@ -253,7 +203,7 @@ const { registry } = defineRegistry(catalog, {
},
});
<Renderer spec={spec} registry={registry} />;
<Renderer spec={spec} registry={registry} />
```
### React Native (Mobile)
@@ -274,40 +224,23 @@ const catalog = defineCatalog(schema, {
});
const { registry } = defineRegistry(catalog, { components: {} });
<Renderer spec={spec} registry={registry} />;
<Renderer spec={spec} registry={registry} />
```
### Remotion (Video)
```tsx
import { Player } from "@remotion/player";
import {
Renderer,
schema,
standardComponentDefinitions,
} from "@json-render/remotion";
import { Renderer, schema, standardComponentDefinitions } from "@json-render/remotion";
// Timeline spec format
const spec = {
composition: {
id: "video",
fps: 30,
width: 1920,
height: 1080,
durationInFrames: 300,
},
composition: { id: "video", fps: 30, width: 1920, height: 1080, durationInFrames: 300 },
tracks: [{ id: "main", name: "Main", type: "video", enabled: true }],
clips: [
{
id: "clip-1",
trackId: "main",
component: "TitleCard",
props: { title: "Hello" },
from: 0,
durationInFrames: 90,
},
{ id: "clip-1", trackId: "main", component: "TitleCard", props: { title: "Hello" }, from: 0, durationInFrames: 90 }
],
audio: { tracks: [] },
audio: { tracks: [] }
};
<Player
@@ -317,7 +250,7 @@ const spec = {
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
/>;
/>
```
### React PDF (Documents)
@@ -328,11 +261,7 @@ import { renderToBuffer } from "@json-render/react-pdf";
const spec = {
root: "doc",
elements: {
doc: {
type: "Document",
props: { title: "Invoice" },
children: ["page-1"],
},
doc: { type: "Document", props: { title: "Invoice" }, children: ["page-1"] },
"page-1": {
type: "Page",
props: { size: "A4" },
@@ -346,14 +275,8 @@ const spec = {
"table-1": {
type: "Table",
props: {
columns: [
{ header: "Item", width: "60%" },
{ header: "Price", width: "40%", align: "right" },
],
rows: [
["Widget A", "$10.00"],
["Widget B", "$25.00"],
],
columns: [{ header: "Item", width: "60%" }, { header: "Price", width: "40%", align: "right" }],
rows: [["Widget A", "$10.00"], ["Widget B", "$25.00"]],
},
children: [],
},
@@ -364,120 +287,6 @@ const spec = {
const buffer = await renderToBuffer(spec);
```
### React Email (Email)
```typescript
import { renderToHtml } from "@json-render/react-email";
import { schema, standardComponentDefinitions } from "@json-render/react-email";
import { defineCatalog } from "@json-render/core";
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
const spec = {
root: "html-1",
elements: {
"html-1": {
type: "Html",
props: { lang: "en", dir: "ltr" },
children: ["head-1", "body-1"],
},
"head-1": { type: "Head", props: {}, children: [] },
"body-1": {
type: "Body",
props: { style: { backgroundColor: "#f6f9fc" } },
children: ["container-1"],
},
"container-1": {
type: "Container",
props: {
style: { maxWidth: "600px", margin: "0 auto", padding: "20px" },
},
children: ["heading-1", "text-1"],
},
"heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
"text-1": {
type: "Text",
props: { text: "Thanks for signing up." },
children: [],
},
},
};
const html = await renderToHtml(spec);
```
### Image (SVG/PNG)
```typescript
import { renderToPng } from "@json-render/image/render";
const spec = {
root: "frame",
elements: {
frame: {
type: "Frame",
props: { width: 1200, height: 630, backgroundColor: "#1a1a2e" },
children: ["heading"],
},
heading: {
type: "Heading",
props: { text: "Hello World", level: "h1", color: "#ffffff" },
children: [],
},
},
};
// Render to PNG (requires @resvg/resvg-js)
const png = await renderToPng(spec, { fonts });
// Or render to SVG string
import { renderToSvg } from "@json-render/image/render";
const svg = await renderToSvg(spec, { fonts });
```
### Three.js (3D)
```tsx
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
} from "@json-render/react-three-fiber";
const catalog = defineCatalog(schema, {
components: {
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
OrbitControls: threeComponentDefinitions.OrbitControls,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
OrbitControls: threeComponents.OrbitControls,
},
});
<ThreeCanvas
spec={spec}
registry={registry}
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
style={{ width: "100%", height: "100vh" }}
/>;
```
## Features
### Streaming (SpecStream)
@@ -527,16 +336,8 @@ Any prop value can be data-driven using expressions:
{
"type": "Icon",
"props": {
"name": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "home",
"$else": "home-outline"
},
"color": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "#007AFF",
"$else": "#8E8E93"
}
"name": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "home", "$else": "home-outline" },
"color": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "#007AFF", "$else": "#8E8E93" }
}
}
```
@@ -555,10 +356,7 @@ Components can trigger actions, including the built-in `setState` action:
```json
{
"type": "Pressable",
"props": {
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
},
"props": { "action": "setState", "actionParams": { "statePath": "/activeTab", "value": "home" } },
"children": ["home-icon"]
}
```
@@ -572,15 +370,9 @@ React to state changes by triggering actions:
```json
{
"type": "Select",
"props": {
"value": { "$bindState": "/form/country" },
"options": ["US", "Canada", "UK"]
},
"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada", "UK"] },
"watch": {
"/form/country": {
"action": "loadCities",
"params": { "country": { "$state": "/form/country" } }
}
"/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } }
}
}
```
@@ -600,12 +392,10 @@ pnpm dev
- http://json-render.localhost:1355 - Docs & Playground
- http://dashboard-demo.json-render.localhost:1355 - Example Dashboard
- http://react-email-demo.json-render.localhost:1355 - React Email Example
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
- Chat Example: run `pnpm dev` in `examples/chat`
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
- Vue Example: run `pnpm dev` in `examples/vue`
- Vite Renderers (React + Vue + Svelte + Solid): run `pnpm dev` in `examples/vite-renderers`
- Vite Renderers (React + Vue): run `pnpm dev` in `examples/vite-renderers`
- React Native example: run `npx expo start` in `examples/react-native`
## How It Works
@@ -615,7 +405,7 @@ flowchart LR
A[User Prompt] --> B[AI + Catalog]
B --> C[JSON Spec]
C --> D[Renderer]
B -.- E([guardrailed])
C -.- F([predictable])
D -.- G([streamed])
-36
View File
@@ -1,41 +1,5 @@
# web
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/codegen@0.13.0
- @json-render/react@0.13.0
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/codegen@0.12.1
- @json-render/react@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/codegen@0.12.0
- @json-render/react@0.12.0
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/codegen@0.11.0
- @json-render/react@0.11.0
## 0.1.3
### Patch Changes
+12 -12
View File
@@ -3,7 +3,7 @@ export const metadata = pageMetadata("docs/ai-sdk")
# AI SDK Integration
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Standalone** (standalone UI) and **Inline** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Generate** (standalone UI) and **Chat** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
## Installation
@@ -11,9 +11,9 @@ Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless str
npm install ai @ai-sdk/react
```
## Standalone Mode
## Generate Mode
In standalone mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
In generate mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
### API Route
@@ -73,9 +73,9 @@ function GenerativeUI() {
}
```
## Inline Mode
## Chat Mode
In inline mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
In chat mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
### API Route
@@ -96,7 +96,7 @@ export async function POST(req: Request) {
const result = streamText({
model: yourModel,
system: catalog.prompt({ mode: "inline" }),
system: catalog.prompt({ mode: "chat" }),
messages,
});
@@ -182,13 +182,13 @@ const systemPrompt = catalog.prompt({
});
```
### Inline Mode Prompt
### Chat Mode Prompt
```typescript
const inlinePrompt = catalog.prompt({ mode: "inline" });
const chatPrompt = catalog.prompt({ mode: "chat" });
```
In inline mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
In chat mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
## Which Mode?
@@ -197,8 +197,8 @@ In inline mode, the prompt instructs the AI to respond conversationally first, t
<thead>
<tr>
<th></th>
<th>Standalone</th>
<th>Inline</th>
<th>Generate</th>
<th>Chat</th>
</tr>
</thead>
<tbody>
@@ -215,7 +215,7 @@ In inline mode, the prompt instructs the AI to respond conversationally first, t
<tr>
<td>System prompt</td>
<td><code>catalog.prompt()</code></td>
<td><code>{"catalog.prompt({ mode: \"inline\" })"}</code></td>
<td><code>{"catalog.prompt({ mode: \"chat\" })"}</code></td>
</tr>
<tr>
<td>Stream utility</td>
+4 -4
View File
@@ -75,7 +75,7 @@ interface Catalog {
interface PromptOptions {
system?: string; // Custom system message intro
customRules?: string[]; // Additional rules to append
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
mode?: "generate" | "chat"; // Output mode (default: "generate")
}
interface SpecValidationResult<T> {
@@ -370,7 +370,7 @@ Most users should use `pipeJsonRender()` instead, which wraps this transform for
### createMixedStreamParser
Parse a mixed stream of text and JSONL patches (used for Inline mode):
Parse a mixed stream of text and JSONL patches (used for Chat + GenUI mode):
```typescript
import { createMixedStreamParser } from '@json-render/core';
@@ -389,7 +389,7 @@ parser.flush();
### pipeJsonRender
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Inline mode API routes.
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Chat mode API routes.
```typescript
import { pipeJsonRender } from '@json-render/core';
@@ -403,7 +403,7 @@ const stream = createUIMessageStream({
return createUIMessageStreamResponse({ stream });
```
See [Generation Modes](/docs/generation-modes) for full Inline mode setup.
See [Generation Modes](/docs/generation-modes) for full Chat mode setup.
### SpecStream Types
-364
View File
@@ -1,364 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/image")
# @json-render/image
Image renderer. Turn JSON specs into SVG and PNG images using [Satori](https://github.com/vercel/satori).
## Install
```bash
npm install @json-render/core @json-render/image
```
For PNG output, also install the optional peer dependency:
```bash
npm install @resvg/resvg-js
```
See the [Image example](https://github.com/vercel-labs/json-render/tree/main/examples/image) for a full working example.
## schema
The image element schema for image specs. Use with `defineCatalog` from core.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema, standardComponentDefinitions } from '@json-render/image';
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
```
## Render Functions
Server-side functions for producing image output. Both accept a spec and optional `RenderOptions`.
```typescript
import { renderToSvg, renderToPng } from '@json-render/image/render';
const svg = await renderToSvg(spec, { fonts });
const png = await renderToPng(spec, { fonts });
await writeFile('output.png', png);
```
### RenderOptions
```typescript
interface RenderOptions {
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
state?: Record<string, unknown>;
fonts?: SatoriOptions['fonts'];
width?: number;
height?: number;
}
```
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>fonts</code></td>
<td><code>{"SatoriOptions['fonts']"}</code></td>
<td><code>[]</code></td>
<td>Font data for text rendering (required for meaningful output)</td>
</tr>
<tr>
<td><code>width</code></td>
<td><code>number</code></td>
<td>Frame prop</td>
<td>Override the output image width</td>
</tr>
<tr>
<td><code>height</code></td>
<td><code>number</code></td>
<td>Frame prop</td>
<td>Override the output image height</td>
</tr>
<tr>
<td><code>registry</code></td>
<td><code>{"Record<string, ComponentRenderer>"}</code></td>
<td><code>{"{}"}</code></td>
<td>Custom component map (merged with standard components)</td>
</tr>
<tr>
<td><code>includeStandard</code></td>
<td><code>boolean</code></td>
<td><code>true</code></td>
<td>Include built-in standard components</td>
</tr>
<tr>
<td><code>state</code></td>
<td><code>{"Record<string, unknown>"}</code></td>
<td><code>{"{}"}</code></td>
<td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>
</tr>
</tbody>
</table>
## Standard Components
### Root
#### Frame
Root image container. Defines the output image dimensions and background. Must be the root element.
```typescript
{
width: number;
height: number;
backgroundColor: string | null;
padding: number | null;
display: "flex" | "none" | null;
flexDirection: "row" | "column" | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
}
```
### Layout
#### Box
Generic container with padding, margin, background, border, and flex alignment. Supports absolute positioning.
```typescript
{
padding: number | null;
paddingTop: number | null;
paddingBottom: number | null;
paddingLeft: number | null;
paddingRight: number | null;
margin: number | null;
backgroundColor: string | null;
borderWidth: number | null;
borderColor: string | null;
borderRadius: number | null;
flex: number | null;
width: number | string | null;
height: number | string | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
flexDirection: "row" | "column" | null;
position: "relative" | "absolute" | null;
top: number | null;
left: number | null;
right: number | null;
bottom: number | null;
overflow: "visible" | "hidden" | null;
}
```
#### Row
Horizontal flex layout with optional wrapping.
```typescript
{
gap: number | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
padding: number | null;
flex: number | null;
wrap: boolean | null;
}
```
#### Column
Vertical flex layout.
```typescript
{
gap: number | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
padding: number | null;
flex: number | null;
}
```
### Content
#### Heading
Heading text at various levels. h1 is largest, h4 is smallest.
```typescript
{
text: string;
level: "h1" | "h2" | "h3" | "h4" | null;
color: string | null;
align: "left" | "center" | "right" | null;
letterSpacing: number | string | null;
lineHeight: number | null;
}
```
#### Text
Body text with configurable size, color, weight, and alignment.
```typescript
{
text: string;
fontSize: number | null;
color: string | null;
align: "left" | "center" | "right" | null;
fontWeight: "normal" | "bold" | null;
fontStyle: "normal" | "italic" | null;
lineHeight: number | null;
letterSpacing: number | string | null;
textDecoration: "none" | "underline" | "line-through" | null;
}
```
#### Image
Image from a URL with optional dimensions and fit.
```typescript
{
src: string;
width: number | null;
height: number | null;
borderRadius: number | null;
objectFit: "contain" | "cover" | "fill" | "none" | null;
}
```
### Decorative
#### Divider
Horizontal line separator.
```typescript
{
color: string | null;
thickness: number | null;
marginTop: number | null;
marginBottom: number | null;
}
```
#### Spacer
Empty vertical space.
```typescript
{
height: number | null;
}
```
## Catalog Definitions
Pre-built definitions for creating image catalogs:
```typescript
import { standardComponentDefinitions } from '@json-render/image/catalog';
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/image';
const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
// Add custom components
},
});
```
## Server-Safe Import
Import schema and catalog definitions without pulling in React or Satori:
```typescript
import { schema, standardComponentDefinitions } from '@json-render/image/server';
```
## Sub-path Exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/image</code></td>
<td>Full package: schema, renderer, components, render functions</td>
</tr>
<tr>
<td><code>@json-render/image/server</code></td>
<td>Schema and catalog definitions only (no React or Satori)</td>
</tr>
<tr>
<td><code>@json-render/image/catalog</code></td>
<td>Standard component definitions and types</td>
</tr>
<tr>
<td><code>@json-render/image/render</code></td>
<td>Server-side render functions only</td>
</tr>
</tbody>
</table>
## Types
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ImageSchema</code></td>
<td>Schema type for image specs</td>
</tr>
<tr>
<td><code>ImageSpec</code></td>
<td>Spec type for image output</td>
</tr>
<tr>
<td><code>RenderOptions</code></td>
<td>Options for render functions</td>
</tr>
<tr>
<td><code>ComponentRenderProps</code></td>
<td>Props passed to component render functions</td>
</tr>
<tr>
<td><code>ComponentRenderer</code></td>
<td>Component render function type</td>
</tr>
<tr>
<td><code>ComponentRegistry</code></td>
<td>Map of component names to render functions</td>
</tr>
<tr>
<td><code>StandardComponentDefinitions</code></td>
<td>Type of the standard component definitions object</td>
</tr>
<tr>
<td><code>StandardComponentProps{'<K>'}</code></td>
<td>Inferred props type for a standard component by name</td>
</tr>
</tbody>
</table>
-104
View File
@@ -1,104 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/jotai")
# @json-render/jotai
Jotai adapter for json-render's `StateStore` interface.
## Installation
```bash
npm install @json-render/jotai @json-render/core @json-render/react jotai
```
## jotaiStateStore
Create a `StateStore` backed by a Jotai atom.
```typescript
import { jotaiStateStore } from "@json-render/jotai";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>atom</code></td>
<td><code>{'WritableAtom<StateModel, [StateModel], void>'}</code></td>
<td>Yes</td>
<td>A writable atom holding the state model.</td>
</tr>
<tr>
<td><code>store</code></td>
<td>Jotai <code>Store</code></td>
<td>No</td>
<td>The Jotai store instance. Defaults to a new store created internally. Pass your own to share state with <code>{'<Provider>'}</code>.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { atom } from "jotai";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
const store = jotaiStateStore({ atom: uiAtom });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Jotai */}
</StateProvider>
```
### Shared Jotai Store
If your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:
```typescript
import { atom, createStore } from "jotai";
import { Provider as JotaiProvider } from "jotai/react";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";
const jStore = createStore();
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
const store = jotaiStateStore({ atom: uiAtom, store: jStore });
```
```tsx
<JotaiProvider store={jStore}>
<StateProvider store={store}>
{/* Both json-render and useAtom() see the same state */}
</StateProvider>
</JotaiProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
-247
View File
@@ -1,247 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/mcp")
# @json-render/mcp
MCP Apps integration for json-render. Serve json-render UIs as interactive [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
## Install
```bash
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
```
For the iframe-side React UI, also install:
```bash
npm install @json-render/react react react-dom
```
See the [MCP example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp) for a full working example.
## Overview
MCP Apps let MCP servers return interactive HTML UIs that render directly inside chat conversations. `@json-render/mcp` bridges json-render catalogs with the MCP Apps protocol:
1. Your **catalog** defines which components and actions the AI can use
2. The **MCP server** exposes the catalog as a tool with the spec schema
3. The **bundled HTML** renders json-render specs inside the host's sandboxed iframe
4. The AI generates a spec, the host renders it, and users interact with the live UI
## Server API
### createMcpApp
Create a fully-configured MCP server. This is the main entry point.
```typescript
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
const server = createMcpApp({
name: "My Dashboard",
version: "1.0.0",
catalog: myCatalog,
html: fs.readFileSync("dist/index.html", "utf-8"),
});
await server.connect(new StdioServerTransport());
```
#### CreateMcpAppOptions
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td><code>string</code></td>
<td>Server name shown in client UIs</td>
</tr>
<tr>
<td><code>version</code></td>
<td><code>string</code></td>
<td>Server version</td>
</tr>
<tr>
<td><code>catalog</code></td>
<td><code>Catalog</code></td>
<td>json-render catalog defining available components</td>
</tr>
<tr>
<td><code>html</code></td>
<td><code>string</code></td>
<td>Self-contained HTML for the iframe UI</td>
</tr>
<tr>
<td><code>tool</code></td>
<td><code>McpToolOptions</code></td>
<td>Optional tool name/title/description overrides</td>
</tr>
</tbody>
</table>
### registerJsonRenderTool
Register a json-render tool on an existing `McpServer`. Use this when you need to add json-render to a server that has other tools.
```typescript
import { registerJsonRenderTool } from "@json-render/mcp";
registerJsonRenderTool(server, {
catalog,
name: "render-ui",
title: "Render UI",
description: "Render an interactive UI",
resourceUri: "ui://render-ui/view.html",
});
```
### registerJsonRenderResource
Register the UI resource that serves the bundled HTML.
```typescript
import { registerJsonRenderResource } from "@json-render/mcp";
registerJsonRenderResource(server, {
resourceUri: "ui://render-ui/view.html",
html: bundledHtml,
});
```
## Client API (`@json-render/mcp/app`)
These exports run inside the sandboxed iframe rendered by the MCP host.
### useJsonRenderApp
React hook that connects to the MCP host, listens for tool results, and maintains the current json-render spec.
```tsx
import { useJsonRenderApp } from "@json-render/mcp/app";
import { JSONUIProvider, Renderer } from "@json-render/react";
function McpAppView({ registry }) {
const { spec, loading, connected, error } = useJsonRenderApp({
name: "my-app",
version: "1.0.0",
});
if (error) return <div>Error: {error.message}</div>;
if (!spec) return <div>Waiting...</div>;
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
);
}
```
#### UseJsonRenderAppReturn
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>spec</code></td>
<td><code>{'Spec | null'}</code></td>
<td>Current json-render spec</td>
</tr>
<tr>
<td><code>loading</code></td>
<td><code>boolean</code></td>
<td>Whether the spec is still being received</td>
</tr>
<tr>
<td><code>connected</code></td>
<td><code>boolean</code></td>
<td>Whether connected to the host</td>
</tr>
<tr>
<td><code>connecting</code></td>
<td><code>boolean</code></td>
<td>Whether currently connecting</td>
</tr>
<tr>
<td><code>error</code></td>
<td><code>{'Error | null'}</code></td>
<td>Connection error, if any</td>
</tr>
<tr>
<td><code>app</code></td>
<td><code>{'App | null'}</code></td>
<td>The underlying MCP App instance</td>
</tr>
<tr>
<td><code>callServerTool</code></td>
<td><code>{'(name, args?) => Promise<void>'}</code></td>
<td>Call an MCP server tool and update spec from result</td>
</tr>
</tbody>
</table>
### buildAppHtml
Generate a self-contained HTML page from bundled JavaScript and CSS.
```typescript
import { buildAppHtml } from "@json-render/mcp/app";
import fs from "node:fs";
const html = buildAppHtml({
title: "Dashboard",
js: fs.readFileSync("dist/app.js", "utf-8"),
css: fs.readFileSync("dist/app.css", "utf-8"),
});
```
## Client Configuration
### Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "path/to/server.ts", "--stdio"]
}
}
}
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/server.ts", "--stdio"]
}
}
}
```
## Supported Clients
MCP Apps are supported by Claude (web and desktop), ChatGPT, VS Code (GitHub Copilot), Cursor, Goose, and Postman.
@@ -1,310 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-email")
# @json-render/react-email
React Email renderer. Turn JSON specs into HTML or plain-text emails using `@react-email/components` and `@react-email/render`.
## Install
```bash
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
```
See the [React Email example](https://github.com/vercel-labs/json-render/tree/main/examples/react-email) for a full working example.
## schema
The email element schema for specs. Use with `defineCatalog` from core.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema, standardComponentDefinitions } from '@json-render/react-email';
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
```
## Render Functions
Server-side functions for producing email output. All accept a spec and optional `RenderOptions`.
```typescript
import { renderToHtml, renderToPlainText } from '@json-render/react-email';
const html = await renderToHtml(spec);
const plainText = await renderToPlainText(spec);
```
### RenderOptions
```typescript
interface RenderOptions {
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
state?: Record<string, unknown>;
}
```
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>registry</code></td>
<td>Custom component map (merged with standard components)</td>
</tr>
<tr>
<td><code>includeStandard</code></td>
<td>Include built-in standard components (default: <code>true</code>)</td>
</tr>
<tr>
<td><code>state</code></td>
<td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>
</tr>
</tbody>
</table>
## defineRegistry
Create a type-safe component registry from a catalog. Components receive `{ props, children, emit, bindings, loading }`.
```tsx
import { defineRegistry } from '@json-render/react-email';
import { Container, Heading, Text } from '@react-email/components';
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<Container style={{ padding: 16, backgroundColor: '#fff' }}>
<Heading>{props.title}</Heading>
{children}
</Container>
),
},
});
const html = await renderToHtml(spec, { registry });
```
## createRenderer
Create a standalone renderer component wired to state, actions, and validation (for interactive previews in the browser).
```typescript
import { createRenderer } from '@json-render/react-email';
const EmailRenderer = createRenderer(catalog, components);
```
## Renderer
The main component that renders a spec to React Email elements. Use inside `JSONUIProvider` when you need state, actions, or visibility.
```typescript
interface RendererProps {
spec: Spec | null;
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
loading?: boolean;
fallback?: ComponentRenderer;
}
```
## Standard Components
### Document structure
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Html</code></td>
<td>Top-level email wrapper. Must be the root element.</td>
</tr>
<tr>
<td><code>Head</code></td>
<td>Email head section. Place inside Html.</td>
</tr>
<tr>
<td><code>Body</code></td>
<td>Email body wrapper. Place inside Html.</td>
</tr>
</tbody>
</table>
### Layout
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Container</code></td>
<td>Constrains content width (e.g. max-width 600px).</td>
</tr>
<tr>
<td><code>Section</code></td>
<td>Groups related content.</td>
</tr>
<tr>
<td><code>Row</code></td>
<td>Horizontal layout row.</td>
</tr>
<tr>
<td><code>Column</code></td>
<td>Column within a Row.</td>
</tr>
</tbody>
</table>
### Content
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Heading</code></td>
<td>Heading text (h1-h6).</td>
</tr>
<tr>
<td><code>Text</code></td>
<td>Body text paragraph.</td>
</tr>
<tr>
<td><code>Link</code></td>
<td>Hyperlink with text and href.</td>
</tr>
<tr>
<td><code>Button</code></td>
<td>Call-to-action button (link styled as button).</td>
</tr>
<tr>
<td><code>Image</code></td>
<td>Image from URL.</td>
</tr>
<tr>
<td><code>Hr</code></td>
<td>Horizontal rule separator.</td>
</tr>
</tbody>
</table>
### Utility
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Preview</code></td>
<td>Preview text for inbox (inside Html).</td>
</tr>
<tr>
<td><code>Markdown</code></td>
<td>Renders markdown content as email-safe HTML.</td>
</tr>
</tbody>
</table>
## Server-Safe Import
Import schema and catalog definitions without pulling in React or `@react-email/components`:
```typescript
import { schema, standardComponentDefinitions } from '@json-render/react-email/server';
```
## Sub-path Exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/react-email</code></td>
<td>Full package: schema, renderer, components, render functions</td>
</tr>
<tr>
<td><code>@json-render/react-email/server</code></td>
<td>Schema and catalog definitions only (no React)</td>
</tr>
<tr>
<td><code>@json-render/react-email/catalog</code></td>
<td>Standard component definitions and types</td>
</tr>
<tr>
<td><code>@json-render/react-email/render</code></td>
<td>Server-side render functions only</td>
</tr>
</tbody>
</table>
## Types
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ReactEmailSchema</code></td>
<td>Schema type for email specs</td>
</tr>
<tr>
<td><code>ReactEmailSpec</code></td>
<td>Spec type for email documents</td>
</tr>
<tr>
<td><code>RenderOptions</code></td>
<td>Options for render functions</td>
</tr>
<tr>
<td><code>ComponentContext</code></td>
<td>Typed component render function context</td>
</tr>
<tr>
<td><code>ComponentFn</code></td>
<td>Component render function type</td>
</tr>
<tr>
<td><code>StandardComponentDefinitions</code></td>
<td>Type of the standard component definitions object</td>
</tr>
<tr>
<td><code>StandardComponentProps&lt;K&gt;</code></td>
<td>Inferred props type for a standard component by name</td>
</tr>
</tbody>
</table>
@@ -1,416 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-three-fiber")
# @json-render/react-three-fiber
React Three Fiber renderer for json-render. 19 built-in 3D components for meshes, lights, models, environments, text, cameras, and controls.
## Installation
```bash
npm install @json-render/react-three-fiber @json-render/core @json-render/react @react-three/fiber @react-three/drei three zod
```
## Entry Points
<table>
<thead>
<tr>
<th>Entry Point</th>
<th>Exports</th>
<th>Use For</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/react-three-fiber</code></td>
<td><code>threeComponents</code>, <code>ThreeRenderer</code>, <code>ThreeCanvas</code>, schemas</td>
<td>React Three Fiber implementations and renderer</td>
</tr>
<tr>
<td><code>@json-render/react-three-fiber/catalog</code></td>
<td><code>threeComponentDefinitions</code></td>
<td>Catalog schemas (no R3F dependency, safe for server)</td>
</tr>
</tbody>
</table>
## Usage
```tsx
import {'{ defineCatalog }'} from "@json-render/core";
import {'{ schema, defineRegistry }'} from "@json-render/react";
import {'{'}
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
{'}'} from "@json-render/react-three-fiber";
const catalog = defineCatalog(schema, {'{'}
components: {'{'}
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
OrbitControls: threeComponentDefinitions.OrbitControls,
{'}'},
actions: {'{}'},
{'}'});
const {'{ registry }'} = defineRegistry(catalog, {'{'}
components: {'{'}
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
OrbitControls: threeComponents.OrbitControls,
{'}'},
{'}'});
```
### ThreeCanvas (convenience)
```tsx
<ThreeCanvas
spec={'{spec}'}
registry={'{registry}'}
shadows
camera={'{'}{'{ position: [5, 5, 5], fov: 50 }'}{'}'}
style={'{'}{'{ width: "100%", height: "100vh" }'}{'}'}
/>
```
### Manual Canvas Setup
```tsx
import {'{ Canvas }'} from "@react-three/fiber";
import {'{ ThreeRenderer }'} from "@json-render/react-three-fiber";
<Canvas shadows>
<ThreeRenderer spec={'{spec}'} registry={'{registry}'} />
</Canvas>
```
## Components
### Primitives
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Box</code></td>
<td>Box mesh (default 1x1x1)</td>
<td><code>width</code>, <code>height</code>, <code>depth</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Sphere</code></td>
<td>Sphere mesh</td>
<td><code>radius</code>, <code>widthSegments</code>, <code>heightSegments</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Cylinder</code></td>
<td>Cylinder mesh</td>
<td><code>radiusTop</code>, <code>radiusBottom</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Cone</code></td>
<td>Cone mesh</td>
<td><code>radius</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Torus</code></td>
<td>Torus (donut) mesh</td>
<td><code>radius</code>, <code>tube</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Plane</code></td>
<td>Flat plane mesh</td>
<td><code>width</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Capsule</code></td>
<td>Capsule mesh</td>
<td><code>radius</code>, <code>length</code>, <code>material</code></td>
</tr>
</tbody>
</table>
All primitives share: <code>position</code>, <code>rotation</code>, <code>scale</code>, <code>castShadow</code>, <code>receiveShadow</code>, <code>material</code>.
### Material Schema
<table>
<thead>
<tr>
<th>Property</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>color</code></td>
<td><code>string</code></td>
<td><code>"#ffffff"</code></td>
</tr>
<tr>
<td><code>metalness</code></td>
<td><code>number</code></td>
<td><code>0</code></td>
</tr>
<tr>
<td><code>roughness</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>emissive</code></td>
<td><code>string</code></td>
<td><code>"#000000"</code></td>
</tr>
<tr>
<td><code>emissiveIntensity</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>opacity</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>transparent</code></td>
<td><code>boolean</code></td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>wireframe</code></td>
<td><code>boolean</code></td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
### Lights
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AmbientLight</code></td>
<td>Uniform illumination</td>
<td><code>color</code>, <code>intensity</code></td>
</tr>
<tr>
<td><code>DirectionalLight</code></td>
<td>Sunlight-style</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>castShadow</code></td>
</tr>
<tr>
<td><code>PointLight</code></td>
<td>Radiates from a point</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>distance</code>, <code>decay</code></td>
</tr>
<tr>
<td><code>SpotLight</code></td>
<td>Cone of light</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>angle</code>, <code>penumbra</code></td>
</tr>
</tbody>
</table>
### Other Components
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Group</code></td>
<td>Container for children</td>
<td><code>position</code>, <code>rotation</code>, <code>scale</code></td>
</tr>
<tr>
<td><code>Model</code></td>
<td>GLTF/GLB model loader</td>
<td><code>url</code>, <code>position</code>, <code>rotation</code>, <code>scale</code></td>
</tr>
<tr>
<td><code>Environment</code></td>
<td>HDRI environment map</td>
<td><code>preset</code>, <code>background</code>, <code>blur</code>, <code>intensity</code></td>
</tr>
<tr>
<td><code>Fog</code></td>
<td>Linear fog effect</td>
<td><code>color</code>, <code>near</code>, <code>far</code></td>
</tr>
<tr>
<td><code>GridHelper</code></td>
<td>Reference grid</td>
<td><code>size</code>, <code>divisions</code>, <code>color</code></td>
</tr>
<tr>
<td><code>Text3D</code></td>
<td>3D text (SDF)</td>
<td><code>text</code>, <code>fontSize</code>, <code>color</code>, <code>anchorX</code>, <code>anchorY</code></td>
</tr>
<tr>
<td><code>PerspectiveCamera</code></td>
<td>Camera</td>
<td><code>position</code>, <code>fov</code>, <code>near</code>, <code>far</code>, <code>makeDefault</code></td>
</tr>
<tr>
<td><code>OrbitControls</code></td>
<td>Camera controls</td>
<td><code>enableDamping</code>, <code>enableZoom</code>, <code>autoRotate</code></td>
</tr>
</tbody>
</table>
## Shared Schemas
Reusable Zod schemas for custom 3D components:
```tsx
import {'{ vector3Schema, materialSchema, transformProps, shadowProps }'} from "@json-render/react-three-fiber";
```
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>vector3Schema</code></td>
<td><code>z.tuple([z.number(), z.number(), z.number()])</code></td>
</tr>
<tr>
<td><code>materialSchema</code></td>
<td>Standard material props (color, metalness, roughness, etc.)</td>
</tr>
<tr>
<td><code>transformProps</code></td>
<td><code>{'{ position, rotation, scale }'}</code> schema fields</td>
</tr>
<tr>
<td><code>shadowProps</code></td>
<td><code>{'{ castShadow, receiveShadow }'}</code> schema fields</td>
</tr>
</tbody>
</table>
## ThreeRenderer
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>spec</code></td>
<td><code>Spec | null</code></td>
<td>The spec to render as a 3D scene</td>
</tr>
<tr>
<td><code>registry</code></td>
<td><code>ComponentRegistry</code></td>
<td>Component registry from <code>defineRegistry</code></td>
</tr>
<tr>
<td><code>store</code></td>
<td><code>StateStore</code></td>
<td>External state store (controlled mode)</td>
</tr>
<tr>
<td><code>initialState</code></td>
<td><code>Record&lt;string, unknown&gt;</code></td>
<td>Initial state (uncontrolled mode)</td>
</tr>
<tr>
<td><code>handlers</code></td>
<td><code>Record&lt;string, Function&gt;</code></td>
<td>Action handlers</td>
</tr>
<tr>
<td><code>loading</code></td>
<td><code>boolean</code></td>
<td>Whether the spec is streaming</td>
</tr>
<tr>
<td><code>children</code></td>
<td><code>ReactNode</code></td>
<td>Additional R3F elements alongside the spec</td>
</tr>
</tbody>
</table>
## ThreeCanvas
Extends <code>ThreeRendererProps</code> with Canvas options:
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shadows</code></td>
<td><code>boolean</code></td>
<td>Enable shadow maps</td>
</tr>
<tr>
<td><code>camera</code></td>
<td><code>object</code></td>
<td>Default camera config (position, fov, etc.)</td>
</tr>
<tr>
<td><code>className</code></td>
<td><code>string</code></td>
<td>CSS class for the canvas container</td>
</tr>
<tr>
<td><code>style</code></td>
<td><code>CSSProperties</code></td>
<td>Inline styles for the canvas container</td>
</tr>
</tbody>
</table>
## Type Helpers
```tsx
import type {'{ ThreeProps }'} from "@json-render/react-three-fiber";
type BoxProps = ThreeProps<"Box">;
type SphereProps = ThreeProps<"Sphere">;
```
-104
View File
@@ -1,104 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/redux")
# @json-render/redux
Redux / Redux Toolkit adapter for json-render's `StateStore` interface.
## Installation
```bash
npm install @json-render/redux @json-render/core @json-render/react redux
# or with Redux Toolkit (recommended):
npm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit
```
## reduxStateStore
Create a `StateStore` backed by a Redux store.
```typescript
import { reduxStateStore } from "@json-render/redux";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>Store</code></td>
<td>Yes</td>
<td>The Redux store instance.</td>
</tr>
<tr>
<td><code>selector</code></td>
<td><code>{'(state: S) => StateModel'}</code></td>
<td>No</td>
<td>Select the json-render slice from the Redux state tree. Defaults to <code>{'(state) => state'}</code>.</td>
</tr>
<tr>
<td><code>dispatch</code></td>
<td><code>{'(nextState: StateModel, store: Store) => void'}</code></td>
<td>Yes</td>
<td>Dispatch an action that replaces the selected slice with the next state.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { reduxStateStore } from "@json-render/redux";
import { StateProvider } from "@json-render/react";
const uiSlice = createSlice({
name: "ui",
initialState: { count: 0 } as Record<string, unknown>,
reducers: {
replaceUiState: (_state, action) => action.payload,
},
});
const reduxStore = configureStore({
reducer: { ui: uiSlice.reducer },
});
const store = reduxStateStore({
store: reduxStore,
selector: (state) => state.ui,
dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),
});
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Redux */}
</StateProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
-201
View File
@@ -1,201 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/api/solid");
# @json-render/solid
SolidJS components, providers, and hooks for rendering json-render specs.
## Installation
<PackageInstall packages="@json-render/core @json-render/solid" />
Peer dependencies: `solid-js ^1.9.0` and `zod ^4.0.0`.
<PackageInstall packages="solid-js zod" />
## Providers
### StateProvider
```tsx
<StateProvider
initialState={{}}
onStateChange={(changes) => console.log(changes)}
>
{/* children */}
</StateProvider>
```
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>store</code>
</td>
<td>
<code>StateStore</code>
</td>
<td>
External store (controlled mode). When provided,{" "}
<code>initialState</code> and <code>onStateChange</code> are ignored.
</td>
</tr>
<tr>
<td>
<code>initialState</code>
</td>
<td>
<code>Record&lt;string, unknown&gt;</code>
</td>
<td>Initial state model for uncontrolled mode.</td>
</tr>
<tr>
<td>
<code>onStateChange</code>
</td>
<td>
<code>
{"(changes: Array<{ path: string; value: unknown }>) => void"}
</code>
</td>
<td>Called for uncontrolled state updates.</td>
</tr>
</tbody>
</table>
### ActionProvider
```tsx
<ActionProvider
handlers={{ submit: async (params) => {} }}
navigate={(path) => {}}
>
{/* children */}
</ActionProvider>
```
### VisibilityProvider
```tsx
<VisibilityProvider>{/* children */}</VisibilityProvider>
```
### ValidationProvider
```tsx
<ValidationProvider customFunctions={{ custom: (value) => Boolean(value) }}>
{/* children */}
</ValidationProvider>
```
### JSONUIProvider
Combined provider wrapper for state, visibility, validation, and actions.
```tsx
<JSONUIProvider
registry={registry}
initialState={{}}
handlers={handlers}
validationFunctions={validationFunctions}
>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
```
## defineRegistry
Create a typed component registry and action helpers from a catalog.
```tsx
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
Button: (renderProps) => (
<button onClick={() => renderProps.emit("press")}>
{renderProps.element.props.label as string}
</button>
),
},
actions: {
submit: async (params, setState, state) => {
// custom action logic
},
},
});
```
## Components
### Renderer
```tsx
<Renderer spec={spec} registry={registry} loading={false} />
```
Renders a `Spec` tree using your registry.
### createRenderer
Build an app-level renderer from catalog + components:
```tsx
const AppRenderer = createRenderer(catalog, {
Card: (renderProps) => <div>{renderProps.children}</div>,
});
<AppRenderer spec={spec} state={{}} onAction={(name, params) => {}} />;
```
## Hooks
- `useStateStore()`
- `useStateValue(path)` - returns an accessor
- `useStateBinding(path)` - returns `[Accessor<T | undefined>, setValue]`
- `useVisibility()` / `useIsVisible(condition)`
- `useActions()` / `useAction(binding)`
- `useValidation()` / `useOptionalValidation()`
- `useFieldValidation(path, config)` - returns accessor-backed `state`, `errors`, and `isValid`
- `useBoundProp(value, bindingPath)`
- `useUIStream(options)`
- `useChatUI(options)`
## Built-in Actions
`ActionProvider` handles these built-in actions:
- `setState`
- `pushState`
- `removeState`
- `validateForm`
## Component Props
Registry components receive:
```ts
interface ComponentRenderProps<P = Record<string, unknown>> {
element: UIElement<string, P>;
children?: JSX.Element;
emit: (event: string) => void;
on: (event: string) => EventHandle;
bindings?: Record<string, string>;
loading?: boolean;
}
```
Use `emit("event")` to dispatch event bindings. Use `on("event")` to access `EventHandle` metadata (`bound`, `shouldPreventDefault`, `emit`).
## Reactivity Notes
- Keep changing reads in JSX expressions, `createMemo`, or `createEffect`.
- Avoid props destructuring in component signatures when you need live updates.
- `StateProvider` and other contexts expose getter-backed values so consumers read live signals.
- `useStateValue`, `useStateBinding`, and `useFieldValidation` expose reactive accessors; call them as functions.
@@ -1,126 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/svelte")
# @json-render/svelte
Svelte 5 components, providers, and helpers for rendering json-render specs.
## Installation
<PackageInstall packages="@json-render/core @json-render/svelte" />
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
<PackageInstall packages="svelte zod" />
## Components
### Renderer
```svelte
<Renderer
spec={spec} // Spec | null
registry={registry}
loading={false}
/>
```
Renders a spec with your component registry. If `spec` is `null`, it renders nothing.
### JsonUIProvider
Convenience wrapper around `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`.
```svelte
<JsonUIProvider
initialState={{}}
handlers={handlers}
validationFunctions={validationFunctions}
>
<Renderer {spec} {registry} />
</JsonUIProvider>
```
## defineRegistry
Create a typed component registry and action handlers from a catalog.
```typescript
import { defineRegistry } from "@json-render/svelte";
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card,
Button,
},
actions: {
submit: async (params, setState, state) => {
// custom action logic
},
},
});
```
`handlers` is designed for `JsonUIProvider`/`ActionProvider`. `executeAction` is an imperative helper.
## Component Props
Registry components receive `BaseComponentProps<TProps>`:
```typescript
interface BaseComponentProps<TProps> {
props: TProps;
children?: Snippet;
emit: (event: string) => void;
bindings?: Record<string, string>;
loading?: boolean;
}
```
Use `emit("eventName")` to trigger handlers declared in the spec `on` bindings.
## Context Helpers
Use these helpers inside Svelte components:
- `getStateValue(path)` - read/write state via `.current`
- `getBoundProp(() => value, () => bindingPath)` - write back resolved `$bindState` / `$bindItem` values
- `isVisible(condition)` - evaluate visibility via `.current`
- `getAction(name)` - read a registered action handler via `.current`
- `getFieldValidation(ctx, path, config)` - get field validation state + actions
For advanced usage, access full contexts:
- `getStateContext()`
- `getActionContext()`
- `getVisibilityContext()`
- `getValidationContext()`
- `getOptionalValidationContext()`
## Streaming
### createUIStream
```typescript
const stream = createUIStream({
api: "/api/generate-ui",
onComplete: (spec) => console.log(spec),
});
await stream.send("Create a login form");
console.log(stream.spec);
console.log(stream.isStreaming);
```
### createChatUI
```typescript
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");
console.log(chat.messages, chat.isStreaming);
```
## Schema Export
Use `schema` from `@json-render/svelte` when defining catalogs for Svelte specs.
@@ -1,77 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/xstate")
# @json-render/xstate
[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface.
Requires `@xstate/store` v3+.
## Installation
```bash
npm install @json-render/xstate @json-render/core @json-render/react @xstate/store
```
## xstateStoreStateStore
Create a `StateStore` backed by an `@xstate/store` atom.
```typescript
import { xstateStoreStateStore } from "@json-render/xstate";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>atom</code></td>
<td><code>{'Atom<StateModel>'}</code></td>
<td>Yes</td>
<td>An <code>@xstate/store</code> atom (from <code>createAtom</code>) holding the json-render state model.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { createAtom } from "@xstate/store";
import { xstateStoreStateStore } from "@json-render/xstate";
import { StateProvider } from "@json-render/react";
const uiAtom = createAtom({ count: 0 });
const store = xstateStoreStateStore({ atom: uiAtom });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through @xstate/store */}
</StateProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
@@ -1,108 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/zustand")
# @json-render/zustand
Zustand adapter for json-render's `StateStore` interface.
Requires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface.
## Installation
```bash
npm install @json-render/zustand @json-render/core @json-render/react zustand
```
## zustandStateStore
Create a `StateStore` backed by a Zustand vanilla store.
```typescript
import { zustandStateStore } from "@json-render/zustand";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>{'StoreApi<S>'}</code></td>
<td>Yes</td>
<td>A Zustand vanilla store (from <code>createStore</code> in <code>zustand/vanilla</code>).</td>
</tr>
<tr>
<td><code>selector</code></td>
<td><code>{'(state: S) => StateModel'}</code></td>
<td>No</td>
<td>Select the json-render slice from the store state. Defaults to the entire state.</td>
</tr>
<tr>
<td><code>updater</code></td>
<td><code>{'(nextState: StateModel, store: StoreApi<S>) => void'}</code></td>
<td>No</td>
<td>Apply a state change back to the store. Defaults to a shallow merge.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { createStore } from "zustand/vanilla";
import { zustandStateStore } from "@json-render/zustand";
import { StateProvider } from "@json-render/react";
const bearStore = createStore(() => ({
count: 0,
name: "Bear",
}));
const store = zustandStateStore({ store: bearStore });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Zustand */}
</StateProvider>
```
### Nested Slice
```typescript
const appStore = createStore(() => ({
ui: { count: 0 },
auth: { token: null },
}));
const store = zustandStateStore({
store: appStore,
selector: (s) => s.ui,
updater: (next, s) => s.setState({ ui: next }),
});
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
@@ -341,8 +341,6 @@ February 2026
### New: Chat Mode (Inline GenUI)
> **Note:** These modes were renamed in v0.12.1 — "Generate" is now "Standalone" and "Chat" is now "Inline". The old names are accepted as deprecated aliases.
json-render now supports two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets the AI respond conversationally with embedded UI specs, ideal for chatbots and copilot experiences.
```typescript
@@ -3,15 +3,15 @@ export const metadata = pageMetadata("docs/generation-modes")
# Generation Modes
json-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.
json-render supports two modes for AI-generated UI: **Generate mode** for standalone UI and **Chat mode** for inline UI within a conversation.
The mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.
<GenerationModesDiagram />
## Standalone Mode
## Generate Mode (Standalone)
In standalone mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
In generate mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
This is the default mode and is ideal for:
@@ -25,7 +25,7 @@ This is the default mode and is ideal for:
```typescript
import { streamText } from "ai";
// Standalone mode is the default (no mode option needed)
// Generate mode is the default (no mode option needed)
const systemPrompt = catalog.prompt({
customRules: [
"Use Card as root for forms and small UIs.",
@@ -74,9 +74,9 @@ The AI outputs only JSONL — one patch per line, no surrounding text:
{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Sign In"}}}
```
## Inline Mode
## Chat Mode (Inline)
In inline mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
In chat mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
This is ideal for:
@@ -92,8 +92,8 @@ import { streamText } from "ai";
import { pipeJsonRender } from "@json-render/core";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
// Enable inline mode
const systemPrompt = catalog.prompt({ mode: "inline" });
// Enable chat mode
const systemPrompt = catalog.prompt({ mode: "chat" });
const result = streamText({
model: yourModel,
@@ -180,8 +180,8 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
<thead>
<tr>
<th />
<th>Standalone</th>
<th>Inline</th>
<th>Generate</th>
<th>Chat</th>
</tr>
</thead>
<tbody>
@@ -198,7 +198,7 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
<tr>
<td>System prompt</td>
<td><code>{"catalog.prompt()"}</code></td>
<td><code>{'catalog.prompt({ mode: "inline" })'}</code></td>
<td><code>{'catalog.prompt({ mode: "chat" })'}</code></td>
</tr>
<tr>
<td>Stream utility</td>
@@ -21,14 +21,6 @@ Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.
<PackageInstall packages="vue zod" />
## For Svelte
<PackageInstall packages="@json-render/core @json-render/svelte" />
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
<PackageInstall packages="svelte zod" />
## For React UI with shadcn/ui
Pre-built components for fast prototyping and production use:
@@ -45,10 +37,6 @@ Requires Tailwind CSS in your project. See the [@json-render/shadcn API referenc
<PackageInstall packages="@json-render/core @json-render/remotion remotion @remotion/player" />
## For React Email
<PackageInstall packages="@json-render/core @json-render/react-email @react-email/components @react-email/render" />
## For External State Management (Optional)
If you want to wire json-render to an existing state management library instead of the built-in store, install the adapter for your library:
+1 -26
View File
@@ -309,35 +309,10 @@ const catalog = defineCatalog(schema, {
const prompt = catalog.prompt();
// Inline mode prompt (formerly "chat")
const inlinePrompt = catalog.prompt({ mode: "inline" });
```
## Generation Modes
The generation mode values passed to `catalog.prompt()` have been renamed for clarity:
- `"generate"` is now `"standalone"`
- `"chat"` is now `"inline"`
The old names are accepted as deprecated aliases, so existing code will continue to work. Update when convenient.
**Before:**
```typescript
const prompt = catalog.prompt({ mode: "generate" });
// Chat mode prompt
const chatPrompt = catalog.prompt({ mode: "chat" });
```
**After:**
```typescript
const prompt = catalog.prompt({ mode: "standalone" });
const inlinePrompt = catalog.prompt({ mode: "inline" });
```
The default mode (when no `mode` option is provided) is `"standalone"`, which behaves identically to the previous `"generate"` default.
## Validation
`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.
+1 -1
View File
@@ -91,7 +91,7 @@ The result is a native UI built from your own components — not an iframe, not
- **[Streaming](/docs/streaming)** — Render progressively as the AI responds. Each JSONL patch adds to the spec and the UI updates in real time.
- **[Data Binding](/docs/data-binding)** — Bind props to runtime data with `$state` paths, repeat elements over arrays, and wire two-way input bindings.
- **[Visibility](/docs/visibility)** — Show or hide elements based on state conditions. The AI can generate conditional UIs without writing logic.
- **[Generation Modes](/docs/generation-modes)** — Standalone mode for full-page generated UI or inline mode for UI embedded in a conversation.
- **[Generation Modes](/docs/generation-modes)** — Generate standalone UI (playground/builder) or inline UI within a chat conversation.
## Next
@@ -9,7 +9,6 @@ What a registry contains depends on the schema you use. Each package defines its
- **`@json-render/react`** — Components (React elements) and action handlers
- **`@json-render/react-native`** — Components (React Native elements) and action handlers
- **`@json-render/react-email`** — Email components (React Email / HTML)
- **`@json-render/remotion`** — Clip components, transitions, and effects
## @json-render/react
@@ -279,31 +278,6 @@ export const { registry } = defineRegistry(catalog, {
See the [@json-render/react-native API reference](/docs/api/react-native) for the full API.
## @json-render/react-email
`@json-render/react-email` uses `defineRegistry` like React and React Native. Components render to React Email primitives (`@react-email/components`). Use `renderToHtml` or `renderToPlainText` for server-side email output:
```tsx
import { defineRegistry } from '@json-render/react-email';
import { renderToHtml } from '@json-render/react-email';
import { Body, Container, Heading, Text } from '@react-email/components';
export const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<Container style={{ padding: 16, backgroundColor: '#fff' }}>
<Heading>{props.title}</Heading>
{children}
</Container>
),
},
});
const html = await renderToHtml(spec, { registry });
```
See the [@json-render/react-email API reference](/docs/api/react-email) for the full API.
## @json-render/remotion
`@json-render/remotion` takes a different approach. Instead of `defineRegistry`, it uses a plain component registry with built-in standard components for video production:
-268
View File
@@ -1,268 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/renderers");
# Renderers
json-render supports multiple output targets. Each renderer takes the same core concept -- a JSON spec constrained to a catalog -- and renders it natively on a different platform or into a different format.
All renderers share the same workflow:
1. Define a catalog with `defineCatalog`
2. AI generates a JSON spec
3. The renderer turns the spec into platform-native output
<table>
<thead>
<tr>
<th>Renderer</th>
<th>Package</th>
<th>Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>React</td>
<td>
<code>@json-render/react</code>
</td>
<td>React component tree</td>
</tr>
<tr>
<td>Vue</td>
<td>
<code>@json-render/vue</code>
</td>
<td>Vue 3 component tree</td>
</tr>
<tr>
<td>Svelte</td>
<td>
<code>@json-render/svelte</code>
</td>
<td>Svelte 5 component tree</td>
</tr>
<tr>
<td>Solid</td>
<td>
<code>@json-render/solid</code>
</td>
<td>SolidJS component tree</td>
</tr>
<tr>
<td>shadcn/ui</td>
<td>
<code>@json-render/shadcn</code>
</td>
<td>Pre-built Radix UI + Tailwind components (uses React renderer)</td>
</tr>
<tr>
<td>React Native</td>
<td>
<code>@json-render/react-native</code>
</td>
<td>Native mobile views</td>
</tr>
<tr>
<td>Image</td>
<td>
<code>@json-render/image</code>
</td>
<td>SVG / PNG (via Satori)</td>
</tr>
<tr>
<td>React PDF</td>
<td>
<code>@json-render/react-pdf</code>
</td>
<td>PDF documents</td>
</tr>
<tr>
<td>Remotion</td>
<td>
<code>@json-render/remotion</code>
</td>
<td>Video compositions</td>
</tr>
</tbody>
</table>
## React
Render specs as React component trees in the browser. Supports data binding, streaming, actions, validation, visibility, and computed values.
```tsx
import { defineRegistry, Renderer } from "@json-render/react";
import { schema } from "@json-render/react/schema";
const { registry } = defineRegistry(catalog, { components });
<Renderer spec={spec} registry={registry} />;
```
Use `StateProvider`, `VisibilityProvider`, and `ActionProvider` for full interactivity. See the [@json-render/react API reference](/docs/api/react) for details.
## Vue
Vue 3 renderer with full feature parity with React: data binding, visibility, actions, validation, repeat scopes, and streaming.
```typescript
import { defineRegistry, Renderer } from "@json-render/vue";
import { schema } from "@json-render/vue/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) =>
h("div", { class: "card" }, [h("h3", null, props.title), children]),
},
});
```
Uses composables (`useStateStore`, `useStateBinding`, `useActions`, etc.) instead of React hooks. See the [@json-render/vue API reference](/docs/api/vue) for details.
## Svelte
Svelte 5 renderer with runes-compatible context helpers, visibility conditions, actions, and streaming support.
```typescript
import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => /* Svelte snippet */,
},
});
```
See the [@json-render/svelte API reference](/docs/api/svelte) for details.
## Solid
SolidJS renderer with fine-grained reactivity, state bindings, validation, visibility, and event-driven actions.
```tsx
import { defineRegistry, Renderer } from "@json-render/solid";
import { schema } from "@json-render/solid/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
},
});
<Renderer spec={spec} registry={registry} />;
```
See the [@json-render/solid API reference](/docs/api/solid) for details.
## shadcn/ui
36 pre-built components using Radix UI and Tailwind CSS. Built on top of `@json-render/react` -- no custom renderer needed.
```tsx
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { defineRegistry, Renderer } from "@json-render/react";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { shadcnComponents } from "@json-render/shadcn";
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Button: shadcnComponentDefinitions.Button,
},
});
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Button: shadcnComponents.Button,
},
});
```
See the [@json-render/shadcn API reference](/docs/api/shadcn) for the full component list.
## React Native
Render specs as native mobile views. Includes 25+ standard components and standard action definitions.
```tsx
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react-native/schema";
import {
standardComponentDefinitions,
standardActionDefinitions,
} from "@json-render/react-native/catalog";
import { defineRegistry, Renderer } from "@json-render/react-native";
const catalog = defineCatalog(schema, {
components: { ...standardComponentDefinitions },
actions: standardActionDefinitions,
});
const { registry } = defineRegistry(catalog, { components: {} });
<Renderer spec={spec} registry={registry} />;
```
See the [@json-render/react-native API reference](/docs/api/react-native) for details.
## Image
Generate SVG and PNG images from JSON specs using Satori. Ideal for OG images, social cards, and banners.
```typescript
import { renderToSvg, renderToPng } from "@json-render/image/render";
const svg = await renderToSvg(spec, { fonts });
const png = await renderToPng(spec, { fonts });
```
Nine standard components: Frame, Box, Row, Column, Heading, Text, Image, Divider, Spacer. PNG output requires `@resvg/resvg-js` as an optional peer dependency.
See the [@json-render/image API reference](/docs/api/image) for details.
## React PDF
Generate PDF documents from JSON specs using `@react-pdf/renderer`. Render to buffer, stream, or file.
```typescript
import {
renderToBuffer,
renderToStream,
renderToFile,
} from "@json-render/react-pdf";
const buffer = await renderToBuffer(spec);
const stream = await renderToStream(spec);
await renderToFile(spec, "./output.pdf");
```
Standard components include Document, Page, View, Row, Column, Heading, Text, Image, Table, List, Divider, Spacer, Link, and PageNumber.
See the [@json-render/react-pdf API reference](/docs/api/react-pdf) for details.
## Remotion
Turn JSON timeline specs into video compositions with Remotion.
```tsx
import { Player } from "@remotion/player";
import { Renderer } from "@json-render/remotion";
<Player
component={Renderer}
inputProps={{ spec }}
durationInFrames={spec.composition.durationInFrames}
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
/>;
```
Uses a timeline spec format with compositions, tracks, and clips. Includes standard components (TitleCard, TypingText, ImageSlide, etc.), transitions (fade, slide, zoom, wipe), and effects.
See the [@json-render/remotion API reference](/docs/api/remotion) for details.
## Custom Renderers
You can build your own renderer for any output target. See the [Custom Schema & Renderer](/docs/custom-schema) guide for how to define a custom schema and wire it to your own rendering logic.
-123
View File
@@ -1,123 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/skills");
# Skills
json-render ships with skills that teach AI coding agents how to use each package. Install a skill and your agent in Cursor, Claude Code, or Codex can generate json-render UIs without manual guidance.
## Available Skills
- **core** — Core schemas, catalogs, and AI prompt generation.
- **react** — React renderer that turns JSON specs into React component trees.
- **react-pdf** — PDF renderer using `@react-pdf/renderer`.
- **react-email** — Email renderer that produces HTML or plain-text emails.
- **react-native** — React Native renderer for native mobile UIs.
- **shadcn** — Pre-built shadcn/ui components (Radix UI + Tailwind).
- **image** — Image renderer that turns JSON specs into SVG and PNG via Satori.
- **remotion** — Remotion renderer for video generation from JSON timeline specs.
- **vue** — Vue 3 renderer for Vue component trees.
- **svelte** — Svelte 5 renderer for Svelte component trees.
- **solid** — SolidJS renderer for fine-grained reactive component trees.
- **codegen** — Code generation utilities for building custom exporters.
- **mcp** — MCP Apps integration for Claude, ChatGPT, Cursor, and VS Code.
- **redux** — Redux adapter for json-render's `StateStore` interface.
- **zustand** — Zustand adapter for json-render's `StateStore` interface.
- **jotai** — Jotai adapter for json-render's `StateStore` interface.
- **xstate** — XState Store adapter for json-render's `StateStore` interface.
## Installation
```bash
npx skills add vercel-labs/json-render --skill core
npx skills add vercel-labs/json-render --skill react
npx skills add vercel-labs/json-render --skill react-pdf
npx skills add vercel-labs/json-render --skill react-email
npx skills add vercel-labs/json-render --skill react-native
npx skills add vercel-labs/json-render --skill shadcn
npx skills add vercel-labs/json-render --skill image
npx skills add vercel-labs/json-render --skill remotion
npx skills add vercel-labs/json-render --skill vue
npx skills add vercel-labs/json-render --skill svelte
npx skills add vercel-labs/json-render --skill solid
npx skills add vercel-labs/json-render --skill codegen
npx skills add vercel-labs/json-render --skill mcp
npx skills add vercel-labs/json-render --skill redux
npx skills add vercel-labs/json-render --skill zustand
npx skills add vercel-labs/json-render --skill jotai
npx skills add vercel-labs/json-render --skill xstate
```
After installing, your AI agent will automatically activate the right skill when it encounters a matching request.
## core
The foundational skill. Teaches agents how to define catalogs, create schemas, build specs, and generate AI prompts. This is the starting point for any json-render project and covers `defineCatalog`, `defineSchema`, `specSchema`, `toPrompt`, and the full spec format.
## react
Teaches agents how to render JSON specs as React component trees using `JsonRender`, `JsonRenderClient`, and `useJsonRender`. Covers custom component registries, client-side interactivity, state management, and streaming integration.
## react-pdf
Teaches agents how to generate PDFs from JSON specs using `@react-pdf/renderer`. Covers the PDF-specific component registry, page layout, and styling.
## react-email
Teaches agents how to render JSON specs as HTML or plain-text emails using React Email components. Covers the email-specific registry and rendering pipeline.
## react-native
Teaches agents how to render JSON specs as native mobile UIs with React Native. Covers the native component registry and platform-specific considerations.
## shadcn
Teaches agents how to use the pre-built shadcn/ui component registry with json-render. Includes Radix UI primitives, Tailwind styling, and the full set of available shadcn components.
## image
Teaches agents how to turn JSON specs into SVG and PNG images using Satori. Covers the image-specific registry, dimensions, fonts, and rendering options.
## remotion
Teaches agents how to generate videos from JSON timeline specs using Remotion. Covers compositions, sequences, timeline structure, and video rendering.
## vue
Teaches agents how to render JSON specs as Vue 3 component trees. Covers the Vue renderer API, custom component registries, and reactivity integration.
## svelte
Teaches agents how to render JSON specs as Svelte 5 component trees. Covers the Svelte renderer API and component registration.
## solid
Teaches agents how to render JSON specs as SolidJS component trees. Covers Solid-specific reactive patterns, provider wiring, bindings, actions, and streaming.
## codegen
Teaches agents how to use code generation utilities to export UI specs as framework-specific source code. Covers the codegen pipeline and custom exporter creation.
## mcp
Teaches agents how to build MCP Apps that serve json-render UIs inside AI tools like Claude, ChatGPT, Cursor, and VS Code. Covers MCP server setup, tool definitions, and UI streaming.
## redux
Teaches agents how to connect a Redux store to json-render's `StateStore` interface for state-driven UIs.
## zustand
Teaches agents how to connect a Zustand store to json-render's `StateStore` interface for lightweight state management.
## jotai
Teaches agents how to connect Jotai atoms to json-render's `StateStore` interface for atomic state management.
## xstate
Teaches agents how to connect an XState Store to json-render's `StateStore` interface for state-machine-driven UIs.
## Source
All skill files are in the [`skills/`](https://github.com/vercel-labs/json-render/tree/main/skills) directory of the repository.
+1 -2
View File
@@ -16,8 +16,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
GitHub repository: https://github.com/vercel-labs/json-render
Documentation: https://json-render.dev/docs
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
npm packages: @json-render/core, @json-render/react, @json-render/remotion, @json-render/codegen
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
+76 -233
View File
@@ -1,257 +1,100 @@
"use client";
function ScheduleItem({
time,
label,
color,
}: {
time: string;
label: string;
color: string;
}) {
function Skeleton({ className = "" }: { className?: string }) {
return <div className={`rounded bg-muted-foreground/10 ${className}`} />;
}
/** Simple form wireframe reused in both diagrams */
function FormUI() {
return (
<div className="flex items-center gap-2 py-1.5 border-t border-border/50 first:border-t-0">
<span className="text-[10px] text-muted-foreground/60 w-12 shrink-0 tabular-nums">
{time}
</span>
<span
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ background: color }}
/>
<span className="text-[11px] text-muted-foreground">{label}</span>
<div className="border border-border rounded-lg p-2.5 space-y-1.5 bg-muted/30">
{/* Title */}
<Skeleton className="h-2.5 w-14 mb-1" />
{/* Input fields */}
<Skeleton className="h-4 w-full rounded-sm" />
<Skeleton className="h-4 w-full rounded-sm" />
{/* Submit button */}
<Skeleton className="h-4 w-16 rounded-sm bg-muted-foreground/20 mt-1" />
</div>
);
}
function InlineModeDiagram() {
function ChatModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-sm font-medium text-foreground text-center mb-3">
Inline Mode
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
Chat Mode
</div>
<div className="flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-col">
<div className="flex-1 p-4 space-y-3 overflow-hidden">
{/* User message */}
<div className="flex justify-end">
<div className="bg-muted-foreground/20 rounded-2xl rounded-br px-3.5 py-2 max-w-[85%]">
<span className="text-[11px] text-foreground/80">
I have back-to-back meetings today
</span>
</div>
</div>
{/* AI response */}
<div className="space-y-2">
<p className="text-[11px] text-muted-foreground/70">
Packed day! Here&apos;s what you&apos;ve got:
</p>
<div className="bg-muted/40 border border-border rounded-xl p-3 w-[92%]">
<div className="text-[11px] font-semibold text-foreground/80 mb-2">
Today&apos;s Schedule
</div>
<ScheduleItem
time="10:00 AM"
label="Design Review"
color="#7aa2f7"
/>
<ScheduleItem
time="1:00 PM"
label="Sprint Planning"
color="#bb9af7"
/>
<ScheduleItem
time="3:30 PM"
label="Team Standup"
color="#9ece6a"
/>
<ScheduleItem
time="4:30 PM"
label="Eng All-Hands"
color="#e0af68"
/>
</div>
</div>
</div>
{/* Prompt input */}
<div className="p-2.5">
<div className="bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5">
<span className="text-[10px] text-muted-foreground/40 flex-1">
Message...
</span>
<div className="w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center">
<svg
className="w-2.5 h-2.5 text-muted-foreground/50"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
transform="rotate(-90 12 12)"
/>
</svg>
</div>
</div>
</div>
</div>
<div className="mt-3 flex flex-col items-center gap-2">
<div className="text-xs font-medium text-muted-foreground">
AI decides when UI beats text
</div>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/50">
<span>AI chatbots</span>
<span className="text-muted-foreground/30">/</span>
<span>Copilots</span>
<span className="text-muted-foreground/30">/</span>
<span>Assistants</span>
</div>
</div>
</div>
);
}
function LandingPage() {
return (
<div className="w-full h-full flex flex-col bg-muted/20">
{/* Nav */}
<div className="flex items-center justify-between px-3 py-2 border-b border-border/50">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
className="shrink-0"
>
<rect
x="2"
y="14"
width="5"
height="8"
rx="1"
className="fill-muted-foreground/60"
/>
<rect
x="9.5"
y="8"
width="5"
height="14"
rx="1"
className="fill-muted-foreground/60"
/>
<rect
x="17"
y="2"
width="5"
height="20"
rx="1"
className="fill-muted-foreground/60"
/>
</svg>
<div className="flex items-center gap-2">
<span className="text-[9px] text-muted-foreground/50">Pricing</span>
<span className="text-[9px] text-muted-foreground/50">Blog</span>
<span className="text-[8px] font-semibold bg-foreground text-background rounded-md px-1.5 py-0.5 whitespace-nowrap">
Get Started
</span>
</div>
</div>
{/* Hero */}
<div className="flex-1 flex flex-col items-center justify-center gap-2.5 text-center px-4">
<span className="text-[8px] font-medium text-green-400 bg-green-400/10 border border-green-400/15 rounded-full px-2 py-0.5">
Trusted by 2,000+ teams
</span>
<div className="text-sm font-bold text-foreground leading-tight">
Analytics that
<br />
move the needle
</div>
<div className="text-[10px] text-muted-foreground/50 leading-relaxed">
Ship what matters. No setup required.
</div>
<div className="flex gap-1.5 mt-1">
<button className="bg-foreground text-background text-[9px] font-semibold rounded-lg px-3 py-1.5 whitespace-nowrap">
Start Free
</button>
<button className="border border-border text-muted-foreground text-[9px] font-medium rounded-lg px-3 py-1.5 whitespace-nowrap">
Book Demo
</button>
</div>
</div>
{/* Footer */}
<div className="flex justify-center gap-3 py-2 border-t border-border/50">
<span className="text-[8px] text-muted-foreground/30">Privacy</span>
<span className="text-[8px] text-muted-foreground/30">Terms</span>
<span className="text-[8px] text-muted-foreground/30">Status</span>
</div>
</div>
);
}
function StandaloneModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-sm font-medium text-foreground text-center mb-3">
Standalone Mode
</div>
<div className="flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-row">
{/* Left panel - conversation */}
<div className="w-[38%] border-r border-border/50 flex flex-col">
<div className="flex-1 p-3 space-y-2.5">
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
{/* Chat area */}
<div className="flex-1 p-3 overflow-hidden flex justify-center">
<div className="w-1/3 min-w-[120px] space-y-3">
{/* User message */}
<div className="flex justify-end">
<div className="bg-muted-foreground/20 rounded-2xl rounded-br px-2.5 py-1.5">
<span className="text-[10px] text-foreground/80 block leading-relaxed">
A landing page for my analytics startup
</span>
<div className="bg-muted rounded-xl px-3 py-2">
<Skeleton className="h-2 w-14" />
</div>
</div>
{/* AI text */}
<p className="text-[10px] text-muted-foreground/60 leading-relaxed">
Here&apos;s a clean landing page with a hero, nav, and CTAs.
</p>
</div>
{/* Prompt input */}
<div className="p-2.5">
<div className="bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5">
<span className="text-[10px] text-muted-foreground/40 flex-1">
Message...
</span>
<div className="w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center">
<svg
className="w-2.5 h-2.5 text-muted-foreground/50"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
transform="rotate(-90 12 12)"
/>
</svg>
{/* Assistant text */}
<div className="space-y-2">
<div className="space-y-1.5">
<Skeleton className="h-2 w-full" />
<Skeleton className="h-2 w-3/4" />
</div>
{/* Inline UI */}
<FormUI />
{/* More text after UI */}
<div className="space-y-1.5">
<Skeleton className="h-2 w-4/5" />
</div>
</div>
</div>
</div>
{/* Right panel - landing page */}
<div className="flex-1">
<LandingPage />
{/* Input bar */}
<div className="p-2 flex justify-center">
<div className="w-1/3 min-w-[120px] flex items-center gap-2">
<Skeleton className="h-7 flex-1 rounded-md" />
<Skeleton className="h-7 w-7 rounded-md" />
</div>
</div>
</div>
<div className="mt-3 flex flex-col items-center gap-2">
<div className="text-xs font-medium text-muted-foreground">
Prompt in, UI out
<div className="text-[10px] text-muted-foreground/60 mt-2 text-center">
Text + UI interleaved in messages
</div>
</div>
);
}
function GenerateModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
Generate Mode
</div>
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
{/* Left panel - prompt */}
<div className="w-[38%] border-r border-border flex flex-col">
<div className="flex-1" />
<div className="p-3 space-y-2">
<Skeleton className="h-7 w-full rounded-md" />
<Skeleton className="h-5 w-16 rounded-md" />
</div>
</div>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/50">
<span>Website builders</span>
<span className="text-muted-foreground/30">/</span>
<span>Text-to-widget</span>
<span className="text-muted-foreground/30">/</span>
<span>Dashboards</span>
{/* Right panel - UI preview */}
<div className="flex-1 p-3 flex items-center justify-center">
<div className="w-3/4">
<FormUI />
</div>
</div>
</div>
<div className="text-[10px] text-muted-foreground/60 mt-2 text-center">
Prompt separate from UI preview
</div>
</div>
);
}
@@ -260,11 +103,11 @@ export function GenerationModesDiagram() {
return (
<div className="not-prose my-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="h-[420px]">
<InlineModeDiagram />
<div className="h-[280px]">
<ChatModeDiagram />
</div>
<div className="h-[420px]">
<StandaloneModeDiagram />
<div className="h-[280px]">
<GenerateModeDiagram />
</div>
</div>
</div>
+1 -1
View File
@@ -100,7 +100,7 @@ export function Header() {
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
<span>12k</span>
<span>11k</span>
</a>
<ThemeToggle />
</nav>
+1 -36
View File
@@ -16,7 +16,6 @@ export const docsNavigation: NavSection[] = [
{ title: "Introduction", href: "/docs" },
{ title: "Installation", href: "/docs/installation" },
{ title: "Quick Start", href: "/docs/quick-start" },
{ title: "Skills", href: "/docs/skills" },
{ title: "Migration Guide", href: "/docs/migration" },
{ title: "Changelog", href: "/docs/changelog" },
],
@@ -37,7 +36,6 @@ export const docsNavigation: NavSection[] = [
{
title: "Rendering",
items: [
{ title: "Renderers", href: "/docs/renderers" },
{ title: "Registry", href: "/docs/registry" },
{ title: "Streaming", href: "/docs/streaming" },
{ title: "Generation Modes", href: "/docs/generation-modes" },
@@ -66,41 +64,21 @@ export const docsNavigation: NavSection[] = [
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-pdf",
external: true,
},
{
title: "React Email",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-email",
external: true,
},
{
title: "Remotion",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion",
external: true,
},
{
title: "Image",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
external: true,
},
{
title: "Svelte",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/svelte",
external: true,
},
{
title: "Vue",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
external: true,
},
{
title: "Renders with Vite (Vue / React / Svelte / Solid)",
title: "Renders with Vite (Vue / React)",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
external: true,
},
{
title: "MCP App",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp",
external: true,
},
],
},
{
@@ -126,24 +104,11 @@ export const docsNavigation: NavSection[] = [
{ title: "@json-render/core", href: "/docs/api/core" },
{ title: "@json-render/react", href: "/docs/api/react" },
{ title: "@json-render/react-pdf", href: "/docs/api/react-pdf" },
{ title: "@json-render/react-email", href: "/docs/api/react-email" },
{ title: "@json-render/shadcn", href: "/docs/api/shadcn" },
{ title: "@json-render/react-native", href: "/docs/api/react-native" },
{ title: "@json-render/image", href: "/docs/api/image" },
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
{ title: "@json-render/vue", href: "/docs/api/vue" },
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
{ title: "@json-render/solid", href: "/docs/api/solid" },
{
title: "@json-render/react-three-fiber",
href: "/docs/api/react-three-fiber",
},
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
{ title: "@json-render/mcp", href: "/docs/api/mcp" },
{ title: "@json-render/redux", href: "/docs/api/redux" },
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
],
},
];
-12
View File
@@ -26,7 +26,6 @@ export const PAGE_TITLES: Record<string, string> = {
"docs/computed-values": "Computed Values",
"docs/visibility": "Visibility",
"docs/watchers": "Watchers",
"docs/renderers": "Renderers",
"docs/generation-modes": "Generation Modes",
"docs/code-export": "Code Export",
"docs/custom-schema": "Custom Schema & Renderer",
@@ -37,27 +36,16 @@ export const PAGE_TITLES: Record<string, string> = {
"docs/ag-ui": "AG-UI Integration",
"docs/migration": "Migration Guide",
"docs/changelog": "Changelog",
"docs/skills": "Skills",
// API references
"docs/api/core": "@json-render/core API",
"docs/api/react": "@json-render/react API",
"docs/api/vue": "@json-render/vue API",
"docs/api/solid": "@json-render/solid API",
"docs/api/react-pdf": "@json-render/react-pdf API",
"docs/api/react-email": "@json-render/react-email API",
"docs/api/react-native": "@json-render/react-native API",
"docs/api/svelte": "@json-render/svelte API",
"docs/api/codegen": "@json-render/codegen API",
"docs/api/image": "@json-render/image API",
"docs/api/remotion": "@json-render/remotion API",
"docs/api/shadcn": "@json-render/shadcn API",
"docs/api/mcp": "@json-render/mcp API",
"docs/api/redux": "@json-render/redux API",
"docs/api/zustand": "@json-render/zustand API",
"docs/api/jotai": "@json-render/jotai API",
"docs/api/react-three-fiber": "@json-render/react-three-fiber API",
"docs/api/xstate": "@json-render/xstate API",
};
/**
+1 -2
View File
@@ -1,11 +1,10 @@
{
"name": "web",
"version": "0.1.7",
"version": "0.1.3",
"type": "module",
"private": true,
"license": "Apache-2.0",
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless json-render next dev --turbopack",
"build": "next build",
"start": "next start",
-36
View File
@@ -1,41 +1,5 @@
# example-chat
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react@0.13.0
- @json-render/shadcn@0.13.0
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react@0.12.1
- @json-render/shadcn@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react@0.11.0
- @json-render/shadcn@0.11.0
## 0.1.3
### Patch Changes
+1 -1
View File
@@ -130,7 +130,7 @@ When the user asks for a quiz, test, or Q&A, build an interactive experience:
4. You can also add a final score section that becomes visible when all questions are submitted.
${explorerCatalog.prompt({
mode: "inline",
mode: "chat",
customRules: [
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
+1 -2
View File
@@ -1,10 +1,9 @@
{
"name": "example-chat",
"version": "0.1.7",
"version": "0.1.3",
"type": "module",
"private": true,
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless chat-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
-36
View File
@@ -1,41 +1,5 @@
# example-dashboard
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/codegen@0.13.0
- @json-render/react@0.13.0
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/codegen@0.12.1
- @json-render/react@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/codegen@0.12.0
- @json-render/react@0.12.0
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/codegen@0.11.0
- @json-render/react@0.11.0
## 0.1.3
### Patch Changes
+1 -2
View File
@@ -1,10 +1,9 @@
{
"name": "example-dashboard",
"version": "0.1.7",
"version": "0.1.3",
"type": "module",
"private": true,
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless dashboard-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
-33
View File
@@ -1,33 +0,0 @@
# example-image
## 0.1.4
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/image@0.13.0
## 0.1.3
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/image@0.12.1
## 0.1.2
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/image@0.12.0
## 0.1.1
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/image@0.11.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-image",
"version": "0.1.4",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
-31
View File
@@ -1,31 +0,0 @@
# example-mcp
## 0.1.3
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/mcp@0.13.0
- @json-render/react@0.13.0
- @json-render/shadcn@0.13.0
## 0.1.2
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/mcp@0.12.1
- @json-render/react@0.12.1
- @json-render/shadcn@0.12.1
## 0.1.1
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/mcp@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
-63
View File
@@ -1,63 +0,0 @@
# MCP App Example
A json-render MCP App that serves interactive shadcn/ui-based UIs directly inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
## Setup
```bash
pnpm install
pnpm build
```
## Usage
### With Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
}
}
}
```
### With Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/examples/mcp/server.ts", "--stdio"]
}
}
}
```
### HTTP Transport
```bash
pnpm start
# Server listens on http://localhost:3001/mcp
```
### Stdio Transport
```bash
pnpm start:stdio
```
## How It Works
1. The Vite build bundles the React app (with shadcn components and `useJsonRenderApp` hook) into a single self-contained HTML file
2. The MCP server registers a `render-ui` tool with the catalog prompt as its description
3. When the LLM calls the tool, it generates a json-render spec constrained to the catalog
4. The host renders the bundled HTML in a sandboxed iframe
5. The iframe receives the spec via the MCP Apps protocol and renders it with json-render
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
<title>json-render MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-34
View File
@@ -1,34 +0,0 @@
{
"name": "example-mcp",
"version": "0.1.3",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"start": "tsx server.ts",
"start:stdio": "tsx server.ts --stdio",
"dev": "tsx watch server.ts"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/mcp": "workspace:*",
"@json-render/react": "workspace:*",
"@json-render/shadcn": "workspace:*",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"@tailwindcss/vite": "^4.2.1",
"@vitejs/plugin-react": "^5.1.4",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "^5.7.2",
"vite": "^6.3.5",
"vite-plugin-singlefile": "^2.3.0"
}
}
-86
View File
@@ -1,86 +0,0 @@
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { catalog } from "./src/catalog.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadHtml(): string {
const htmlPath = path.join(__dirname, "dist", "index.html");
if (!fs.existsSync(htmlPath)) {
throw new Error(
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
);
}
return fs.readFileSync(htmlPath, "utf-8");
}
async function startStdio() {
const html = loadHtml();
const server = await createMcpApp({
name: "json-render Example",
version: "1.0.0",
catalog,
html,
});
await server.connect(new StdioServerTransport());
}
async function startHttp() {
const html = loadHtml();
const port = parseInt(process.env.PORT ?? "3001", 10);
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
expressApp.all("/mcp", async (req, res) => {
const server = await createMcpApp({
name: "json-render Example",
version: "1.0.0",
catalog,
html,
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
expressApp.listen(port, () => {
console.log(`MCP server listening on http://localhost:${port}/mcp`);
});
}
if (process.argv.includes("--stdio")) {
startStdio().catch((e) => {
console.error(e);
process.exit(1);
});
} else {
startHttp().catch((e) => {
console.error(e);
process.exit(1);
});
}
-10
View File
@@ -1,10 +0,0 @@
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
export const catalog = defineCatalog(schema, {
components: {
...shadcnComponentDefinitions,
},
actions: {},
});
-106
View File
@@ -1,106 +0,0 @@
@import "tailwindcss";
@source "../../../packages/shadcn/src";
@source "../../../packages/react/src";
@custom-variant dark (&:is([data-theme="dark"] *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
[data-theme="dark"] {
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--popover: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--popover-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
}
* {
box-sizing: border-box;
border-color: var(--color-border);
}
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply text-foreground;
background-color: var(--color-background) !important;
}
}
button {
cursor: pointer;
}
-269
View File
@@ -1,269 +0,0 @@
import "./globals.css";
import {
Component,
useState,
useEffect,
useCallback,
type ReactNode,
} from "react";
import { createRoot } from "react-dom/client";
import { App as McpApp } from "@modelcontextprotocol/ext-apps";
import { JSONUIProvider, Renderer, defineRegistry } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
import type { Spec } from "@json-render/core";
import { catalog } from "./catalog";
const { registry } = defineRegistry(catalog, {
components: {
...shadcnComponents,
Avatar: ({ props }: { props: Record<string, unknown> }) => {
const name = (props.name as string) || (props.alt as string) || "?";
const src = props.src as string | undefined;
const initials = name
.split(" ")
.map((n) => n[0])
.join("")
.slice(0, 2)
.toUpperCase();
const size = props.size === "lg" ? 48 : props.size === "sm" ? 32 : 40;
const [imgFailed, setImgFailed] = useState(false);
const onError = useCallback(() => setImgFailed(true), []);
const showImg = src && !imgFailed;
return (
<div
style={{
width: size,
height: size,
borderRadius: "50%",
overflow: "hidden",
backgroundColor: "var(--color-background-tertiary, #27272a)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
{showImg ? (
<img
src={src}
alt={name}
referrerPolicy="no-referrer"
crossOrigin="anonymous"
width={size}
height={size}
onError={onError}
style={{ objectFit: "cover", width: size, height: size }}
/>
) : (
<span
style={{
fontSize: size * 0.4,
color: "var(--color-text-secondary, #a1a1aa)",
}}
>
{initials}
</span>
)}
</div>
);
},
},
});
class ErrorBoundary extends Component<
{ children: ReactNode },
{ error: Error | null }
> {
state = { error: null as Error | null };
static getDerivedStateFromError(error: Error) {
return { error };
}
render() {
if (this.state.error) {
return (
<div
style={{
padding: 16,
color: "#dc2626",
fontFamily: "monospace",
fontSize: 13,
}}
>
Error: {this.state.error.message}
</div>
);
}
return this.props.children;
}
}
function forceFullWidth(spec: Spec): Spec {
if (!spec.elements) return spec;
const elements = { ...spec.elements };
for (const [key, el] of Object.entries(elements)) {
if (el.type === "Card" && el.props) {
elements[key] = {
...el,
props: { ...el.props, maxWidth: "full", centered: false },
};
}
}
return { ...spec, elements };
}
function parseSpec(
result: { content?: Array<{ type: string; text?: string }> } | undefined,
): Spec | null {
const text = result?.content?.find((c) => c.type === "text")?.text;
if (!text) return null;
try {
return forceFullWidth(JSON.parse(text) as Spec);
} catch {
return null;
}
}
function applyHostContext(ctx: {
theme?: string;
styles?: { variables?: Record<string, string> };
}) {
if (ctx.theme) {
document.documentElement.setAttribute("data-theme", ctx.theme);
document.documentElement.style.colorScheme = ctx.theme;
}
if (ctx.styles?.variables) {
const root = document.documentElement;
for (const [key, value] of Object.entries(ctx.styles.variables)) {
root.style.setProperty(key, value);
}
}
}
function McpAppView() {
const [spec, setSpec] = useState<Spec | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let specReceived = false;
function handleSpec(s: Spec) {
if (specReceived) return;
specReceived = true;
setSpec(s);
}
function onMessage(event: MessageEvent) {
if (specReceived) return;
const data = event.data as Record<string, unknown> | undefined;
if (!data || typeof data !== "object") return;
const method = data.method as string | undefined;
const params = data.params as Record<string, unknown> | undefined;
// Cursor sends the spec via tool-input
if (method === "ui/notifications/tool-input" && params?.arguments) {
const args = params.arguments as Record<string, unknown>;
const rawSpec = args.spec;
if (
rawSpec &&
typeof rawSpec === "object" &&
"root" in rawSpec &&
"elements" in rawSpec
) {
handleSpec(forceFullWidth(rawSpec as Spec));
}
}
}
window.addEventListener("message", onMessage);
const app = new McpApp({ name: "json-render", version: "1.0.0" });
app.ontoolresult = (result) => {
const parsed = parseSpec(
result as { content?: Array<{ type: string; text?: string }> },
);
if (parsed) handleSpec(parsed);
};
app.onhostcontextchanged = (ctx) =>
applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);
app.onerror = (err: unknown) => {
setError(err instanceof Error ? err.message : String(err));
};
app
.connect()
.then(() => {
const ctx = app.getHostContext?.();
if (ctx)
applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);
// Fallback: if host didn't provide theme, detect via media query
if (!ctx || !(ctx as Record<string, unknown>).theme) {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
document.documentElement.setAttribute(
"data-theme",
prefersDark ? "dark" : "light",
);
document.documentElement.style.colorScheme = prefersDark
? "dark"
: "light";
}
})
.catch((err: unknown) =>
setError(err instanceof Error ? err.message : String(err)),
);
return () => {
window.removeEventListener("message", onMessage);
app.close().catch(() => {});
};
}, []);
if (error) {
return (
<div
style={{
padding: 16,
color: "#dc2626",
fontFamily: "monospace",
fontSize: 13,
}}
>
{error}
</div>
);
}
if (!spec) {
return (
<div
style={{
padding: 16,
color: "#6b7280",
fontFamily: "sans-serif",
fontSize: 14,
}}
>
Loading...
</div>
);
}
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<div className="w-full">
<Renderer spec={spec} registry={registry} />
</div>
</JSONUIProvider>
);
}
createRoot(document.getElementById("root")!).render(
<ErrorBoundary>
<McpAppView />
</ErrorBoundary>,
);
-89
View File
@@ -1,89 +0,0 @@
import { JSONUIProvider, Renderer } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
import { defineRegistry } from "@json-render/react";
import { useJsonRenderApp } from "@json-render/mcp/app";
import { catalog } from "./catalog";
import { useState, useEffect } from "react";
const { registry } = defineRegistry(catalog, {
components: {
...shadcnComponents,
},
});
const debugStyle = {
padding: 12,
fontFamily: "monospace",
fontSize: 12,
color: "#000",
background: "#fffbe6",
border: "1px solid #faad14",
borderRadius: 4,
margin: 8,
whiteSpace: "pre-wrap" as const,
wordBreak: "break-all" as const,
};
export function McpAppView() {
const [logs, setLogs] = useState<string[]>(["App mounted"]);
const addLog = (msg: string) => {
setLogs((prev) => [
...prev,
`${new Date().toISOString().slice(11, 23)} ${msg}`,
]);
};
const { spec, loading, connected, connecting, error } = useJsonRenderApp({
name: "json-render-mcp-example",
version: "1.0.0",
});
useEffect(() => {
addLog(
`State: connecting=${connecting} connected=${connected} error=${error?.message ?? "none"} loading=${loading} spec=${spec ? "yes" : "null"}`,
);
}, [connecting, connected, error, loading, spec]);
useEffect(() => {
if (spec) {
addLog(
`Spec received: root=${spec.root}, elements=${Object.keys(spec.elements ?? {}).join(",")}`,
);
}
}, [spec]);
if (error) {
return (
<div style={debugStyle}>
<div style={{ color: "#ef4444", fontWeight: "bold" }}>
Connection error: {error.message}
</div>
<div style={{ marginTop: 8 }}>{logs.join("\n")}</div>
</div>
);
}
if (!spec) {
return (
<div style={debugStyle}>
<div>
{connecting
? "Connecting to host..."
: loading
? "Waiting for UI spec..."
: "No spec received."}
</div>
<div style={{ marginTop: 8 }}>{logs.join("\n")}</div>
</div>
);
}
return (
<div>
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
</div>
);
}
-17
View File
@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react-jsx"
},
"include": ["src", "server.ts"]
}
-18
View File
@@ -1,18 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss(), viteSingleFile()],
resolve: {
dedupe: ["react", "react-dom"],
},
build: {
outDir: "dist",
emptyOutDir: false,
rollupOptions: {
input: "index.html",
},
},
});
-36
View File
@@ -1,41 +1,5 @@
# example-no-ai
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react@0.13.0
- @json-render/shadcn@0.13.0
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react@0.12.1
- @json-render/shadcn@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react@0.11.0
- @json-render/shadcn@0.11.0
## 0.1.3
### Patch Changes
+1 -2
View File
@@ -1,10 +1,9 @@
{
"name": "example-no-ai",
"version": "0.1.7",
"version": "0.1.3",
"type": "module",
"private": true,
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless no-ai-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
-9
View File
@@ -1,9 +0,0 @@
# Vercel AI Gateway
# Automatically authenticated when deployed on Vercel
# For local development, get your key from https://vercel.com/ai-gateway
AI_GATEWAY_API_KEY=
# AI Model Configuration
# Override the default model used for email generation
# Default: anthropic/claude-haiku-4.5
AI_GATEWAY_MODEL=anthropic/claude-haiku-4.5
-25
View File
@@ -1,25 +0,0 @@
# example-react-email
## 0.1.3
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react-email@0.13.0
## 0.1.2
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react-email@0.12.1
## 0.1.1
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-email@0.12.0
@@ -1,47 +0,0 @@
import {
renderToHtml,
renderToPlainText,
} from "@json-render/react-email/render";
import { examples } from "@/lib/examples";
import type { Spec } from "@json-render/core";
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const name = searchParams.get("name") ?? "vercel-invite";
const plain = searchParams.get("plain") === "1";
const example = examples.find((e) => e.name === name);
if (!example) {
return new Response("Example not found", { status: 404 });
}
return emailResponse(example.spec, plain);
}
export async function POST(req: Request) {
const { spec, plain } = (await req.json()) as {
spec: Spec;
plain?: boolean;
};
if (!spec || !spec.root || !spec.elements) {
return new Response("Invalid spec", { status: 400 });
}
return emailResponse(spec, plain ?? false);
}
async function emailResponse(spec: Spec, plain: boolean) {
const content = plain
? await renderToPlainText(spec)
: await renderToHtml(spec);
return new Response(content, {
headers: {
"Content-Type": plain
? "text/plain; charset=utf-8"
: "text/html; charset=utf-8",
"Cache-Control": "no-store",
},
});
}
@@ -1,34 +0,0 @@
import { streamText } from "ai";
import { buildUserPrompt, type Spec } from "@json-render/core";
import { emailCatalog } from "@/lib/catalog";
export const maxDuration = 60;
const SYSTEM_PROMPT = emailCatalog.prompt();
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
export async function POST(req: Request) {
const { prompt, startingSpec } = (await req.json()) as {
prompt: string;
startingSpec?: Spec | null;
};
if (!prompt || typeof prompt !== "string") {
return Response.json({ error: "prompt is required" }, { status: 400 });
}
const userPrompt = buildUserPrompt({
prompt,
currentSpec: startingSpec,
});
const result = streamText({
model: process.env.AI_GATEWAY_MODEL ?? DEFAULT_MODEL,
system: SYSTEM_PROMPT,
prompt: userPrompt,
temperature: 0.7,
});
return result.toTextStreamResponse();
}
-130
View File
@@ -1,130 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
-23
View File
@@ -1,23 +0,0 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "json-render React Email Example",
description:
"Generate HTML emails from JSON specs with @json-render/react-email",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
-608
View File
@@ -1,608 +0,0 @@
"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import { examples } from "@/lib/examples";
import { createSpecStreamCompiler } from "@json-render/core";
import type { Spec } from "@json-render/core";
import { cn } from "@/lib/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { FileText, Download, Loader2, ArrowRight, Square } from "lucide-react";
type Mode = "scratch" | "example";
type MobileView = "json" | "preview";
interface Selection {
mode: Mode;
exampleName?: string;
}
const HTML_REFRESH_INTERVAL_MS = 2000;
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
return (
<button
onClick={() => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
className="text-xs text-muted-foreground hover:text-foreground transition-colors font-mono"
>
{copied ? "copied" : "copy"}
</button>
);
}
function isRenderableSpec(spec: Spec | null): spec is Spec {
if (!spec?.root || !spec.elements) return false;
const root = spec.elements[spec.root];
if (!root) return false;
if (root.type !== "Html" || !root.children?.length) return false;
const childTypes = root.children.map((id) => spec.elements[id]?.type);
return childTypes.includes("Head") || childTypes.includes("Body");
}
export default function Page() {
const [selection, setSelection] = useState<Selection>({
mode: "example",
exampleName: examples[0]!.name,
});
const [prompt, setPrompt] = useState("");
const [generating, setGenerating] = useState(false);
const [generatedSpec, setGeneratedSpec] = useState<Spec | null>(null);
const [htmlContent, setHtmlContent] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [mobileView, setMobileView] = useState<MobileView>("preview");
const [examplesSheetOpen, setExamplesSheetOpen] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const inputRef = useRef<HTMLTextAreaElement>(null);
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
const abortRef = useRef<AbortController | null>(null);
const codeScrollRef = useRef<HTMLDivElement>(null);
const mobileCodeScrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!generating) return;
codeScrollRef.current?.scrollTo({
top: codeScrollRef.current.scrollHeight,
});
mobileCodeScrollRef.current?.scrollTo({
top: mobileCodeScrollRef.current.scrollHeight,
});
}, [generating, generatedSpec]);
const currentExample =
selection.mode === "example"
? examples.find((e) => e.name === selection.exampleName)
: null;
const activeSpec = generatedSpec ?? currentExample?.spec ?? null;
useEffect(() => {
inputRef.current?.focus();
}, [selection.mode, selection.exampleName]);
const fetchHtml = useCallback(async (spec: Spec, signal?: AbortSignal) => {
const res = await fetch("/api/email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ spec }),
signal,
});
if (!res.ok) throw new Error("Failed to render email");
const html = await res.text();
setHtmlContent(html);
}, []);
// Fetch HTML for example specs on selection change
useEffect(() => {
if (selection.mode !== "example" || !currentExample || generatedSpec)
return;
fetchHtml(currentExample.spec).catch(() => {});
}, [selection.mode, currentExample, generatedSpec, fetchHtml]);
// Progressive HTML refresh during generation
const lastRefreshSpec = useRef<string>("");
const generatedSpecRef = useRef<Spec | null>(null);
generatedSpecRef.current = generatedSpec;
useEffect(() => {
if (!generating) return;
const interval = setInterval(() => {
const spec = generatedSpecRef.current;
if (!spec) return;
const specKey = JSON.stringify(spec);
if (specKey === lastRefreshSpec.current) return;
if (!isRenderableSpec(spec)) return;
lastRefreshSpec.current = specKey;
setRefreshing(true);
fetchHtml(spec)
.catch(() => {})
.finally(() => setRefreshing(false));
}, HTML_REFRESH_INTERVAL_MS);
return () => clearInterval(interval);
}, [generating, fetchHtml]);
const handleGenerate = useCallback(async () => {
if (!prompt.trim()) return;
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setGenerating(true);
setError(null);
lastRefreshSpec.current = "";
try {
const startingSpec =
selection.mode === "example" && currentExample
? currentExample.spec
: null;
const res = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: prompt.trim(), startingSpec }),
signal: controller.signal,
});
if (!res.ok) throw new Error("Generation failed");
const reader = res.body?.getReader();
if (!reader) throw new Error("No response body");
const decoder = new TextDecoder();
const compiler = createSpecStreamCompiler<Spec>(
startingSpec ? { ...startingSpec } : {},
);
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const { result, newPatches } = compiler.push(chunk);
if (newPatches.length > 0) setGeneratedSpec(result);
}
const finalSpec = compiler.getResult();
setGeneratedSpec(finalSpec);
setGenerating(false);
await fetchHtml(finalSpec);
} catch (e) {
if (controller.signal.aborted) return;
setError(e instanceof Error ? e.message : "Something went wrong");
setGenerating(false);
}
}, [prompt, selection, currentExample, fetchHtml]);
const handleStop = useCallback(() => {
abortRef.current?.abort();
setGenerating(false);
if (isRenderableSpec(generatedSpec)) {
fetchHtml(generatedSpec).catch(() => {});
}
}, [generatedSpec, fetchHtml]);
const select = (next: Selection) => {
abortRef.current?.abort();
setSelection(next);
setGeneratedSpec(null);
setHtmlContent(null);
setError(null);
setPrompt("");
setGenerating(false);
setExamplesSheetOpen(false);
};
const handleDownload = async () => {
if (!activeSpec) return;
const spec = generatedSpec ?? currentExample?.spec;
if (!spec) return;
const res = await fetch("/api/email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ spec }),
});
if (!res.ok) return;
const html = await res.text();
const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "email.html";
a.click();
URL.revokeObjectURL(url);
};
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleGenerate();
}
},
[handleGenerate],
);
const jsonCode = activeSpec
? JSON.stringify(activeSpec, null, 2)
: "// select an example or generate an email";
// ---------------------------------------------------------------------------
// Pane: Chat / Examples
// ---------------------------------------------------------------------------
const chatPane = (
<div className="h-full flex flex-col">
<div className="border-b border-border px-3 h-9 flex items-center gap-2">
<FileText className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-mono text-muted-foreground">
json-render / react-email
</span>
</div>
<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
<p className="px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground">
start
</p>
<button
onClick={() => select({ mode: "scratch" })}
className={cn(
"w-full text-left px-3 py-2 rounded text-sm transition-colors",
selection.mode === "scratch"
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
>
<span className="font-medium">From scratch</span>
</button>
<p className="px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground">
examples
</p>
{examples.map((ex) => (
<button
key={ex.name}
onClick={() => select({ mode: "example", exampleName: ex.name })}
className={cn(
"w-full text-left px-3 py-2 rounded text-sm transition-colors",
selection.mode === "example" &&
selection.exampleName === ex.name
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
>
<span className="font-medium">{ex.label}</span>
<p className="text-xs text-muted-foreground/70 mt-0.5 leading-snug">
{ex.description}
</p>
</button>
))}
</div>
</ScrollArea>
<div
className="border-t border-border p-3 cursor-text"
onMouseDown={(e) => {
const target = e.target as HTMLElement;
if (!target.closest("button") && target.tagName !== "TEXTAREA") {
e.preventDefault();
inputRef.current?.focus();
}
}}
>
{error && (
<div className="mb-2 rounded bg-destructive/10 px-3 py-1.5 text-xs text-destructive">
{error}
</div>
)}
<textarea
ref={inputRef}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
selection.mode === "scratch"
? "Describe the email you want..."
: `Modify the ${currentExample?.label ?? "example"}...`
}
className="w-full bg-background text-sm resize-none outline-none placeholder:text-muted-foreground/50"
rows={2}
autoFocus
/>
<div className="flex justify-between items-center mt-2">
<span className="text-[11px] text-muted-foreground">
{selection.mode === "example" && currentExample
? currentExample.label
: "scratch"}
</span>
{generating ? (
<button
onClick={handleStop}
className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors"
aria-label="Stop"
>
<Square className="h-3 w-3" fill="currentColor" />
</button>
) : (
<button
onClick={handleGenerate}
disabled={!prompt.trim()}
className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30"
aria-label="Generate"
>
<ArrowRight className="h-3.5 w-3.5" />
</button>
)}
</div>
</div>
</div>
);
// ---------------------------------------------------------------------------
// Pane: JSON Spec
// ---------------------------------------------------------------------------
const codePane = (
<div className="h-full flex flex-col">
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
<span className="text-xs font-mono text-foreground">json</span>
{generating && (
<Loader2 className="h-3 w-3 text-muted-foreground animate-spin" />
)}
<div className="flex-1" />
{activeSpec && <CopyButton text={jsonCode} />}
</div>
<div ref={codeScrollRef} className="flex-1 overflow-auto">
<pre className="p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre">
{jsonCode}
</pre>
</div>
</div>
);
// ---------------------------------------------------------------------------
// Pane: Email Preview
// ---------------------------------------------------------------------------
const previewPane = (
<div className="h-full flex flex-col">
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
<span className="text-xs font-mono text-foreground">preview</span>
{(generating || refreshing) && (
<Loader2 className="h-3 w-3 text-muted-foreground animate-spin" />
)}
<div className="flex-1" />
{activeSpec && (
<button
onClick={handleDownload}
className="text-xs text-muted-foreground hover:text-foreground transition-colors font-mono flex items-center gap-1"
>
<Download className="h-3 w-3" />
download
</button>
)}
</div>
<div className="flex-1 relative bg-neutral-600">
{htmlContent ? (
<div className="h-full flex justify-center p-5 overflow-auto">
<iframe
srcDoc={htmlContent}
className="w-[620px] h-full border-none bg-white shadow-sm"
title="Email preview"
/>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center gap-2 text-neutral-400">
<FileText className="h-10 w-10" />
<p className="text-sm">
{selection.mode === "scratch"
? "Enter a prompt to generate an email"
: "Select an example to preview"}
</p>
</div>
)}
</div>
</div>
);
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
return (
<div className="h-dvh flex flex-col">
{/* Desktop: 3-pane resizable layout */}
<div className="hidden lg:flex flex-1 min-h-0">
<ResizablePanelGroup className="flex-1">
<ResizablePanel defaultSize={25} minSize={15}>
{chatPane}
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={35} minSize={20}>
{codePane}
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40} minSize={20}>
{previewPane}
</ResizablePanel>
</ResizablePanelGroup>
</div>
{/* Mobile: toolbar + content + prompt */}
<div className="flex lg:hidden flex-col flex-1 min-h-0">
<div className="border-b border-border px-3 h-9 flex items-center gap-3 shrink-0">
<button
onClick={() => setExamplesSheetOpen(true)}
className="text-xs font-mono font-medium px-1.5 py-0.5 rounded bg-muted text-foreground shrink-0"
>
{selection.mode === "example" && currentExample
? currentExample.label
: "scratch"}
</button>
{(["json", "preview"] as const).map((tab) => (
<button
key={tab}
onClick={() => setMobileView(tab)}
className={cn(
"text-xs font-mono transition-colors shrink-0",
mobileView === tab
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
{tab}
</button>
))}
{(generating || refreshing) && (
<Loader2 className="h-3 w-3 text-muted-foreground animate-spin shrink-0" />
)}
<div className="flex-1" />
{activeSpec && (
<button
onClick={handleDownload}
className="text-xs text-muted-foreground hover:text-foreground transition-colors font-mono flex items-center gap-1"
>
<Download className="h-3 w-3" />
</button>
)}
</div>
<div ref={mobileCodeScrollRef} className="flex-1 min-h-0 overflow-auto">
{mobileView === "json" ? (
<pre className="p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre">
{jsonCode}
</pre>
) : (
<div className="h-full relative bg-neutral-600">
{htmlContent ? (
<div className="h-full flex justify-center p-5 overflow-auto">
<iframe
srcDoc={htmlContent}
className="w-[620px] h-full border-none bg-white shadow-sm"
title="Email preview"
/>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center gap-2 text-neutral-400">
<FileText className="h-10 w-10" />
<p className="text-sm">Enter a prompt to generate an email</p>
</div>
)}
</div>
)}
</div>
<div
className="border-t border-border p-3 shrink-0 cursor-text"
onMouseDown={(e) => {
const target = e.target as HTMLElement;
if (!target.closest("button") && target.tagName !== "TEXTAREA") {
e.preventDefault();
mobileInputRef.current?.focus();
}
}}
>
<textarea
ref={mobileInputRef}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
selection.mode === "scratch"
? "Describe the email you want..."
: `Modify the ${currentExample?.label ?? "example"}...`
}
className="w-full bg-background text-base resize-none outline-none placeholder:text-muted-foreground/50"
rows={2}
/>
<div className="flex justify-between items-center mt-2">
<span className="text-[11px] text-muted-foreground">
{selection.mode === "example" && currentExample
? currentExample.label
: "scratch"}
</span>
{generating ? (
<button
onClick={handleStop}
className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors"
aria-label="Stop"
>
<Square className="h-3 w-3" fill="currentColor" />
</button>
) : (
<button
onClick={handleGenerate}
disabled={!prompt.trim()}
className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30"
aria-label="Generate"
>
<ArrowRight className="h-3.5 w-3.5" />
</button>
)}
</div>
</div>
<Sheet open={examplesSheetOpen} onOpenChange={setExamplesSheetOpen}>
<SheetContent side="left" className="w-80 p-0">
<SheetTitle className="sr-only">Examples</SheetTitle>
<ScrollArea className="h-full">
<div className="p-2 space-y-1">
<p className="px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground">
start
</p>
<button
onClick={() => select({ mode: "scratch" })}
className={cn(
"w-full text-left px-3 py-2 rounded text-sm transition-colors",
selection.mode === "scratch"
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
>
From scratch
</button>
<p className="px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground">
examples
</p>
{examples.map((ex) => (
<button
key={ex.name}
onClick={() =>
select({ mode: "example", exampleName: ex.name })
}
className={cn(
"w-full text-left px-3 py-2 rounded text-sm transition-colors",
selection.mode === "example" &&
selection.exampleName === ex.name
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
>
<span className="font-medium">{ex.label}</span>
<p className="text-xs text-muted-foreground/70 mt-0.5 leading-snug">
{ex.description}
</p>
</button>
))}
</div>
</ScrollArea>
</SheetContent>
</Sheet>
</div>
</div>
);
}
-23
View File
@@ -1,23 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
@@ -1,64 +0,0 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
@@ -1,53 +0,0 @@
"use client";
import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
...props
}: ResizablePrimitive.GroupProps) {
return (
<ResizablePrimitive.Group
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full aria-[orientation=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: ResizablePrimitive.SeparatorProps & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.Separator
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.Separator>
);
}
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
@@ -1,58 +0,0 @@
"use client";
import * as React from "react";
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };
@@ -1,28 +0,0 @@
"use client";
import * as React from "react";
import { Separator as SeparatorPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };
@@ -1,143 +0,0 @@
"use client";
import * as React from "react";
import { XIcon } from "lucide-react";
import { Dialog as SheetPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
showCloseButton?: boolean;
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
@@ -1,91 +0,0 @@
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Tabs as TabsPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className,
)}
{...props}
/>
);
}
const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
},
);
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
@@ -1,18 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };
-8
View File
@@ -1,8 +0,0 @@
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react-email/server";
import { standardComponentDefinitions } from "@json-render/react-email/catalog";
export const emailCatalog = defineCatalog(schema, {
components: standardComponentDefinitions,
actions: {},
});
File diff suppressed because it is too large Load Diff
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-6
View File
@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
-11
View File
@@ -1,11 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: [
"@react-email/components",
"@react-email/render",
"@json-render/react-email",
],
};
export default nextConfig;
-39
View File
@@ -1,39 +0,0 @@
{
"name": "example-react-email",
"version": "0.1.3",
"type": "module",
"private": true,
"scripts": {
"dev": "portless react-email-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@ai-sdk/gateway": "^3.0.52",
"@json-render/core": "workspace:*",
"@json-render/react-email": "workspace:*",
"@tailwindcss/postcss": "^4.1.18",
"ai": "6.0.94",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"radix-ui": "^1.4.3",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-resizable-panels": "^4.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
"zod": "4.3.6"
},
"devDependencies": {
"@internal/typescript-config": "workspace:*",
"@types/node": "^22.10.0",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"shadcn": "^3.8.5",
"tw-animate-css": "^1.4.0",
"typescript": "^5.7.2"
}
}
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 KiB

-11
View File
@@ -1,11 +0,0 @@
{
"extends": "../../packages/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
-32
View File
@@ -1,37 +1,5 @@
# example-react-native
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react-native@0.13.0
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react-native@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-native@0.12.0
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react-native@0.11.0
## 0.1.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-native",
"version": "0.1.7",
"version": "0.1.3",
"private": true,
"main": "expo-router/entry",
"scripts": {
-32
View File
@@ -1,37 +1,5 @@
# example-react-pdf
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react-pdf@0.13.0
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react-pdf@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-pdf@0.12.0
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react-pdf@0.11.0
## 0.1.3
### Patch Changes
+1 -2
View File
@@ -1,10 +1,9 @@
{
"name": "example-react-pdf",
"version": "0.1.7",
"version": "0.1.3",
"type": "module",
"private": true,
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless react-pdf-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
-10
View File
@@ -1,10 +0,0 @@
# example-react-three-fiber
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react-three-fiber@0.13.0
- @json-render/react@0.13.0
-20
View File
@@ -1,20 +0,0 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "json-render React Three Fiber Example",
description: "3D scenes from JSON specs with @json-render/react-three-fiber",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body style={{ margin: 0, fontFamily: "system-ui, sans-serif" }}>
{children}
</body>
</html>
);
}
-238
View File
@@ -1,238 +0,0 @@
"use client";
import { useState } from "react";
import { defineCatalog } from "@json-render/core";
import type { ComputedFunction } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
} from "@json-render/react-three-fiber";
import { scenes } from "./scenes";
const catalog = defineCatalog(schema, {
components: {
...threeComponentDefinitions,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
...threeComponents,
},
});
function num(v: unknown, fallback: number): number {
return typeof v === "number" ? v : fallback;
}
const computedFunctions: Record<string, ComputedFunction> = {
halfHeight: (a) => num(a.h, 1) / 2,
helixX: (a) => {
const angle =
(num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +
num(a.strand, 0) * Math.PI;
return num(a.radius, 1.8) * Math.cos(angle);
},
helixY: (a) => num(a.i, 0) * num(a.spacing, 0.3) + num(a.offset, -6),
helixZ: (a) => {
const angle =
(num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +
num(a.strand, 0) * Math.PI;
return num(a.radius, 1.8) * Math.sin(angle);
},
helixHue: (a) => {
const t = num(a.i, 0) / num(a.count, 40);
return `hsl(${Math.round(t * 270 + 180)}, 85%, 60%)`;
},
spiralX: (a) => {
const t = num(a.i, 0) / num(a.count, 60);
const r = 0.5 + t * num(a.maxRadius, 7);
const angle = t * num(a.turns, 4) * 2 * Math.PI;
return r * Math.cos(angle);
},
spiralZ: (a) => {
const t = num(a.i, 0) / num(a.count, 60);
const r = 0.5 + t * num(a.maxRadius, 7);
const angle = t * num(a.turns, 4) * 2 * Math.PI;
return r * Math.sin(angle);
},
spiralY: (a) => {
const i = num(a.i, 0);
return Math.sin(i * 1.7) * 0.4;
},
spiralScale: (a) => {
const t = num(a.i, 0) / num(a.count, 60);
return 0.08 + (1 - t) * 0.25;
},
spiralEmissive: (a) => {
const t = num(a.i, 0) / num(a.count, 60);
return Math.max(0, 1 - t * 1.5);
},
circleX: (a) => {
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
return num(a.radius, 3) * Math.cos(angle);
},
circleZ: (a) => {
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
return num(a.radius, 3) * Math.sin(angle);
},
circleY: (a) => {
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
return Math.sin(angle * num(a.freq, 3)) * num(a.amp, 0.3);
},
circleAngle: (a) => {
return (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
},
circleHue: (a) => {
const t = num(a.i, 0) / num(a.count, 8);
return `hsl(${Math.round(t * 360)}, ${num(a.sat, 70)}%, ${num(a.lit, 80)}%)`;
},
};
function highlightJson(json: string): string {
return json.replace(
/("(?:\\.|[^"\\])*")\s*(:)|("(?:\\.|[^"\\])*")|([-+]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)|([{}[\]:,])/g,
(_, key, colon, str, num, lit, punct) => {
if (key) return `<span style="color:#FF4D8D">${key}</span>${colon}`;
if (str) return `<span style="color:#00CA50">${str}</span>`;
if (num) return `<span style="color:#47A8FF">${num}</span>`;
if (lit) return `<span style="color:#47A8FF">${lit}</span>`;
if (punct) return `<span style="color:#666">${punct}</span>`;
return _;
},
);
}
const LIST_WIDTH = 220;
const JSON_WIDTH = 380;
const HEADER_HEIGHT = 40;
const headerStyle: React.CSSProperties = {
height: HEADER_HEIGHT,
display: "flex",
alignItems: "center",
padding: "0 16px",
fontSize: 11,
fontWeight: 600,
color: "#555",
letterSpacing: "0.08em",
textTransform: "uppercase",
fontFamily: "ui-monospace, monospace",
borderBottom: "1px solid #1e1e1e",
flexShrink: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
boxSizing: "border-box",
};
export default function Page() {
const [selectedIndex, setSelectedIndex] = useState(0);
const selected = scenes[selectedIndex]!;
return (
<div style={{ height: "100vh", display: "flex", background: "#0a0a0a" }}>
<div
style={{
width: LIST_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderRight: "1px solid #1e1e1e",
background: "#0f0f0f",
}}
>
<div style={headerStyle}>Scenes</div>
<div style={{ flex: 1, overflowY: "auto", padding: "6px 0" }}>
{scenes.map((scene, i) => (
<button
key={scene.name}
onClick={() => setSelectedIndex(i)}
style={{
display: "block",
width: "100%",
padding: "8px 16px",
fontSize: 13,
border: "none",
textAlign: "left",
background:
i === selectedIndex
? "rgba(255,255,255,0.08)"
: "transparent",
color: i === selectedIndex ? "#fff" : "#888",
fontWeight: i === selectedIndex ? 500 : 400,
cursor: "pointer",
borderLeft:
i === selectedIndex
? "2px solid #fff"
: "2px solid transparent",
fontFamily: "inherit",
}}
>
{scene.name}
</button>
))}
</div>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
minWidth: 0,
}}
>
<div style={headerStyle}>{selected.description}</div>
<div style={{ flex: 1, background: "#000", position: "relative" }}>
<ThreeCanvas
key={selectedIndex}
spec={selected.spec}
registry={registry}
functions={computedFunctions}
shadows
camera={{ position: [0, 0, 5], fov: 50 }}
style={{ width: "100%", height: "100%" }}
/>
</div>
</div>
<div
style={{
width: JSON_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderLeft: "1px solid #1e1e1e",
background: "#0d0d0d",
}}
>
<div style={headerStyle}>Spec JSON</div>
<pre
style={{
flex: 1,
margin: 0,
padding: 14,
overflowY: "auto",
overflowX: "auto",
fontSize: 11,
lineHeight: 1.6,
fontFamily:
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
color: "#EDEDED",
tabSize: 2,
}}
dangerouslySetInnerHTML={{
__html: highlightJson(JSON.stringify(selected.spec, null, 2)),
}}
/>
</div>
</div>
);
}
@@ -1,9 +0,0 @@
import type { Spec } from "@json-render/core";
export type Scene = { name: string; description: string; spec: Spec };
export const PI = Math.PI;
export function range(n: number) {
return Array.from({ length: n }, (_, i) => ({ i }));
}
@@ -1,474 +0,0 @@
import type { Scene } from "./_helpers";
export const clockworkOrrery: Scene = {
name: "Clockwork Orrery",
description:
"Interlocking brass/gold rings spinning at different tilts and speeds with orbiting metallic planets. Like the inside of a celestial clockwork mechanism.",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"env",
"ambient",
"key",
"warm",
"accent",
"center",
"ring-spin-0",
"ring-spin-1",
"ring-spin-2",
"ring-spin-3",
"planet-orbit-0",
"planet-orbit-1",
"planet-orbit-2",
"sparkles",
"post",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 3, 8],
fov: 50,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "dawn",
background: true,
blur: 1,
intensity: 0.5,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#332200", intensity: 0.4 },
children: [],
},
key: {
type: "DirectionalLight",
props: {
position: [5, 10, 5],
rotation: null,
scale: null,
color: "#ffddaa",
intensity: 2,
castShadow: true,
},
children: [],
},
warm: {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#ffaa44",
intensity: 30,
distance: 15,
decay: 2,
castShadow: false,
},
children: [],
},
accent: {
type: "PointLight",
props: {
position: [-4, 3, -4],
rotation: null,
scale: null,
color: "#ffcc88",
intensity: 15,
distance: 15,
decay: 2,
castShadow: false,
},
children: [],
},
center: {
type: "Float",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.5,
rotationIntensity: 0.1,
floatIntensity: 0.2,
enabled: true,
},
children: ["center-sphere"],
},
"center-sphere": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.5,
widthSegments: 32,
heightSegments: 16,
color: "#ffddaa",
transmission: 1,
thickness: 0.8,
roughness: 0,
chromaticAberration: 0.1,
ior: 1.8,
distortion: 0.05,
distortionScale: null,
temporalDistortion: null,
samples: 10,
resolution: 256,
},
children: [],
},
"ring-spin-0": {
type: "Spin",
props: {
position: null,
rotation: [0.3, 0, 0.8],
scale: null,
speed: 1.2,
axis: "y",
},
children: ["ring-torus-0"],
},
"ring-torus-0": {
type: "Torus",
props: {
position: null,
rotation: [1.5708, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ffcc44",
metalness: 0.95,
roughness: 0.1,
emissive: "#cc9900",
emissiveIntensity: 0.4,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 2,
tube: 0.04,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"ring-spin-1": {
type: "Spin",
props: {
position: null,
rotation: [0.9, 0, -0.2],
scale: null,
speed: -0.7,
axis: "y",
},
children: ["ring-torus-1"],
},
"ring-torus-1": {
type: "Torus",
props: {
position: null,
rotation: [1.5708, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#cc8833",
metalness: 0.95,
roughness: 0.1,
emissive: "#aa6622",
emissiveIntensity: 0.3,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 3.2,
tube: 0.035,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"ring-spin-2": {
type: "Spin",
props: {
position: null,
rotation: [-0.4, 0, 0.5],
scale: null,
speed: 0.4,
axis: "y",
},
children: ["ring-torus-2"],
},
"ring-torus-2": {
type: "Torus",
props: {
position: null,
rotation: [1.5708, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#bb9944",
metalness: 0.95,
roughness: 0.1,
emissive: "#886622",
emissiveIntensity: 0.25,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 4.5,
tube: 0.03,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"ring-spin-3": {
type: "Spin",
props: {
position: null,
rotation: [0.1, 0, -0.1],
scale: null,
speed: -0.2,
axis: "y",
},
children: ["ring-torus-3"],
},
"ring-torus-3": {
type: "Torus",
props: {
position: null,
rotation: [1.5708, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#997744",
metalness: 0.95,
roughness: 0.1,
emissive: "#664422",
emissiveIntensity: 0.15,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 6,
tube: 0.025,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"planet-orbit-0": {
type: "Orbit",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.6,
radius: 2.5,
tilt: 0.3,
},
children: ["planet-spin-0"],
},
"planet-spin-0": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 3,
axis: "y",
},
children: ["planet-sphere-0"],
},
"planet-sphere-0": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ffffff",
metalness: 1,
roughness: 0.02,
emissive: "#ffffff",
emissiveIntensity: 0.2,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.2,
widthSegments: 32,
heightSegments: 16,
},
children: [],
},
"planet-orbit-1": {
type: "Orbit",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: -0.9,
radius: 3.8,
tilt: 0.8,
},
children: ["planet-spin-1"],
},
"planet-spin-1": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -2,
axis: "x",
},
children: ["planet-sphere-1"],
},
"planet-sphere-1": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ffcc88",
metalness: 0.8,
roughness: 0.15,
emissive: "#ffcc88",
emissiveIntensity: 0.2,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.15,
widthSegments: 32,
heightSegments: 16,
},
children: [],
},
"planet-orbit-2": {
type: "Orbit",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.35,
radius: 5.2,
tilt: -0.5,
},
children: ["planet-spin-2"],
},
"planet-spin-2": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.5,
axis: "z",
},
children: ["planet-sphere-2"],
},
"planet-sphere-2": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ccaa66",
metalness: 0.9,
roughness: 0.05,
emissive: "#ccaa66",
emissiveIntensity: 0.2,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.18,
widthSegments: 32,
heightSegments: 16,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: null,
rotation: null,
scale: [14, 10, 14],
count: 150,
speed: 0.15,
opacity: 0.3,
color: "#ffcc66",
size: 1,
noise: 2,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 1.2,
luminanceThreshold: 0.2,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.5, darkness: 0.5 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 4,
maxDistance: 18,
minPolarAngle: 0.3,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.3,
},
children: [],
},
},
},
};
@@ -1,692 +0,0 @@
import type { Scene } from "./_helpers";
export const deepSeaAbyss: Scene = {
name: "Deep Sea Abyss",
description:
"Bioluminescent deep ocean. Pitch black with glowing jellyfish-like creatures.",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"env",
"fog",
"ambient",
"jelly-1-pulse",
"jelly-2-pulse",
"jelly-3-pulse",
"light-1",
"light-2",
"light-3",
"tentacles-1",
"tentacles-2",
"tentacles-3",
"sparkles-1",
"sparkles-2",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 1, 6],
fov: 55,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "night",
background: false,
blur: 1,
intensity: 0.05,
},
children: [],
},
fog: {
type: "Fog",
props: { color: "#000510", near: 2, far: 18 },
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#001133", intensity: 0.15 },
children: [],
},
"jelly-1-pulse": {
type: "Pulse",
props: {
position: [0, 1, 0],
rotation: null,
scale: null,
speed: 0.6,
min: 0.85,
max: 1.15,
},
children: ["jelly-1-spin"],
},
"jelly-1-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.2,
axis: "y",
},
children: ["jelly-1-body"],
},
"jelly-1-body": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 1,
widthSegments: 64,
heightSegments: 32,
color: "#00ccff",
speed: 2,
distort: 0.5,
metalness: 0,
roughness: 0.2,
},
children: [],
},
"jelly-2-pulse": {
type: "Pulse",
props: {
position: [-4, -1, -3],
rotation: null,
scale: null,
speed: 0.8,
min: 0.8,
max: 1.2,
},
children: ["jelly-2-spin"],
},
"jelly-2-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.3,
axis: "y",
},
children: ["jelly-2-body"],
},
"jelly-2-body": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.7,
widthSegments: 64,
heightSegments: 32,
color: "#cc44ff",
speed: 2.5,
distort: 0.6,
metalness: 0,
roughness: 0.2,
},
children: [],
},
"jelly-3-pulse": {
type: "Pulse",
props: {
position: [3, 0.5, -5],
rotation: null,
scale: null,
speed: 0.5,
min: 0.9,
max: 1.1,
},
children: ["jelly-3-spin"],
},
"jelly-3-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.15,
axis: "y",
},
children: ["jelly-3-body"],
},
"jelly-3-body": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.5,
widthSegments: 64,
heightSegments: 32,
color: "#ff44aa",
speed: 1.5,
distort: 0.4,
metalness: 0,
roughness: 0.3,
},
children: [],
},
"light-1": {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#00ccff",
intensity: 20,
distance: 12,
decay: 2,
castShadow: null,
},
children: [],
},
"light-2": {
type: "PointLight",
props: {
position: [-4, -1, -3],
rotation: null,
scale: null,
color: "#cc44ff",
intensity: 15,
distance: 10,
decay: 2,
castShadow: null,
},
children: [],
},
"light-3": {
type: "PointLight",
props: {
position: [3, 0.5, -5],
rotation: null,
scale: null,
color: "#ff44aa",
intensity: 10,
distance: 8,
decay: 2,
castShadow: null,
},
children: [],
},
"tentacles-1": {
type: "Float",
props: {
position: [0, -1, 0],
rotation: null,
scale: null,
speed: 0.3,
rotationIntensity: 0.5,
floatIntensity: 0.8,
enabled: true,
},
children: ["t1-s1", "t1-s2", "t1-s3", "t1-s4", "t1-s5", "t1-s6"],
},
"t1-s1": {
type: "GlassSphere",
props: {
position: [0.3, -0.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.12,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s2": {
type: "GlassSphere",
props: {
position: [-0.2, -1.2, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.1,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s3": {
type: "GlassSphere",
props: {
position: [0.4, -1.9, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.08,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s4": {
type: "GlassSphere",
props: {
position: [-0.3, -2.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.07,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s5": {
type: "GlassSphere",
props: {
position: [0.1, -3.0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.06,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s6": {
type: "GlassSphere",
props: {
position: [-0.1, -3.4, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.05,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"tentacles-2": {
type: "Float",
props: {
position: [-4, -2.5, -3],
rotation: null,
scale: null,
speed: 0.4,
rotationIntensity: 0.5,
floatIntensity: 0.8,
enabled: true,
},
children: ["t2-s1", "t2-s2", "t2-s3", "t2-s4", "t2-s5"],
},
"t2-s1": {
type: "GlassSphere",
props: {
position: [0.3, -0.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.1,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t2-s2": {
type: "GlassSphere",
props: {
position: [-0.2, -1.1, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.08,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t2-s3": {
type: "GlassSphere",
props: {
position: [0.4, -1.7, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.06,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t2-s4": {
type: "GlassSphere",
props: {
position: [-0.3, -2.2, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.05,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t2-s5": {
type: "GlassSphere",
props: {
position: [0.1, -2.6, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.05,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"tentacles-3": {
type: "Float",
props: {
position: [3, -1, -5],
rotation: null,
scale: null,
speed: 0.35,
rotationIntensity: 0.5,
floatIntensity: 0.8,
enabled: true,
},
children: ["t3-s1", "t3-s2", "t3-s3", "t3-s4"],
},
"t3-s1": {
type: "GlassSphere",
props: {
position: [0.3, -0.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.09,
widthSegments: 16,
heightSegments: 8,
color: "#ff44aa",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t3-s2": {
type: "GlassSphere",
props: {
position: [-0.2, -1.0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.07,
widthSegments: 16,
heightSegments: 8,
color: "#ff44aa",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t3-s3": {
type: "GlassSphere",
props: {
position: [0.4, -1.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.06,
widthSegments: 16,
heightSegments: 8,
color: "#ff44aa",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t3-s4": {
type: "GlassSphere",
props: {
position: [-0.1, -1.9, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.05,
widthSegments: 16,
heightSegments: 8,
color: "#ff44aa",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"sparkles-1": {
type: "Sparkles",
props: {
position: [0, 0, -3],
rotation: null,
scale: [15, 10, 15],
count: 300,
speed: 0.1,
opacity: 0.4,
color: "#00aaff",
size: 1,
noise: 3,
},
children: [],
},
"sparkles-2": {
type: "Sparkles",
props: {
position: [0, -2, 0],
rotation: null,
scale: [12, 6, 12],
count: 150,
speed: 0.05,
opacity: 0.2,
color: "#8844ff",
size: 0.8,
noise: 2,
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 3,
maxDistance: 18,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.15,
},
children: [],
},
},
},
};
@@ -1,940 +0,0 @@
import { PI } from "./_helpers";
import type { Scene } from "./_helpers";
export const floatingIslands: Scene = {
name: "Floating Islands",
description:
"Sky, clouds, islands with trees and bridges floating in sunset light",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"sky",
"ambient",
"sun",
"clouds",
"island-main-float",
"island-left-float",
"island-right-float",
"island-far-float",
"island-tiny1-float",
"island-tiny2-float",
"bridge-1",
"bridge-2",
"sparkles",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [8, 6, 14],
rotation: null,
scale: null,
fov: 50,
near: 0.1,
far: 500,
makeDefault: true,
},
children: [],
},
sky: {
type: "Environment",
props: {
preset: "dawn",
background: true,
blur: 0.8,
intensity: 0.5,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#ffd4a0", intensity: 0.5 },
children: [],
},
sun: {
type: "DirectionalLight",
props: {
position: [8, 12, 6],
rotation: null,
scale: null,
color: "#ffcc88",
intensity: 2.5,
castShadow: true,
},
children: [],
},
clouds: {
type: "Cloud",
props: {
position: [0, 4, -8],
rotation: null,
scale: null,
seed: 7,
segments: 40,
bounds: [25, 3, 20],
volume: 10,
speed: 0.1,
fade: 25,
opacity: 0.6,
color: "#ffe8d0",
growth: 8,
},
children: [],
},
"island-main-float": {
type: "Float",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.4,
rotationIntensity: 0.02,
floatIntensity: 0.3,
enabled: true,
},
children: ["island-main"],
},
"island-main": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"main-base",
"main-top",
"main-underhang",
"tree-1-trunk",
"tree-1-canopy",
"tree-2-trunk",
"tree-2-canopy",
"tree-3-trunk",
"tree-3-canopy",
"house-base",
"house-roof",
],
},
"main-base": {
type: "Cylinder",
props: {
position: [0, -0.5, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 3,
radiusBottom: 1.5,
height: 2,
radialSegments: 16,
},
children: [],
},
"main-top": {
type: "Cylinder",
props: {
position: [0, 0.55, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#4a8c3f",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 3.1,
radiusBottom: 3,
height: 0.3,
radialSegments: 16,
},
children: [],
},
"main-underhang": {
type: "Cone",
props: {
position: [0, -2.5, 0],
rotation: [PI, 0, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#6B4513",
metalness: 0.1,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1.5,
height: 2.5,
radialSegments: 12,
},
children: [],
},
"tree-1-trunk": {
type: "Cylinder",
props: {
position: [1, 1, 0.5],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.06,
radiusBottom: 0.1,
height: 1.5,
radialSegments: 8,
},
children: [],
},
"tree-1-canopy": {
type: "Sphere",
props: {
position: [1, 2.2, 0.5],
rotation: null,
scale: [1, 1.2, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#2d7a2d",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.6,
widthSegments: 16,
heightSegments: 12,
},
children: [],
},
"tree-2-trunk": {
type: "Cylinder",
props: {
position: [-1.2, 0.8, -0.8],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.05,
radiusBottom: 0.08,
height: 1.1,
radialSegments: 8,
},
children: [],
},
"tree-2-canopy": {
type: "Sphere",
props: {
position: [-1.2, 1.7, -0.8],
rotation: null,
scale: [1, 1.3, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#3a8c3a",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.45,
widthSegments: 16,
heightSegments: 12,
},
children: [],
},
"tree-3-trunk": {
type: "Cylinder",
props: {
position: [-0.3, 1.2, 1.5],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.07,
radiusBottom: 0.12,
height: 2,
radialSegments: 8,
},
children: [],
},
"tree-3-canopy": {
type: "Sphere",
props: {
position: [-0.3, 2.7, 1.5],
rotation: null,
scale: [1.2, 1.4, 1.2],
castShadow: true,
receiveShadow: false,
material: {
color: "#1a6b1a",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.7,
widthSegments: 16,
heightSegments: 12,
},
children: [],
},
"house-base": {
type: "RoundedBox",
props: {
position: [0, 0.9, -0.5],
rotation: [0, 0.3, 0],
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#e8d8c0",
metalness: 0,
roughness: 0.8,
emissive: "#ffddaa",
emissiveIntensity: 0.05,
opacity: null,
transparent: null,
wireframe: null,
},
width: 0.8,
height: 0.7,
depth: 0.7,
radius: 0.05,
smoothness: 4,
},
children: [],
},
"house-roof": {
type: "Cone",
props: {
position: [0, 1.55, -0.5],
rotation: [0, 1.085, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#a04030",
metalness: 0.1,
roughness: 0.8,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.6,
height: 0.5,
radialSegments: 4,
},
children: [],
},
"island-left-float": {
type: "Float",
props: {
position: [-6, 0.1, 2],
rotation: null,
scale: null,
speed: 0.6,
rotationIntensity: 0.03,
floatIntensity: 0.5,
enabled: true,
},
children: ["island-left"],
},
"island-left": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"left-base",
"left-top",
"left-under",
"left-tree-t",
"left-tree-c",
],
},
"left-base": {
type: "Cylinder",
props: {
position: [0, -0.3, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 1.8,
radiusBottom: 0.8,
height: 1.2,
radialSegments: 12,
},
children: [],
},
"left-top": {
type: "Cylinder",
props: {
position: [0, 0.25, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#4a8c3f",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 1.85,
radiusBottom: 1.8,
height: 0.2,
radialSegments: 12,
},
children: [],
},
"left-under": {
type: "Cone",
props: {
position: [0, -1.5, 0],
rotation: [PI, 0, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#6B4513",
metalness: 0.1,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.8,
height: 1.5,
radialSegments: 10,
},
children: [],
},
"left-tree-t": {
type: "Cylinder",
props: {
position: [0.3, 0.8, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.04,
radiusBottom: 0.07,
height: 1,
radialSegments: 8,
},
children: [],
},
"left-tree-c": {
type: "Sphere",
props: {
position: [0.3, 1.6, 0],
rotation: null,
scale: [1, 1.2, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#2d7a2d",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.4,
widthSegments: 12,
heightSegments: 8,
},
children: [],
},
"island-right-float": {
type: "Float",
props: {
position: [5.5, 0, -3],
rotation: null,
scale: null,
speed: 0.5,
rotationIntensity: 0.02,
floatIntensity: 0.4,
enabled: true,
},
children: ["island-right"],
},
"island-right": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"right-base",
"right-top",
"right-under",
"right-tree-t",
"right-tree-c",
],
},
"right-base": {
type: "Cylinder",
props: {
position: [0, -0.3, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 2,
radiusBottom: 1,
height: 1.5,
radialSegments: 12,
},
children: [],
},
"right-top": {
type: "Cylinder",
props: {
position: [0, 0.4, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#4a8c3f",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 2.05,
radiusBottom: 2,
height: 0.2,
radialSegments: 12,
},
children: [],
},
"right-under": {
type: "Cone",
props: {
position: [0, -2, 0],
rotation: [PI, 0, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#6B4513",
metalness: 0.1,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1,
height: 2,
radialSegments: 10,
},
children: [],
},
"right-tree-t": {
type: "Cylinder",
props: {
position: [-0.5, 0.9, 0.3],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.05,
radiusBottom: 0.09,
height: 1.3,
radialSegments: 8,
},
children: [],
},
"right-tree-c": {
type: "Sphere",
props: {
position: [-0.5, 2, 0.3],
rotation: null,
scale: [1, 1.3, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#3a8c3a",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.5,
widthSegments: 12,
heightSegments: 8,
},
children: [],
},
"island-far-float": {
type: "Float",
props: {
position: [2, 3, -10],
rotation: null,
scale: [0.7, 0.7, 0.7],
speed: 0.3,
rotationIntensity: 0.01,
floatIntensity: 0.3,
enabled: true,
},
children: ["far-base", "far-top", "far-under"],
},
"far-base": {
type: "Cylinder",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 2.5,
radiusBottom: 1.2,
height: 1.5,
radialSegments: 12,
},
children: [],
},
"far-top": {
type: "Cylinder",
props: {
position: [0, 0.65, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#3a7a3a",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 2.55,
radiusBottom: 2.5,
height: 0.2,
radialSegments: 12,
},
children: [],
},
"far-under": {
type: "Cone",
props: {
position: [0, -1.8, 0],
rotation: [PI, 0, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#6B4513",
metalness: 0.1,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1.2,
height: 2,
radialSegments: 10,
},
children: [],
},
"island-tiny1-float": {
type: "Float",
props: {
position: [-3, 2.5, -5],
rotation: null,
scale: [0.4, 0.4, 0.4],
speed: 0.8,
rotationIntensity: 0.05,
floatIntensity: 0.8,
enabled: true,
},
children: ["tiny1-rock"],
},
"tiny1-rock": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: [1, 0.6, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1,
widthSegments: 12,
heightSegments: 8,
},
children: [],
},
"island-tiny2-float": {
type: "Float",
props: {
position: [8, -0.5, 1],
rotation: null,
scale: [0.3, 0.3, 0.3],
speed: 1,
rotationIntensity: 0.08,
floatIntensity: 1,
enabled: true,
},
children: ["tiny2-rock"],
},
"tiny2-rock": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: [1.2, 0.5, 0.8],
castShadow: true,
receiveShadow: false,
material: {
color: "#7a5a10",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1,
widthSegments: 12,
heightSegments: 8,
},
children: [],
},
"bridge-1": {
type: "RoundedBox",
props: {
position: [-3.57, -0.1, 1.19],
rotation: [0, 0.32, 0],
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
width: 3.5,
height: 0.5,
depth: 0.6,
radius: 0.05,
smoothness: 2,
},
children: [],
},
"bridge-2": {
type: "RoundedBox",
props: {
position: [3.19, -0.1, -1.74],
rotation: [0, 0.5, 0],
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
width: 3.0,
height: 0.5,
depth: 0.6,
radius: 0.05,
smoothness: 2,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: [0, 2, 0],
rotation: null,
scale: [20, 8, 20],
count: 120,
speed: 0.1,
opacity: 0.5,
color: "#ffeeaa",
size: 1.5,
noise: 1,
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 8,
maxDistance: 30,
minPolarAngle: 0.2,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.3,
target: [0, 1, 0],
},
children: [],
},
},
},
};
@@ -1,164 +0,0 @@
import type { Scene } from "./_helpers";
export const hyperspaceTunnel: Scene = {
name: "Hyperspace Tunnel",
description:
"Neon warp tunnel at breakneck speed -- bloom, camera shake, particles streaming past",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"controls",
"tunnel",
"stars",
"sparkles-core",
"sparkles-wide",
"ambient",
"center-light",
"shake",
"post",
"fog",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 0, 2],
rotation: null,
scale: null,
fov: 80,
near: 0.1,
far: 200,
makeDefault: true,
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableRotate: false,
enableZoom: false,
enablePan: false,
},
children: [],
},
tunnel: {
type: "WarpTunnel",
props: {
position: null,
rotation: null,
scale: null,
ringCount: 100,
radius: 3,
length: 50,
speed: 12,
tubeRadius: 0.025,
color1: "#00ffff",
color2: "#ff00ff",
},
children: [],
},
stars: {
type: "Stars",
props: {
radius: 80,
depth: 60,
count: 6000,
factor: 5,
saturation: 0.4,
fade: true,
speed: 3,
},
children: [],
},
"sparkles-core": {
type: "Sparkles",
props: {
position: [0, 0, -10],
rotation: null,
scale: [4, 4, 30],
count: 300,
speed: 3,
opacity: 1,
color: "#00ffff",
size: 2,
noise: 0.5,
},
children: [],
},
"sparkles-wide": {
type: "Sparkles",
props: {
position: [0, 0, -15],
rotation: null,
scale: [6, 6, 40],
count: 200,
speed: 4,
opacity: 0.7,
color: "#ff44ff",
size: 1.5,
noise: 1,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#110033", intensity: 0.2 },
children: [],
},
"center-light": {
type: "PointLight",
props: {
position: [0, 0, -5],
rotation: null,
scale: null,
color: "#00ccff",
intensity: 30,
distance: 30,
decay: 2,
castShadow: false,
},
children: [],
},
shake: {
type: "CameraShake",
props: {
intensity: 0.3,
maxYaw: 0.03,
maxPitch: 0.03,
maxRoll: 0.02,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 2.5,
luminanceThreshold: 0.1,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.3, darkness: 0.9 },
children: [],
},
fog: {
type: "Fog",
props: { color: "#030008", near: 5, far: 50 },
children: [],
},
},
},
};
@@ -1,29 +0,0 @@
import type { Scene } from "./_helpers";
import { hyperspaceTunnel } from "./hyperspace-tunnel";
import { orbitalChaos } from "./orbital-chaos";
import { clockworkOrrery } from "./clockwork-orrery";
import { deepSeaAbyss } from "./deep-sea-abyss";
import { stormCell } from "./storm-cell";
import { perpetualMotion } from "./perpetual-motion";
import { floatingIslands } from "./floating-islands";
import { productShowroom } from "./product-showroom";
import { portalGallery } from "./portal-gallery";
import { pipes } from "./pipes";
import { mystify } from "./mystify";
import { starfield } from "./starfield";
export type { Scene };
export const scenes: Scene[] = [
hyperspaceTunnel,
orbitalChaos,
clockworkOrrery,
deepSeaAbyss,
stormCell,
perpetualMotion,
floatingIslands,
productShowroom,
portalGallery,
pipes,
mystify,
starfield,
];
@@ -1,400 +0,0 @@
import type { Scene } from "./_helpers";
export const mystify: Scene = {
name: "Mystify",
description:
"Neon wireframe torus knots orbiting in the void -- the Windows Mystify screensaver in 3D",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"ambient",
"shape1",
"shape2",
"shape3",
"shape4",
"shape5",
"shape6",
"shape7",
"post",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 0, 10],
rotation: null,
scale: null,
fov: 55,
near: 0.1,
far: 1000,
makeDefault: true,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#111111", intensity: 0.1 },
children: [],
},
shape1: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.5,
radius: 3,
tilt: 0.3,
},
children: ["s1-spin"],
},
"s1-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.5,
axis: "x",
},
children: ["s1-body"],
},
"s1-body": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.4,
tube: 0.02,
p: 2,
q: 3,
tubularSegments: 128,
radialSegments: 4,
material: {
color: "#00ffff",
emissive: "#00ffff",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape2: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.4,
radius: 4,
tilt: -0.5,
},
children: ["s2-spin"],
},
"s2-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -1,
axis: "z",
},
children: ["s2-body"],
},
"s2-body": {
type: "Torus",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.6,
tube: 0.015,
radialSegments: 4,
tubularSegments: 128,
material: {
color: "#ff00ff",
emissive: "#ff00ff",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape3: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.7,
radius: 2.5,
tilt: 0.8,
},
children: ["s3-spin"],
},
"s3-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 2,
axis: "y",
},
children: ["s3-body"],
},
"s3-body": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.35,
tube: 0.015,
p: 3,
q: 2,
tubularSegments: 128,
radialSegments: 4,
material: {
color: "#ffff00",
emissive: "#ffff00",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape4: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.6,
radius: 3.5,
tilt: -0.2,
},
children: ["s4-spin"],
},
"s4-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.2,
axis: "x",
},
children: ["s4-body"],
},
"s4-body": {
type: "Torus",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.5,
tube: 0.012,
radialSegments: 4,
tubularSegments: 128,
material: {
color: "#00ff44",
emissive: "#00ff44",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape5: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.3,
radius: 5,
tilt: 0.6,
},
children: ["s5-spin"],
},
"s5-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -1.8,
axis: "z",
},
children: ["s5-body"],
},
"s5-body": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.5,
tube: 0.018,
p: 2,
q: 5,
tubularSegments: 128,
radialSegments: 4,
material: {
color: "#ff4400",
emissive: "#ff4400",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape6: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.8,
radius: 2,
tilt: -0.7,
},
children: ["s6-spin"],
},
"s6-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 2.5,
axis: "y",
},
children: ["s6-body"],
},
"s6-body": {
type: "Torus",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.3,
tube: 0.01,
radialSegments: 4,
tubularSegments: 128,
material: {
color: "#4488ff",
emissive: "#4488ff",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape7: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.45,
radius: 4.5,
tilt: 1,
},
children: ["s7-spin"],
},
"s7-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.8,
axis: "x",
},
children: ["s7-body"],
},
"s7-body": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.45,
tube: 0.015,
p: 3,
q: 4,
tubularSegments: 128,
radialSegments: 4,
material: {
color: "#ff88ff",
emissive: "#ff88ff",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 3,
luminanceThreshold: 0.05,
luminanceSmoothing: 0.3,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.2, darkness: 0.8 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: true,
enableRotate: true,
minDistance: 4,
maxDistance: 18,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.3,
},
children: [],
},
},
},
};
@@ -1,554 +0,0 @@
import { PI } from "./_helpers";
import type { Scene } from "./_helpers";
export const orbitalChaos: Scene = {
name: "Orbital Chaos",
description:
"Everything in motion -- orbiting glass, spinning knots, morphing core, all with bloom",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"env",
"stars",
"ambient",
"core-light",
"accent-1",
"accent-2",
"core-pulse",
"orbit-1",
"orbit-2",
"orbit-3",
"orbit-4",
"orbit-5",
"orbit-6",
"ring-spin-fast",
"ring-spin-slow",
"sparkles",
"post",
"fog",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 2, 8],
fov: 50,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: { preset: "night", background: false, blur: 1, intensity: 0.15 },
children: [],
},
stars: {
type: "Stars",
props: {
radius: 120,
depth: 60,
count: 8000,
factor: 5,
saturation: 0.2,
fade: true,
speed: 0.5,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#110022", intensity: 0.3 },
children: [],
},
"core-light": {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#ff44aa",
intensity: 80,
distance: 25,
decay: 2,
castShadow: false,
},
children: [],
},
"accent-1": {
type: "PointLight",
props: {
position: [5, 3, 5],
rotation: null,
scale: null,
color: "#00ffff",
intensity: 25,
distance: 20,
decay: 2,
castShadow: false,
},
children: [],
},
"accent-2": {
type: "PointLight",
props: {
position: [-5, -2, -5],
rotation: null,
scale: null,
color: "#ff00ff",
intensity: 25,
distance: 20,
decay: 2,
castShadow: false,
},
children: [],
},
"core-pulse": {
type: "Pulse",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.8,
min: 0.85,
max: 1.15,
},
children: ["core-spin"],
},
"core-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.3,
axis: "y",
},
children: ["core-blob"],
},
"core-blob": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 1.2,
widthSegments: 64,
heightSegments: 32,
color: "#ff2288",
speed: 4,
distort: 0.6,
metalness: 0.1,
roughness: 0.1,
},
children: [],
},
"orbit-1": {
type: "Orbit",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.8,
radius: 4,
tilt: 0.5,
},
children: ["orb-1-spin"],
},
"orb-1-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 2,
axis: "x",
},
children: ["orb-1"],
},
"orb-1": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.4,
widthSegments: 32,
heightSegments: 16,
color: "#00ffff",
transmission: 1,
thickness: 0.5,
roughness: 0,
chromaticAberration: 0.15,
ior: 1.8,
distortion: 0.1,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"orbit-2": {
type: "Orbit",
props: {
position: [0, 0.5, 0],
rotation: null,
scale: null,
speed: -0.6,
radius: 5.5,
tilt: 1,
},
children: ["orb-2-spin"],
},
"orb-2-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 3,
axis: "z",
},
children: ["orb-2"],
},
"orb-2": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ff00ff",
metalness: 0.9,
roughness: 0.1,
emissive: "#ff00ff",
emissiveIntensity: 0.8,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.35,
tube: 0.12,
tubularSegments: 64,
radialSegments: 8,
p: 2,
q: 3,
},
children: [],
},
"orbit-3": {
type: "Orbit",
props: {
position: [0, -0.3, 0],
rotation: null,
scale: null,
speed: 1.2,
radius: 3.2,
tilt: 0.3,
},
children: ["orb-3"],
},
"orb-3": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.3,
widthSegments: 32,
heightSegments: 16,
color: "#ff8800",
transmission: 1,
thickness: 0.4,
roughness: 0,
chromaticAberration: 0.1,
ior: 1.6,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"orbit-4": {
type: "Orbit",
props: {
position: [0, 1, 0],
rotation: null,
scale: null,
speed: -1.5,
radius: 6.5,
tilt: -0.8,
},
children: ["orb-4-spin"],
},
"orb-4-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -4,
axis: "y",
},
children: ["orb-4"],
},
"orb-4": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#00ff88",
metalness: null,
roughness: null,
emissive: "#00ff88",
emissiveIntensity: 0.8,
opacity: null,
transparent: null,
wireframe: true,
},
radius: 0.35,
widthSegments: 16,
heightSegments: 12,
},
children: [],
},
"orbit-5": {
type: "Orbit",
props: {
position: [0, -0.5, 0],
rotation: null,
scale: null,
speed: 0.4,
radius: 8,
tilt: 0.2,
},
children: ["orb-5-spin"],
},
"orb-5-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.5,
axis: "z",
},
children: ["orb-5"],
},
"orb-5": {
type: "GlassBox",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
width: 0.5,
height: 0.5,
depth: 0.5,
color: "#ff44ff",
transmission: 1,
thickness: 0.4,
roughness: 0,
chromaticAberration: 0.08,
ior: 1.7,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"orbit-6": {
type: "Orbit",
props: {
position: [0, 0.8, 0],
rotation: null,
scale: null,
speed: 2,
radius: 2.2,
tilt: 1.5,
},
children: ["orb-6"],
},
"orb-6": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.25,
widthSegments: 32,
heightSegments: 16,
color: "#ffcc00",
speed: 5,
distort: 0.7,
metalness: 0.5,
roughness: 0.1,
},
children: [],
},
"ring-spin-fast": {
type: "Spin",
props: {
position: null,
rotation: [0.3, 0, 0],
scale: null,
speed: 0.5,
axis: "y",
},
children: ["ring-inner"],
},
"ring-inner": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#00ffff",
metalness: 0.9,
roughness: 0.1,
emissive: "#00ccff",
emissiveIntensity: 0.5,
opacity: 0.3,
transparent: true,
wireframe: null,
},
radius: 9,
tube: 0.015,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"ring-spin-slow": {
type: "Spin",
props: {
position: null,
rotation: [-0.5, 0, 0.3],
scale: null,
speed: -0.3,
axis: "y",
},
children: ["ring-outer"],
},
"ring-outer": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ff00ff",
metalness: 0.9,
roughness: 0.1,
emissive: "#ff00aa",
emissiveIntensity: 0.3,
opacity: 0.2,
transparent: true,
wireframe: null,
},
radius: 11,
tube: 0.012,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: null,
rotation: null,
scale: [18, 18, 18],
count: 400,
speed: 0.4,
opacity: 0.5,
color: "#ffddaa",
size: 1.2,
noise: 2,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 2,
luminanceThreshold: 0.15,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.4, darkness: 0.7 },
children: [],
},
fog: {
type: "Fog",
props: { color: "#030008", near: 10, far: 40 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 5,
maxDistance: 25,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.4,
},
children: [],
},
},
},
};
@@ -1,307 +0,0 @@
import { PI } from "./_helpers";
import type { Scene } from "./_helpers";
export const perpetualMotion: Scene = {
name: "Perpetual Motion Machine",
description:
"A gyroscope/gimbal with 3 nested spinning rings on different axes, with a perfectly still glass sphere floating at the center. Clean, minimal, mesmerizing.",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"env",
"ambient",
"key",
"fill",
"outer-spin",
"sparkles",
"floor",
"shadows",
"post",
"controls",
],
},
env: {
type: "Environment",
props: {
preset: "studio",
background: false,
blur: null,
intensity: 1,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#ffffff", intensity: 0.3 },
children: [],
},
key: {
type: "DirectionalLight",
props: {
position: [5, 8, 5],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 2,
castShadow: true,
},
children: [],
},
fill: {
type: "PointLight",
props: {
position: [-3, 4, -3],
rotation: null,
scale: null,
color: "#aaccff",
intensity: 10,
distance: 15,
decay: 2,
castShadow: false,
},
children: [],
},
"outer-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.4,
axis: "y",
},
children: ["outer-ring", "mid-spin"],
},
"outer-ring": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#cccccc",
metalness: 1,
roughness: 0.05,
emissive: "#ffffff",
emissiveIntensity: 0.05,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 4,
tube: 0.06,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"mid-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.8,
axis: "x",
},
children: ["mid-ring", "inner-spin"],
},
"mid-ring": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ffcc44",
metalness: 0.95,
roughness: 0.08,
emissive: "#cc9900",
emissiveIntensity: 0.1,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 3,
tube: 0.05,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"inner-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.5,
axis: "z",
},
children: ["inner-ring", "center-float"],
},
"inner-ring": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#cc8844",
metalness: 0.9,
roughness: 0.1,
emissive: "#996633",
emissiveIntensity: 0.1,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 2,
tube: 0.04,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"center-float": {
type: "Float",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.3,
rotationIntensity: 0,
floatIntensity: 0.1,
enabled: true,
},
children: ["center-orb"],
},
"center-orb": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
radius: 0.8,
widthSegments: 64,
heightSegments: 32,
color: "#ffffff",
transmission: 1,
thickness: 1,
roughness: 0,
chromaticAberration: 0.12,
ior: 2,
distortion: 0.1,
distortionScale: 0.4,
temporalDistortion: 0.4,
samples: 10,
resolution: 256,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: [0, 0, 0],
rotation: null,
scale: [10, 10, 10],
count: 100,
speed: 0.1,
opacity: 0.2,
color: "#ccddff",
size: 0.8,
noise: 2,
},
children: [],
},
floor: {
type: "ReflectorPlane",
props: {
position: [0, -4.5, 0],
rotation: [-PI / 2, 0, 0],
scale: null,
width: 25,
height: 25,
color: "#1a1a2a",
resolution: 1024,
blur: 400,
mirror: 0.7,
mixBlur: 10,
mixStrength: 2,
depthScale: 0.2,
metalness: 0.5,
roughness: 1,
},
children: [],
},
shadows: {
type: "ContactShadows",
props: {
position: [0, -4.5, 0],
rotation: null,
scale: null,
opacity: 0.3,
width: 12,
height: 12,
blur: 2.5,
near: null,
far: 10,
smooth: true,
resolution: 512,
frames: null,
color: "#000000",
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 0.8,
luminanceThreshold: 0.25,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.5, darkness: 0.4 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 5,
maxDistance: 16,
minPolarAngle: 0.3,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.3,
},
children: [],
},
},
},
};
@@ -1,764 +0,0 @@
import type { Scene } from "./_helpers";
const goldMaterial = {
color: "#cc8833",
metalness: 1,
roughness: 0.05,
};
const blueMaterial = {
color: "#3388cc",
metalness: 1,
roughness: 0.05,
};
const magentaMaterial = {
color: "#cc3388",
metalness: 1,
roughness: 0.05,
};
export const pipes: Scene = {
name: "Pipes",
description:
"Classic 3D Pipes screensaver -- chrome pipes snaking through space at right angles",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"camera",
"env",
"ambient",
"directional",
"pipe-system",
"post",
"fog",
"controls",
],
},
camera: {
type: "PerspectiveCamera",
props: {
position: [0, 3, 12],
rotation: null,
scale: null,
fov: 50,
near: null,
far: null,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "warehouse",
background: false,
blur: 0.5,
intensity: 1,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#ffffff", intensity: 0.3 },
children: [],
},
directional: {
type: "DirectionalLight",
props: {
position: [5, 10, 5],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 1,
castShadow: null,
},
children: [],
},
"pipe-system": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.1,
axis: "y",
},
children: ["pipe1-group", "pipe2-group", "pipe3-group"],
},
"pipe1-group": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"p1-j0",
"p1-s1",
"p1-j1",
"p1-s2",
"p1-j2",
"p1-s3",
"p1-j3",
"p1-s4",
"p1-j4",
"p1-s5",
"p1-j5",
"p1-s6",
"p1-j6",
],
},
"p1-j0": {
type: "Sphere",
props: {
position: [-3, -3, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j1": {
type: "Sphere",
props: {
position: [-3, 0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j2": {
type: "Sphere",
props: {
position: [1, 0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j3": {
type: "Sphere",
props: {
position: [1, 2, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j4": {
type: "Sphere",
props: {
position: [1, 2, -3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j5": {
type: "Sphere",
props: {
position: [1, 0, -3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j6": {
type: "Sphere",
props: {
position: [-1, 0, -3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-s1": {
type: "Cylinder",
props: {
position: [-3, -1.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p1-s2": {
type: "Cylinder",
props: {
position: [-1, 0, 0],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 4,
radialSegments: 12,
},
children: [],
},
"p1-s3": {
type: "Cylinder",
props: {
position: [1, 1, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"p1-s4": {
type: "Cylinder",
props: {
position: [1, 2, -1.5],
rotation: [1.5708, 0, 0],
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p1-s5": {
type: "Cylinder",
props: {
position: [1, 1, -3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"p1-s6": {
type: "Cylinder",
props: {
position: [0, 0, -3],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"pipe2-group": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"p2-j0",
"p2-s1",
"p2-j1",
"p2-s2",
"p2-j2",
"p2-s3",
"p2-j3",
"p2-s4",
"p2-j4",
"p2-s5",
"p2-j5",
"p2-s6",
"p2-j6",
],
},
"p2-j0": {
type: "Sphere",
props: {
position: [2, -3, 2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j1": {
type: "Sphere",
props: {
position: [2, 1, 2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j2": {
type: "Sphere",
props: {
position: [2, 1, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j3": {
type: "Sphere",
props: {
position: [-1, 1, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j4": {
type: "Sphere",
props: {
position: [-1, 4, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j5": {
type: "Sphere",
props: {
position: [-3, 4, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j6": {
type: "Sphere",
props: {
position: [-3, 4, 1],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-s1": {
type: "Cylinder",
props: {
position: [2, -1, 2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 4,
radialSegments: 12,
},
children: [],
},
"p2-s2": {
type: "Cylinder",
props: {
position: [2, 1, 0],
rotation: [1.5708, 0, 0],
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 4,
radialSegments: 12,
},
children: [],
},
"p2-s3": {
type: "Cylinder",
props: {
position: [0.5, 1, -2],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p2-s4": {
type: "Cylinder",
props: {
position: [-1, 2.5, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p2-s5": {
type: "Cylinder",
props: {
position: [-2, 4, -2],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"p2-s6": {
type: "Cylinder",
props: {
position: [-3, 4, -0.5],
rotation: [1.5708, 0, 0],
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"pipe3-group": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"p3-j0",
"p3-s1",
"p3-j1",
"p3-s2",
"p3-j2",
"p3-s3",
"p3-j3",
"p3-s4",
"p3-j4",
"p3-s5",
"p3-j5",
],
},
"p3-j0": {
type: "Sphere",
props: {
position: [-2, 3, 3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j1": {
type: "Sphere",
props: {
position: [-2, -2, 3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j2": {
type: "Sphere",
props: {
position: [1, -2, 3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j3": {
type: "Sphere",
props: {
position: [1, -2, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j4": {
type: "Sphere",
props: {
position: [1, 0, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j5": {
type: "Sphere",
props: {
position: [-3, 0, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-s1": {
type: "Cylinder",
props: {
position: [-2, 0.5, 3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 5,
radialSegments: 12,
},
children: [],
},
"p3-s2": {
type: "Cylinder",
props: {
position: [-0.5, -2, 3],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p3-s3": {
type: "Cylinder",
props: {
position: [1, -2, 0.5],
rotation: [1.5708, 0, 0],
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 5,
radialSegments: 12,
},
children: [],
},
"p3-s4": {
type: "Cylinder",
props: {
position: [1, -1, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"p3-s5": {
type: "Cylinder",
props: {
position: [-1, 0, -2],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 4,
radialSegments: 12,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 0.4,
luminanceThreshold: 0.4,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.3, darkness: 0.4 },
children: [],
},
fog: {
type: "Fog",
props: { color: "#111111", near: 10, far: 30 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: null,
enablePan: null,
enableRotate: null,
minDistance: 5,
maxDistance: 20,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.6,
},
children: [],
},
},
},
};

Some files were not shown because too many files have changed in this diff Show More