mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 20:38:21 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9adcc09204 | ||
|
|
519a538aae | ||
|
|
a99d68c481 | ||
|
|
40892a6af0 | ||
|
|
7628640984 | ||
|
|
bf3a7ec61d | ||
|
|
453484985a |
@@ -23,7 +23,9 @@
|
|||||||
"@json-render/solid",
|
"@json-render/solid",
|
||||||
"@json-render/react-three-fiber",
|
"@json-render/react-three-fiber",
|
||||||
"@json-render/yaml",
|
"@json-render/yaml",
|
||||||
"@json-render/ink"
|
"@json-render/shadcn-svelte",
|
||||||
|
"@json-render/ink",
|
||||||
|
"@json-render/next"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ npm install @json-render/core @json-render/svelte
|
|||||||
npm install @json-render/core @json-render/solid
|
npm install @json-render/core @json-render/solid
|
||||||
# or for terminal UIs
|
# or for terminal UIs
|
||||||
npm install @json-render/core @json-render/ink ink react
|
npm install @json-render/core @json-render/ink ink react
|
||||||
|
# or for full Next.js apps (routes, layouts, SSR, metadata)
|
||||||
|
npm install @json-render/core @json-render/react @json-render/next
|
||||||
# or for 3D scenes
|
# or for 3D scenes
|
||||||
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
|
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
|
||||||
```
|
```
|
||||||
@@ -125,8 +127,10 @@ function Dashboard({ spec }) {
|
|||||||
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
|
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
|
||||||
| `@json-render/solid` | SolidJS renderer with fine-grained reactive contexts |
|
| `@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/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
||||||
|
| `@json-render/shadcn-svelte`| 36 pre-built shadcn-svelte components (Svelte 5 + Tailwind CSS) |
|
||||||
| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components) |
|
| `@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/react-native` | React Native renderer with standard mobile components |
|
||||||
|
| `@json-render/next` | Next.js renderer — JSON becomes full apps with routes, layouts, SSR |
|
||||||
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
||||||
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
|
| `@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/react-email` | React Email renderer for HTML/plain-text emails from specs |
|
||||||
@@ -482,6 +486,80 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
/>;
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Next.js (Full Apps)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { NextAppSpec } from "@json-render/next";
|
||||||
|
import { createNextApp } from "@json-render/next/server";
|
||||||
|
import { NextAppProvider } from "@json-render/next";
|
||||||
|
|
||||||
|
const spec: NextAppSpec = {
|
||||||
|
metadata: { title: { default: "My App", template: "%s | My App" } },
|
||||||
|
layouts: {
|
||||||
|
main: {
|
||||||
|
root: "shell",
|
||||||
|
elements: {
|
||||||
|
shell: { type: "Container", props: {}, children: ["nav", "slot"] },
|
||||||
|
nav: { type: "NavBar", props: {}, children: [] },
|
||||||
|
slot: { type: "Slot", props: {}, children: [] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
"/": {
|
||||||
|
layout: "main",
|
||||||
|
metadata: { title: "Home" },
|
||||||
|
page: {
|
||||||
|
root: "hero",
|
||||||
|
elements: {
|
||||||
|
hero: { type: "Card", props: { title: "Welcome" }, children: [] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Server: creates Page, generateMetadata, generateStaticParams
|
||||||
|
const app = createNextApp({ spec });
|
||||||
|
|
||||||
|
// Client: wrap your layout with NextAppProvider
|
||||||
|
// <NextAppProvider registry={registry} handlers={handlers}>
|
||||||
|
// {children}
|
||||||
|
// </NextAppProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### shadcn-svelte (Svelte)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/svelte/schema";
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/svelte";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn-svelte";
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponentDefinitions.Card,
|
||||||
|
Stack: shadcnComponentDefinitions.Stack,
|
||||||
|
Heading: shadcnComponentDefinitions.Heading,
|
||||||
|
Button: shadcnComponentDefinitions.Button,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponents.Card,
|
||||||
|
Stack: shadcnComponents.Stack,
|
||||||
|
Heading: shadcnComponents.Heading,
|
||||||
|
Button: shadcnComponents.Button,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// In your Svelte component:
|
||||||
|
// <Renderer spec={spec} registry={registry} />
|
||||||
|
```
|
||||||
|
|
||||||
### Ink (Terminal)
|
### Ink (Terminal)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
# web
|
# web
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/codegen@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
- @json-render/yaml@0.16.0
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/codegen@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
- @json-render/yaml@0.15.0
|
||||||
|
|
||||||
## 0.1.9
|
## 0.1.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/next")
|
||||||
|
|
||||||
|
# @json-render/next
|
||||||
|
|
||||||
|
Next.js renderer. JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/core @json-render/react @json-render/next
|
||||||
|
```
|
||||||
|
|
||||||
|
## schema
|
||||||
|
|
||||||
|
The Next.js app schema for multi-page specs. Use with `defineCatalog` from core.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineCatalog } from '@json-render/core';
|
||||||
|
import { schema } from '@json-render/next/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
Card: {
|
||||||
|
props: z.object({ title: z.string() }),
|
||||||
|
description: 'Card container',
|
||||||
|
},
|
||||||
|
NavBar: {
|
||||||
|
props: z.object({ links: z.array(z.object({ href: z.string(), label: z.string() })) }),
|
||||||
|
description: 'Navigation bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## createNextApp
|
||||||
|
|
||||||
|
Create all exports needed for a Next.js `[[...slug]]` catch-all route.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createNextApp } from '@json-render/next/server';
|
||||||
|
|
||||||
|
const { Page, generateMetadata, generateStaticParams } = createNextApp({
|
||||||
|
spec: myAppSpec,
|
||||||
|
loaders: {
|
||||||
|
loadPost: async ({ slug }) => {
|
||||||
|
const post = await db.post.findUnique({ where: { slug } });
|
||||||
|
return { post };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Option</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>spec</code></td>
|
||||||
|
<td><code>{'NextAppSpec | (() => NextAppSpec | Promise<NextAppSpec>)'}</code></td>
|
||||||
|
<td>The application spec (static or dynamic)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>loaders</code></td>
|
||||||
|
<td><code>{'Record<string, LoaderFn>'}</code></td>
|
||||||
|
<td>Server-side data loaders keyed by name</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Returns
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Export</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>Page</code></td>
|
||||||
|
<td>Async Server Component for <code>page.tsx</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>generateMetadata</code></td>
|
||||||
|
<td>Metadata generator for Next.js SEO</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>generateStaticParams</code></td>
|
||||||
|
<td>Static params for pre-rendering at build time</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## NextAppSpec
|
||||||
|
|
||||||
|
The top-level spec defining an entire Next.js application.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface NextAppSpec {
|
||||||
|
metadata?: NextMetadata;
|
||||||
|
routes: Record<string, NextRouteSpec>;
|
||||||
|
layouts?: Record<string, Spec>;
|
||||||
|
state?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route Patterns
|
||||||
|
|
||||||
|
Routes use Next.js URL conventions:
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Pattern</th>
|
||||||
|
<th>Example Match</th>
|
||||||
|
<th>Params</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>/</code></td>
|
||||||
|
<td><code>/</code></td>
|
||||||
|
<td><code>{'{}'}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/about</code></td>
|
||||||
|
<td><code>/about</code></td>
|
||||||
|
<td><code>{'{}'}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/blog/[slug]</code></td>
|
||||||
|
<td><code>/blog/hello</code></td>
|
||||||
|
<td><code>{'{ slug: "hello" }'}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/docs/[...path]</code></td>
|
||||||
|
<td><code>/docs/a/b/c</code></td>
|
||||||
|
<td><code>{'{ path: ["a","b","c"] }'}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/app/[[...path]]</code></td>
|
||||||
|
<td><code>/app</code> or <code>/app/x/y</code></td>
|
||||||
|
<td><code>{'{ path: [] }'}</code> or <code>{'{ path: ["x","y"] }'}</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## NextAppProvider
|
||||||
|
|
||||||
|
Client component that provides the component registry and action handlers to all pages.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { NextAppProvider } from '@json-render/next';
|
||||||
|
|
||||||
|
export default function Layout({ children }) {
|
||||||
|
return (
|
||||||
|
<NextAppProvider registry={registry} handlers={handlers}>
|
||||||
|
{children}
|
||||||
|
</NextAppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Components
|
||||||
|
|
||||||
|
### Slot
|
||||||
|
|
||||||
|
Placeholder in layouts where page content is rendered. Every layout MUST include a Slot.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Slot", "props": {}, "children": [] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Link
|
||||||
|
|
||||||
|
Client-side navigation wrapping `next/link`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Link", "props": { "href": "/about" }, "children": ["link-text"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Actions
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Params</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>setState</code></td>
|
||||||
|
<td><code>{'{ statePath, value }'}</code></td>
|
||||||
|
<td>Update a value in state</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>pushState</code></td>
|
||||||
|
<td><code>{'{ statePath, value, clearStatePath? }'}</code></td>
|
||||||
|
<td>Append to array in state</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>removeState</code></td>
|
||||||
|
<td><code>{'{ statePath, index }'}</code></td>
|
||||||
|
<td>Remove from array by index</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>navigate</code></td>
|
||||||
|
<td><code>{'{ href }'}</code></td>
|
||||||
|
<td>Client-side navigation</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Server Utilities
|
||||||
|
|
||||||
|
### matchRoute
|
||||||
|
|
||||||
|
Match a pathname against a spec's routes.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { matchRoute } from '@json-render/next/server';
|
||||||
|
|
||||||
|
const matched = matchRoute(spec, '/blog/hello-world');
|
||||||
|
// { route: NextRouteSpec, pattern: '/blog/[slug]', params: { slug: 'hello-world' } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### resolveMetadata
|
||||||
|
|
||||||
|
Resolve merged metadata for a route.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { resolveMetadata } from '@json-render/next/server';
|
||||||
|
|
||||||
|
const metadata = resolveMetadata(spec, matchedRoute?.route);
|
||||||
|
```
|
||||||
|
|
||||||
|
### slugToPath
|
||||||
|
|
||||||
|
Convert catch-all slug array to pathname.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { slugToPath } from '@json-render/next/server';
|
||||||
|
|
||||||
|
slugToPath(undefined); // "/"
|
||||||
|
slugToPath(['blog', 'hello']); // "/blog/hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Entry Points
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Import</th>
|
||||||
|
<th>Contents</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>@json-render/next</code></td>
|
||||||
|
<td>Client components (NextAppProvider, PageRenderer, Link)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>@json-render/next/server</code></td>
|
||||||
|
<td>Server utilities (createNextApp, matchRoute, schema)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/shadcn-svelte")
|
||||||
|
|
||||||
|
# @json-render/shadcn-svelte
|
||||||
|
|
||||||
|
Pre-built [shadcn-svelte](https://www.shadcn-svelte.com/) components for json-render. 36 components built on Svelte 5 + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/shadcn-svelte @json-render/core @json-render/svelte zod
|
||||||
|
```
|
||||||
|
|
||||||
|
Your app must have Tailwind CSS configured.
|
||||||
|
|
||||||
|
## Entry Points
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Entry Point</th>
|
||||||
|
<th>Exports</th>
|
||||||
|
<th>Use For</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>@json-render/shadcn-svelte</code></td>
|
||||||
|
<td><code>shadcnComponents</code>, <code>shadcnComponentDefinitions</code></td>
|
||||||
|
<td>Svelte implementations + catalog schemas</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>@json-render/shadcn-svelte/catalog</code></td>
|
||||||
|
<td><code>shadcnComponentDefinitions</code></td>
|
||||||
|
<td>Catalog schemas only (no Svelte dependency, safe for server)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Pick the components you need from the standard definitions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/svelte/schema";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog";
|
||||||
|
import { defineRegistry } from "@json-render/svelte";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn-svelte";
|
||||||
|
|
||||||
|
// Catalog: pick definitions
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponentDefinitions.Card,
|
||||||
|
Stack: shadcnComponentDefinitions.Stack,
|
||||||
|
Heading: shadcnComponentDefinitions.Heading,
|
||||||
|
Button: shadcnComponentDefinitions.Button,
|
||||||
|
Input: shadcnComponentDefinitions.Input,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registry: pick matching implementations
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponents.Card,
|
||||||
|
Stack: shadcnComponents.Stack,
|
||||||
|
Heading: shadcnComponents.Heading,
|
||||||
|
Button: shadcnComponents.Button,
|
||||||
|
Input: shadcnComponents.Input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Then render in your Svelte component:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Renderer, JsonUIProvider } from "@json-render/svelte";
|
||||||
|
|
||||||
|
export let spec;
|
||||||
|
export let registry;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<JsonUIProvider initialState={spec?.state ?? {}}>
|
||||||
|
<Renderer {spec} {registry} />
|
||||||
|
</JsonUIProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Components
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>Card</code></td>
|
||||||
|
<td>Container card with optional title, description, maxWidth, centered</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Stack</code></td>
|
||||||
|
<td>Flex container with direction, gap, align, justify</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Grid</code></td>
|
||||||
|
<td>Grid layout with columns (1-6) and gap</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Separator</code></td>
|
||||||
|
<td>Visual separator line with orientation</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>Tabs</code></td>
|
||||||
|
<td>Tabbed navigation with tabs array, defaultValue, value</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Accordion</code></td>
|
||||||
|
<td>Collapsible sections with items array and type (single/multiple)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Collapsible</code></td>
|
||||||
|
<td>Single collapsible section with title and defaultOpen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Pagination</code></td>
|
||||||
|
<td>Page navigation with totalPages and page</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Overlay
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>Dialog</code></td>
|
||||||
|
<td>Modal dialog with title, description, openPath</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Drawer</code></td>
|
||||||
|
<td>Bottom drawer with title, description, openPath</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Tooltip</code></td>
|
||||||
|
<td>Hover tooltip with content and text</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Popover</code></td>
|
||||||
|
<td>Click-triggered popover with trigger and content</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DropdownMenu</code></td>
|
||||||
|
<td>Dropdown menu with label and items array</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 with level (h1-h4)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Text</code></td>
|
||||||
|
<td>Paragraph with variant (body, caption, muted, lead, code)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Image</code></td>
|
||||||
|
<td>Image with alt, width, height</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Avatar</code></td>
|
||||||
|
<td>User avatar with src, name, size</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Badge</code></td>
|
||||||
|
<td>Status badge with text and variant</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Alert</code></td>
|
||||||
|
<td>Alert banner with title, message, type</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Carousel</code></td>
|
||||||
|
<td>Horizontally scrollable carousel with items</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Table</code></td>
|
||||||
|
<td>Data table with columns and rows</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Feedback
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>Progress</code></td>
|
||||||
|
<td>Progress bar with value, max, label</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Skeleton</code></td>
|
||||||
|
<td>Loading placeholder with width, height, rounded</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Spinner</code></td>
|
||||||
|
<td>Loading spinner with size and label</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>Button</code></td>
|
||||||
|
<td>Clickable button with label, variant, disabled</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Link</code></td>
|
||||||
|
<td>Anchor link with label and href</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Input</code></td>
|
||||||
|
<td>Text input with label, name, type, placeholder, value, checks</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Textarea</code></td>
|
||||||
|
<td>Multi-line text input with label, name, placeholder, rows, value, checks</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Select</code></td>
|
||||||
|
<td>Dropdown select with label, name, options, value, checks</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Checkbox</code></td>
|
||||||
|
<td>Checkbox with label, name, checked</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Radio</code></td>
|
||||||
|
<td>Radio button group with label, name, options, value</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Switch</code></td>
|
||||||
|
<td>Toggle switch with label, name, checked</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Slider</code></td>
|
||||||
|
<td>Range slider with label, min, max, step, value</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Toggle</code></td>
|
||||||
|
<td>Toggle button with label, pressed, variant</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>ToggleGroup</code></td>
|
||||||
|
<td>Group of toggle buttons with items, type, value</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>ButtonGroup</code></td>
|
||||||
|
<td>Group of buttons with buttons array and selected</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `/catalog` entry point has no Svelte dependency -- use it for server-side prompt generation
|
||||||
|
- Components use Tailwind CSS classes -- your app must have Tailwind configured
|
||||||
|
- Component implementations use bundled shadcn-svelte primitives (not your app's `$lib/components/ui/`)
|
||||||
|
- Form inputs support `checks` for validation (type + message pairs) and `validateOn` for timing
|
||||||
|
- Events: inputs emit `change`/`submit`/`focus`/`blur`; buttons emit `press`; selects emit `change`/`select`
|
||||||
@@ -16,8 +16,8 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
|
|||||||
|
|
||||||
GitHub repository: https://github.com/vercel-labs/json-render
|
GitHub repository: https://github.com/vercel-labs/json-render
|
||||||
Documentation: https://json-render.dev/docs
|
Documentation: https://json-render.dev/docs
|
||||||
npm packages: @json-render/core, @json-render/react, @json-render/ink, @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, @json-render/yaml
|
npm packages: @json-render/core, @json-render/react, @json-render/next, @json-render/ink, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/shadcn-svelte, @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, @json-render/yaml
|
||||||
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, ink, react-pdf, react-email, react-native, shadcn, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
|
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, next, ink, react-pdf, react-email, react-native, shadcn, shadcn-svelte, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,11 @@ export const docsNavigation: NavSection[] = [
|
|||||||
items: [
|
items: [
|
||||||
{ title: "@json-render/core", href: "/docs/api/core" },
|
{ title: "@json-render/core", href: "/docs/api/core" },
|
||||||
{ title: "@json-render/react", href: "/docs/api/react" },
|
{ title: "@json-render/react", href: "/docs/api/react" },
|
||||||
|
{ title: "@json-render/next", href: "/docs/api/next" },
|
||||||
{ title: "@json-render/react-pdf", href: "/docs/api/react-pdf" },
|
{ title: "@json-render/react-pdf", href: "/docs/api/react-pdf" },
|
||||||
{ title: "@json-render/react-email", href: "/docs/api/react-email" },
|
{ title: "@json-render/react-email", href: "/docs/api/react-email" },
|
||||||
{ title: "@json-render/shadcn", href: "/docs/api/shadcn" },
|
{ title: "@json-render/shadcn", href: "/docs/api/shadcn" },
|
||||||
|
{ title: "@json-render/shadcn-svelte", href: "/docs/api/shadcn-svelte" },
|
||||||
{ title: "@json-render/react-native", href: "/docs/api/react-native" },
|
{ title: "@json-render/react-native", href: "/docs/api/react-native" },
|
||||||
{ title: "@json-render/image", href: "/docs/api/image" },
|
{ title: "@json-render/image", href: "/docs/api/image" },
|
||||||
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
// API references
|
// API references
|
||||||
"docs/api/core": "@json-render/core API",
|
"docs/api/core": "@json-render/core API",
|
||||||
"docs/api/react": "@json-render/react API",
|
"docs/api/react": "@json-render/react API",
|
||||||
|
"docs/api/next": "@json-render/next API",
|
||||||
"docs/api/vue": "@json-render/vue API",
|
"docs/api/vue": "@json-render/vue API",
|
||||||
"docs/api/solid": "@json-render/solid API",
|
"docs/api/solid": "@json-render/solid API",
|
||||||
"docs/api/react-pdf": "@json-render/react-pdf API",
|
"docs/api/react-pdf": "@json-render/react-pdf API",
|
||||||
@@ -53,6 +54,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/api/image": "@json-render/image API",
|
"docs/api/image": "@json-render/image API",
|
||||||
"docs/api/remotion": "@json-render/remotion API",
|
"docs/api/remotion": "@json-render/remotion API",
|
||||||
"docs/api/shadcn": "@json-render/shadcn API",
|
"docs/api/shadcn": "@json-render/shadcn API",
|
||||||
|
"docs/api/shadcn-svelte": "@json-render/shadcn-svelte API",
|
||||||
"docs/api/mcp": "@json-render/mcp API",
|
"docs/api/mcp": "@json-render/mcp API",
|
||||||
"docs/api/redux": "@json-render/redux API",
|
"docs/api/redux": "@json-render/redux API",
|
||||||
"docs/api/zustand": "@json-render/zustand API",
|
"docs/api/zustand": "@json-render/zustand API",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-chat
|
# example-chat
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
- @json-render/shadcn@0.16.0
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
- @json-render/shadcn@0.15.0
|
||||||
|
|
||||||
## 0.1.9
|
## 0.1.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-chat",
|
"name": "example-chat",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-dashboard
|
# example-dashboard
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/codegen@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/codegen@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
|
||||||
## 0.1.9
|
## 0.1.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-dashboard",
|
"name": "example-dashboard",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# example-game-engine
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
- @json-render/react-three-fiber@0.16.0
|
||||||
|
- @json-render/yaml@0.16.0
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
- @json-render/react-three-fiber@0.15.0
|
||||||
|
- @json-render/yaml@0.15.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-game-engine",
|
"name": "example-game-engine",
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-image
|
# example-image
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/image@0.16.0
|
||||||
|
|
||||||
|
## 0.1.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/image@0.15.0
|
||||||
|
|
||||||
## 0.1.6
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-image",
|
"name": "example-image",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# example-ink-chat
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/ink@0.16.0
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/ink@0.15.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-ink-chat",
|
"name": "example-ink-chat",
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -85,29 +85,72 @@ function getStepHint(type: string, isLast: boolean): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// System prompt — embeds catalog documentation so the AI knows what components
|
// System prompt — handwritten design guidance + catalog documentation.
|
||||||
// are available and how to output JSONL patches.
|
// Follows the same pattern as examples/chat/lib/agent.ts: a rich
|
||||||
|
// AGENT_INSTRUCTIONS string with catalog.prompt() appended at the end.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const SYSTEM_PROMPT = catalog.prompt({
|
const AGENT_INSTRUCTIONS = `You are a terminal assistant that renders polished, information-dense UIs. You call tools for real-time data, then build clean terminal dashboards.
|
||||||
system:
|
|
||||||
"You are a terminal chat assistant. Use tools for real-time data (web_search, get_weather, get_hacker_news, get_github_repo, get_crypto_price), then render results as rich terminal UIs.",
|
WORKFLOW:
|
||||||
|
1. Call the appropriate tools to gather real data. Use web_search for topics not covered by the specialized tools (get_weather, get_hacker_news, get_github_repo, get_crypto_price).
|
||||||
|
2. While tools run, output a single short status line (e.g. "Looking up weather data..."). This is the ONLY text allowed outside the spec fence.
|
||||||
|
3. After tools return, output ALL content inside a \`\`\`spec fence. Never write paragraphs of prose outside the fence.
|
||||||
|
4. For simple text replies (greetings, clarifications), still use a \`\`\`spec with a Markdown component.
|
||||||
|
|
||||||
|
DESIGN PRINCIPLES:
|
||||||
|
- HIERARCHY: Every response needs clear visual structure. Start with an h1 Heading for the topic. Use h2 Headings for subsections. Use Card to group related content into shaded areas — Cards render as subtle background fills, not bordered boxes.
|
||||||
|
- LEAD WITH THE STORY: Open with a brief Markdown paragraph (2-3 sentences) that tells the user the key insight or takeaway. Don't just dump data — frame it.
|
||||||
|
- SUMMARY METRICS: After the narrative, show 2-4 Metric components for the most important numbers. Metric displays a dim label, bold value, and optional colored trend (up=green, down=red). Group them in a horizontal Box (flexDirection: row, gap: 3) so they read like a dashboard header. Use KeyValue only for simple label:value pairs that don't need emphasis.
|
||||||
|
- DETAIL SECTIONS: Below the summary, use h2 Headings to introduce each section, followed by a single focused visualization (Table, BarChart, or set of KeyValues).
|
||||||
|
- ONE REPRESENTATION PER DATA POINT: Never show the same value as both a number and a percentage and a bar. Pick the most meaningful format. Use BarChart with showValues:true OR showPercentage:true, not both.
|
||||||
|
- TABLES: Always set explicit column widths so columns don't collapse. Use headerColor:"cyan". Keep column headers short (abbreviate if needed). Right-align numeric columns.
|
||||||
|
- CHARTS: Use distinct colors per bar in BarChart. Good palette: cyan, green, yellow, magenta, blue, red. Use Sparkline for compact inline trends alongside other content.
|
||||||
|
- COLOR STRATEGY: Use color with intention, not decoration. cyan for labels and headers. green for positive values, growth, success. red for negative values, decline, errors. yellow for warnings or neutral highlights. dimColor:true for secondary/supporting text. Avoid coloring everything — contrast comes from restraint.
|
||||||
|
- TABLES: Use borderStyle:"single" on Tables for a clean outline. Do NOT put Tables inside Cards — Tables have their own border and don't need additional wrapping.
|
||||||
|
- SPACING: Use gap:1 between sections. Don't over-pad. Keep the UI compact and scannable. NEVER add padding to the root element — the app already provides outer padding.
|
||||||
|
- WIDTH: Target 80 columns. Set explicit widths on Tables (total columns should sum to ~70-75). Use wrap:"truncate-end" on Text in tight spaces.
|
||||||
|
- CALLOUTS: Use Callout for key takeaways, important notes, tips, and warnings. Set type (info/tip/warning/important) for a colored left border accent. Keep content concise — one key point per Callout.
|
||||||
|
- TIMELINES: Use Timeline for historical events, step-by-step processes, and milestones. Set status per item (completed/current/upcoming) for colored dots. Include dates when available.
|
||||||
|
- NEVER use emojis anywhere — not in text, labels, titles, table cells, Heading text, or component props. Plain text only.
|
||||||
|
|
||||||
|
DASHBOARD PATTERN (use for data-heavy responses):
|
||||||
|
Root Box (column, gap:1) >
|
||||||
|
Heading (h1, topic title)
|
||||||
|
Markdown (2-3 sentence summary with key takeaway)
|
||||||
|
Box (row, gap:3) > [Metric, Metric, Metric] (top-line metrics, no Card)
|
||||||
|
Heading (h2, section title)
|
||||||
|
Table (borderStyle:"single")
|
||||||
|
Card (title:"Section Name") > BarChart (bar charts go in a titled Card — the Card title replaces h2)
|
||||||
|
Callout (type:"tip", key takeaway or closing note)
|
||||||
|
Card wrapping rules: Wrap BarCharts in a Card with a title. Do NOT wrap Metrics or Tables in Cards — Metrics stand alone, Tables have their own border.
|
||||||
|
|
||||||
|
COMPARISON PATTERN:
|
||||||
|
Use BarChart when you want the user to see relative magnitudes at a glance.
|
||||||
|
Use Table when there are 3+ columns of mixed data types.
|
||||||
|
Never use both for the same data.
|
||||||
|
|
||||||
|
TREND PATTERN:
|
||||||
|
Use Sparkline for compact inline trend next to a KeyValue.
|
||||||
|
Use BarChart with year/period labels for detailed time-series.
|
||||||
|
|
||||||
|
INTERACTIVITY:
|
||||||
|
- You can create interactive forms, surveys, and selection interfaces. The user navigates with arrow keys, selects with Space/Enter, and types into text fields.
|
||||||
|
- ALWAYS include a submit action on interactive UIs. Add a Text or StatusLine telling the user how to submit. Wire submit events to a "submit" action — the app collects form state automatically.
|
||||||
|
- ALWAYS populate the state field with sensible defaults for all bound values.
|
||||||
|
- Use $bindState on interactive components for two-way binding. Example: { "value": { "$state": "/choice" }, "$bindState": { "value": "/choice" } }.
|
||||||
|
- Use Tabs for multi-section surveys. Use ConfirmInput for yes/no prompts.
|
||||||
|
- After receiving form data, acknowledge the user's choices meaningfully — don't just echo them back.
|
||||||
|
|
||||||
|
${catalog.prompt({
|
||||||
mode: "inline",
|
mode: "inline",
|
||||||
customRules: [
|
customRules: [
|
||||||
"ALL text MUST go inside the spec using the Markdown component. NEVER write prose, summaries, or explanations outside the ```spec fence. The ONLY text allowed outside the fence is a single short status like 'Searching…' while tools run.",
|
"ALL text MUST go inside the spec using the Markdown component. The ONLY text outside the fence is a short tool-status line.",
|
||||||
"For text-only answers (greetings, clarifications), still output a ```spec with a Markdown component.",
|
"For text-only answers, still output a spec with a Markdown component.",
|
||||||
"Prefer Table for structured data (forecasts, leaderboards, language breakdowns) and KeyValue for label-value pairs (stats, metadata).",
|
"Prefer Table for structured data and KeyValue for label-value pairs.",
|
||||||
|
"NEVER use emojis anywhere in your output. Plain text only.",
|
||||||
// Interactive components
|
|
||||||
"INTERACTIVE UIs: You can create interactive forms, surveys, and selection interfaces that the user actually interacts with in the terminal. The user navigates with arrow keys, selects with Space/Enter, and types into text fields.",
|
|
||||||
"When creating interactive UIs, ALWAYS include a submit action. Add an element (usually a Text or StatusLine) that tells the user to press Enter or a specific key to submit. Wire up the submit event to a 'submit' action — the app will collect the form state and send it back to you automatically.",
|
|
||||||
"For interactive specs, ALWAYS populate the state field with sensible defaults for all bound values. Example: if a Select binds to /framework, include state: { framework: 'react' }.",
|
|
||||||
'IMPORTANT: Use $bindState on interactive components so their values are written back to state. Example for Select: { "value": { "$state": "/choice" }, "$bindState": { "value": "/choice" } }.',
|
|
||||||
"For surveys/quizzes with multiple sections, use Tabs to organize them. Bind the active tab to state and use visible conditions on children.",
|
|
||||||
"For confirmation prompts, use ConfirmInput with a clear message. The confirm/deny events can trigger actions.",
|
|
||||||
"After receiving submitted form data, acknowledge the user's choices and respond to them — don't just repeat what they selected.",
|
|
||||||
],
|
],
|
||||||
});
|
})}`;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -236,7 +279,6 @@ function MessageView({ message }: { message: Message }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Text bold>AI:</Text>
|
|
||||||
{message.spec ? (
|
{message.spec ? (
|
||||||
<JSONUIProvider initialState={message.spec.state ?? {}}>
|
<JSONUIProvider initialState={message.spec.state ?? {}}>
|
||||||
<DisableFocus />
|
<DisableFocus />
|
||||||
@@ -317,9 +359,6 @@ function LiveInteractiveSpec({
|
|||||||
if (interactiveKeys.length === 0) {
|
if (interactiveKeys.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Text bold color="green">
|
|
||||||
AI:
|
|
||||||
</Text>
|
|
||||||
<JSONUIProvider store={store} handlers={handlers}>
|
<JSONUIProvider store={store} handlers={handlers}>
|
||||||
<Renderer spec={spec} />
|
<Renderer spec={spec} />
|
||||||
</JSONUIProvider>
|
</JSONUIProvider>
|
||||||
@@ -331,9 +370,6 @@ function LiveInteractiveSpec({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Text bold color="green">
|
|
||||||
AI:
|
|
||||||
</Text>
|
|
||||||
{interactiveKeys.length > 1 && (
|
{interactiveKeys.length > 1 && (
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
Step {step + 1} of {interactiveKeys.length}
|
Step {step + 1} of {interactiveKeys.length}
|
||||||
@@ -417,7 +453,7 @@ export function App() {
|
|||||||
try {
|
try {
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: gateway(process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
|
model: gateway(process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
|
||||||
system: SYSTEM_PROMPT,
|
system: AGENT_INSTRUCTIONS,
|
||||||
messages: history,
|
messages: history,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
abortSignal: controller.signal,
|
abortSignal: controller.signal,
|
||||||
@@ -535,11 +571,26 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column" padding={1} minHeight={stdout.rows}>
|
<Box flexDirection="column" padding={1} minHeight={stdout.rows}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box marginBottom={1}>
|
<Box marginBottom={1} gap={1}>
|
||||||
<Text bold>json-render terminal chat</Text>
|
<Text bold color="cyan">
|
||||||
<Text dimColor> (Ctrl+C to exit)</Text>
|
json-render
|
||||||
|
</Text>
|
||||||
|
<Text dimColor>Ctrl+C to exit</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Empty state — show example prompts when no conversation yet */}
|
||||||
|
{messages.length === 0 && !isStreaming && (
|
||||||
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
|
<Text dimColor>Try asking:</Text>
|
||||||
|
<Box flexDirection="column" paddingLeft={2} marginTop={1} gap={0}>
|
||||||
|
<Text dimColor>{" weather in tokyo"}</Text>
|
||||||
|
<Text dimColor>{" top hacker news stories"}</Text>
|
||||||
|
<Text dimColor>{" tell me about vercel/next.js"}</Text>
|
||||||
|
<Text dimColor>{" bitcoin price"}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Message history — collapsed when interactive wizard is active */}
|
{/* Message history — collapsed when interactive wizard is active */}
|
||||||
{liveSpec && !isStreaming ? (
|
{liveSpec && !isStreaming ? (
|
||||||
<>
|
<>
|
||||||
@@ -568,10 +619,9 @@ export function App() {
|
|||||||
{/* Live spec preview while streaming */}
|
{/* Live spec preview while streaming */}
|
||||||
{isStreaming && streamingSpec && streamingSpec.root && (
|
{isStreaming && streamingSpec && streamingSpec.root && (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Text bold>AI:</Text>
|
|
||||||
<JSONUIProvider initialState={streamingSpec.state ?? {}}>
|
<JSONUIProvider initialState={streamingSpec.state ?? {}}>
|
||||||
<DisableFocus />
|
<DisableFocus />
|
||||||
<Renderer spec={streamingSpec} />
|
<Renderer spec={streamingSpec} loading />
|
||||||
</JSONUIProvider>
|
</JSONUIProvider>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -275,7 +275,8 @@ export const getGitHubRepo = tool({
|
|||||||
const languageBreakdown = Object.entries(languages)
|
const languageBreakdown = Object.entries(languages)
|
||||||
.map(([lang, bytes]) => ({
|
.map(([lang, bytes]) => ({
|
||||||
language: lang,
|
language: lang,
|
||||||
percentage: Math.round((bytes / totalBytes) * 100),
|
percentage:
|
||||||
|
totalBytes > 0 ? Math.round((bytes / totalBytes) * 100) : 0,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.percentage - a.percentage)
|
.sort((a, b) => b.percentage - a.percentage)
|
||||||
.slice(0, 6);
|
.slice(0, 6);
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
# example-mcp
|
# example-mcp
|
||||||
|
|
||||||
|
## 0.1.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/mcp@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
- @json-render/shadcn@0.16.0
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/mcp@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
- @json-render/shadcn@0.15.0
|
||||||
|
|
||||||
## 0.1.5
|
## 0.1.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-mcp",
|
"name": "example-mcp",
|
||||||
"version": "0.1.5",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# example-next-website-builder
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/next@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
- @json-render/shadcn@0.16.0
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { NextAppProvider } from "@json-render/next";
|
||||||
|
import { registry } from "@/lib/registry";
|
||||||
|
|
||||||
|
export default function WebsiteLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <NextAppProvider registry={registry}>{children}</NextAppProvider>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getPageData, generateMetadata, generateStaticParams } from "@/lib/app";
|
||||||
|
import { WebsiteRenderer } from "./renderer";
|
||||||
|
|
||||||
|
export { generateMetadata, generateStaticParams };
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ slug?: string[] }>;
|
||||||
|
}) {
|
||||||
|
const data = await getPageData({ params });
|
||||||
|
if (!data) notFound();
|
||||||
|
return <WebsiteRenderer {...data} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { PageRenderer, type PageRendererProps } from "@json-render/next";
|
||||||
|
|
||||||
|
export function WebsiteRenderer(props: PageRendererProps) {
|
||||||
|
return <PageRenderer {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { NextAppSpec } from "@json-render/next";
|
||||||
|
import { getSpec, setSpec } from "@/lib/spec-store";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return Response.json(getSpec());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(req: Request) {
|
||||||
|
setSpec((await req.json()) as NextAppSpec);
|
||||||
|
return Response.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Editor } from "@/components/editor";
|
||||||
|
|
||||||
|
export default function BuilderPage() {
|
||||||
|
return <Editor />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@source "../../../packages/shadcn/src/**/*.tsx";
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
: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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { GeistSans } from "geist/font/sans";
|
||||||
|
import { GeistMono } from "geist/font/mono";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Next Website Builder | json-render",
|
||||||
|
description:
|
||||||
|
"Build entire Next.js websites from JSON specs with @json-render/next",
|
||||||
|
icons: { icon: "/icon.svg" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className={`${GeistSans.variable} ${GeistMono.variable}`}>
|
||||||
|
<body className="font-sans antialiased">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type MouseEvent,
|
||||||
|
} from "react";
|
||||||
|
import { JsonEditor, type JsonValue } from "@visual-json/react";
|
||||||
|
import type { NextAppSpec } from "@json-render/next";
|
||||||
|
import { NextAppProvider, PageRenderer } from "@json-render/next";
|
||||||
|
import {
|
||||||
|
ResizablePanelGroup,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizableHandle,
|
||||||
|
} from "@/components/ui/resizable";
|
||||||
|
import { AddressBar } from "@/components/route-tabs";
|
||||||
|
import { registry } from "@/lib/registry";
|
||||||
|
|
||||||
|
export function Editor() {
|
||||||
|
const [spec, setSpec] = useState<NextAppSpec | null>(null);
|
||||||
|
const [activeRoute, setActiveRoute] = useState("/");
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/spec")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data: NextAppSpec) => setSpec(data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = useCallback((value: JsonValue) => {
|
||||||
|
const updated = value as unknown as NextAppSpec;
|
||||||
|
setSpec(updated);
|
||||||
|
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
fetch("/api/spec", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(updated),
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePreviewClick = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
const anchor = (e.target as HTMLElement).closest("a");
|
||||||
|
if (!anchor) return;
|
||||||
|
const href = anchor.getAttribute("href");
|
||||||
|
if (!href || href.startsWith("http") || href.startsWith("mailto:")) return;
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveRoute(href);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const currentRoute = useMemo(() => {
|
||||||
|
if (!spec) return null;
|
||||||
|
return spec.routes[activeRoute] ?? null;
|
||||||
|
}, [spec, activeRoute]);
|
||||||
|
|
||||||
|
const layoutSpec = useMemo(() => {
|
||||||
|
if (!spec || !currentRoute?.layout || !spec.layouts) return null;
|
||||||
|
return spec.layouts[currentRoute.layout] ?? null;
|
||||||
|
}, [spec, currentRoute]);
|
||||||
|
|
||||||
|
const initialState = useMemo(() => {
|
||||||
|
if (!spec || !currentRoute) return undefined;
|
||||||
|
const merged: Record<string, unknown> = {};
|
||||||
|
if (spec.state) Object.assign(merged, spec.state);
|
||||||
|
if (currentRoute.page.state) Object.assign(merged, currentRoute.page.state);
|
||||||
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||||
|
}, [spec, currentRoute]);
|
||||||
|
|
||||||
|
if (!spec) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen text-muted-foreground">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col">
|
||||||
|
<div className="flex items-center justify-between px-4 h-12 border-b border-border bg-background shrink-0">
|
||||||
|
<span className="text-sm font-semibold">Next Website Builder</span>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
View Website
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ResizablePanelGroup orientation="horizontal" className="flex-1">
|
||||||
|
<ResizablePanel defaultSize={45} minSize={25}>
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between px-3 h-10 border-b border-border bg-muted/30">
|
||||||
|
<span className="text-xs font-mono text-muted-foreground">
|
||||||
|
spec.json
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen((v) => !v)}
|
||||||
|
className="flex items-center justify-center w-6 h-6 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||||
|
title={sidebarOpen ? "Hide sidebar" : "Show sidebar"}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
|
<path d="M9 3v18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<JsonEditor
|
||||||
|
value={spec as unknown as JsonValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
sidebarOpen={sidebarOpen}
|
||||||
|
height="100%"
|
||||||
|
className="h-full"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--vj-bg": "var(--background)",
|
||||||
|
"--vj-bg-panel": "var(--background)",
|
||||||
|
"--vj-bg-hover": "var(--muted)",
|
||||||
|
"--vj-bg-selected": "var(--primary)",
|
||||||
|
"--vj-bg-selected-muted": "var(--muted)",
|
||||||
|
"--vj-text": "var(--foreground)",
|
||||||
|
"--vj-text-selected": "var(--primary-foreground)",
|
||||||
|
"--vj-text-muted": "var(--muted-foreground)",
|
||||||
|
"--vj-text-dim": "var(--muted-foreground)",
|
||||||
|
"--vj-border": "var(--border)",
|
||||||
|
"--vj-border-subtle": "var(--border)",
|
||||||
|
"--vj-accent": "var(--primary)",
|
||||||
|
"--vj-accent-muted": "var(--muted)",
|
||||||
|
"--vj-input-bg": "var(--secondary)",
|
||||||
|
"--vj-input-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
<ResizablePanel defaultSize={55} minSize={30}>
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<AddressBar route={activeRoute} onNavigate={setActiveRoute} />
|
||||||
|
<div
|
||||||
|
className="flex-1 overflow-auto bg-background"
|
||||||
|
onClick={handlePreviewClick}
|
||||||
|
>
|
||||||
|
{currentRoute ? (
|
||||||
|
<NextAppProvider registry={registry}>
|
||||||
|
<PageRenderer
|
||||||
|
spec={currentRoute.page}
|
||||||
|
initialState={initialState}
|
||||||
|
layoutSpec={layoutSpec}
|
||||||
|
/>
|
||||||
|
</NextAppProvider>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
||||||
|
Route not found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useState, type KeyboardEvent } from "react";
|
||||||
|
|
||||||
|
interface AddressBarProps {
|
||||||
|
route: string;
|
||||||
|
onNavigate: (route: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddressBar({ route, onNavigate }: AddressBarProps) {
|
||||||
|
const [value, setValue] = useState(route);
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
|
||||||
|
const commit = useCallback(() => {
|
||||||
|
let normalized = value.trim();
|
||||||
|
if (!normalized.startsWith("/")) normalized = "/" + normalized;
|
||||||
|
onNavigate(normalized);
|
||||||
|
setValue(normalized);
|
||||||
|
}, [value, onNavigate]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
commit();
|
||||||
|
(e.target as HTMLInputElement).blur();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[commit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayValue = focused ? value : route;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center h-10 px-2 border-b border-border bg-muted/30">
|
||||||
|
<div className="flex items-center flex-1 h-7 rounded-md border border-border bg-background px-2 gap-1.5">
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-muted-foreground shrink-0"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M2 12h20" />
|
||||||
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={displayValue}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onFocus={() => {
|
||||||
|
setFocused(true);
|
||||||
|
setValue(route);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setFocused(false);
|
||||||
|
commit();
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
spellCheck={false}
|
||||||
|
className="flex-1 bg-transparent text-xs font-mono text-foreground outline-none placeholder:text-muted-foreground"
|
||||||
|
placeholder="/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"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 };
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createNextApp } from "@json-render/next/server";
|
||||||
|
import { getSpec } from "./spec-store";
|
||||||
|
|
||||||
|
export const { getPageData, generateMetadata, generateStaticParams } =
|
||||||
|
createNextApp({
|
||||||
|
spec: () => getSpec(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/next/server";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
|
import { websiteComponentDefinitions } from "./website-catalog";
|
||||||
|
|
||||||
|
export const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
...shadcnComponentDefinitions,
|
||||||
|
...websiteComponentDefinitions,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
import type { NextAppSpec } from "@json-render/next";
|
||||||
|
|
||||||
|
export const defaultSpec: NextAppSpec = {
|
||||||
|
metadata: {
|
||||||
|
title: {
|
||||||
|
default: "Acme Inc",
|
||||||
|
template: "%s | Acme Inc",
|
||||||
|
},
|
||||||
|
description: "We build the future of software.",
|
||||||
|
icons: "/icon.svg",
|
||||||
|
},
|
||||||
|
|
||||||
|
state: {},
|
||||||
|
|
||||||
|
layouts: {
|
||||||
|
main: {
|
||||||
|
root: "wrapper",
|
||||||
|
elements: {
|
||||||
|
wrapper: {
|
||||||
|
type: "Stack",
|
||||||
|
props: {
|
||||||
|
direction: "vertical",
|
||||||
|
gap: "none",
|
||||||
|
align: "stretch",
|
||||||
|
className: "min-h-screen",
|
||||||
|
},
|
||||||
|
children: ["header", "content", "footer"],
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
type: "Header",
|
||||||
|
props: {
|
||||||
|
brand: "Acme Inc",
|
||||||
|
links: [
|
||||||
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: "About", href: "/about" },
|
||||||
|
{ label: "Contact", href: "/contact" },
|
||||||
|
],
|
||||||
|
variant: "simple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: "Slot",
|
||||||
|
props: {},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: "Footer",
|
||||||
|
props: {
|
||||||
|
brand: "Acme Inc",
|
||||||
|
links: [
|
||||||
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: "About", href: "/about" },
|
||||||
|
{ label: "Contact", href: "/contact" },
|
||||||
|
],
|
||||||
|
copyright: "2026 Acme Inc. All rights reserved.",
|
||||||
|
variant: "simple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
routes: {
|
||||||
|
"/": {
|
||||||
|
layout: "main",
|
||||||
|
metadata: {
|
||||||
|
title: "Home",
|
||||||
|
description: "Welcome to Acme Inc - we build the future of software.",
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
root: "page",
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
type: "Stack",
|
||||||
|
props: {
|
||||||
|
direction: "vertical",
|
||||||
|
gap: "none",
|
||||||
|
align: "stretch",
|
||||||
|
},
|
||||||
|
children: ["hero", "features", "testimonials", "cta"],
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
type: "Hero",
|
||||||
|
props: {
|
||||||
|
headline: "Build the future with Acme",
|
||||||
|
description:
|
||||||
|
"We help companies ship better software, faster. Our platform provides the tools, infrastructure, and insights you need to build world-class products.",
|
||||||
|
primaryCta: { label: "Get Started", href: "/contact" },
|
||||||
|
secondaryCta: { label: "Learn More", href: "/about" },
|
||||||
|
badge: "Now in Beta",
|
||||||
|
variant: "centered",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: "Features",
|
||||||
|
props: {
|
||||||
|
headline: "Why Acme?",
|
||||||
|
description:
|
||||||
|
"Everything you need to build, deploy, and scale your applications.",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Lightning Fast",
|
||||||
|
description:
|
||||||
|
"Optimized for speed with edge-first architecture and smart caching.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Secure by Default",
|
||||||
|
description:
|
||||||
|
"Enterprise-grade security with end-to-end encryption and SOC 2 compliance.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Developer First",
|
||||||
|
description:
|
||||||
|
"APIs, SDKs, and CLI tools designed to fit your workflow perfectly.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: 3,
|
||||||
|
variant: "cards",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testimonials: {
|
||||||
|
type: "Testimonials",
|
||||||
|
props: {
|
||||||
|
headline: "Trusted by industry leaders",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
"Acme has completely transformed how we ship products. Our deployment time went from hours to minutes.",
|
||||||
|
author: "Jordan Lee",
|
||||||
|
role: "VP Engineering at Globex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
"The developer experience is unmatched. Our team onboarded in a single afternoon.",
|
||||||
|
author: "Priya Patel",
|
||||||
|
role: "CTO at Initech",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
"We cut our infrastructure costs by 40% within the first quarter of switching to Acme.",
|
||||||
|
author: "Sam Ortiz",
|
||||||
|
role: "Head of Platform at Hooli",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
type: "CTA",
|
||||||
|
props: {
|
||||||
|
headline: "Ready to get started?",
|
||||||
|
description:
|
||||||
|
"Join thousands of teams building better software with Acme.",
|
||||||
|
buttonLabel: "Contact Us",
|
||||||
|
buttonHref: "/contact",
|
||||||
|
variant: "banner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"/about": {
|
||||||
|
layout: "main",
|
||||||
|
metadata: {
|
||||||
|
title: "About",
|
||||||
|
description:
|
||||||
|
"Learn about our mission, values, and the team behind Acme.",
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
root: "page",
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
type: "Stack",
|
||||||
|
props: {
|
||||||
|
direction: "vertical",
|
||||||
|
gap: "none",
|
||||||
|
align: "stretch",
|
||||||
|
},
|
||||||
|
children: ["hero", "features", "team", "cta"],
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
type: "Hero",
|
||||||
|
props: {
|
||||||
|
headline: "About Acme Inc",
|
||||||
|
description:
|
||||||
|
"Founded in 2024, we are on a mission to make software development accessible to everyone. We believe the best tools should be simple, powerful, and delightful to use.",
|
||||||
|
variant: "centered",
|
||||||
|
primaryCta: null,
|
||||||
|
secondaryCta: null,
|
||||||
|
badge: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: "Features",
|
||||||
|
props: {
|
||||||
|
headline: "Our Values",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Simplicity",
|
||||||
|
description:
|
||||||
|
"We remove complexity so you can focus on building great products.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Transparency",
|
||||||
|
description:
|
||||||
|
"Open communication, honest pricing, and clear documentation.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Craftsmanship",
|
||||||
|
description:
|
||||||
|
"Every detail matters. We sweat the small stuff so you don't have to.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: 3,
|
||||||
|
variant: "simple",
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
type: "Team",
|
||||||
|
props: {
|
||||||
|
headline: "Meet the Team",
|
||||||
|
description:
|
||||||
|
"The people behind Acme who are passionate about building great developer tools.",
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
name: "Alex Chen",
|
||||||
|
role: "CEO & Co-founder",
|
||||||
|
bio: "Previously at Vercel and Google. Passionate about developer experience.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sarah Kim",
|
||||||
|
role: "CTO & Co-founder",
|
||||||
|
bio: "Built distributed systems at AWS. Loves solving hard infrastructure problems.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Marcus Rivera",
|
||||||
|
role: "Head of Design",
|
||||||
|
bio: "Former design lead at Figma and Stripe. Obsessed with clarity and craft.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variant: "grid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
type: "CTA",
|
||||||
|
props: {
|
||||||
|
headline: "Want to join us?",
|
||||||
|
description:
|
||||||
|
"We are always looking for talented people to join our team.",
|
||||||
|
buttonLabel: "Get in Touch",
|
||||||
|
buttonHref: "/contact",
|
||||||
|
variant: "centered",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"/contact": {
|
||||||
|
layout: "main",
|
||||||
|
metadata: {
|
||||||
|
title: "Contact",
|
||||||
|
description: "Get in touch with the Acme team.",
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
root: "page",
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
type: "Stack",
|
||||||
|
props: {
|
||||||
|
direction: "vertical",
|
||||||
|
gap: "none",
|
||||||
|
align: "stretch",
|
||||||
|
},
|
||||||
|
children: ["form"],
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: "ContactForm",
|
||||||
|
props: {
|
||||||
|
headline: "Get in Touch",
|
||||||
|
description:
|
||||||
|
"Have a question or want to work with us? Fill out the form below and we'll get back to you within 24 hours.",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: "Name",
|
||||||
|
type: "text" as const,
|
||||||
|
placeholder: "Your name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Email",
|
||||||
|
type: "email" as const,
|
||||||
|
placeholder: "you@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Message",
|
||||||
|
type: "textarea" as const,
|
||||||
|
placeholder: "Tell us about your project...",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
submitLabel: "Send Message",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { defineRegistry } from "@json-render/react";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
|
import { catalog } from "./catalog";
|
||||||
|
import { websiteComponents } from "./website-components";
|
||||||
|
|
||||||
|
export const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
...shadcnComponents,
|
||||||
|
...websiteComponents,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { NextAppSpec } from "@json-render/next";
|
||||||
|
import { defaultSpec } from "./default-spec";
|
||||||
|
|
||||||
|
let currentSpec: NextAppSpec = defaultSpec;
|
||||||
|
|
||||||
|
export function getSpec(): NextAppSpec {
|
||||||
|
return currentSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSpec(spec: NextAppSpec): void {
|
||||||
|
currentSpec = spec;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const linkSchema = z.object({
|
||||||
|
label: z.string(),
|
||||||
|
href: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctaSchema = z.object({
|
||||||
|
label: z.string(),
|
||||||
|
href: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const websiteComponentDefinitions = {
|
||||||
|
Header: {
|
||||||
|
props: z.object({
|
||||||
|
brand: z.string(),
|
||||||
|
links: z.array(linkSchema),
|
||||||
|
variant: z.enum(["simple", "centered"]).nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
"Site header / navigation bar. Simple: brand left, links right. Centered: brand centered above links.",
|
||||||
|
example: {
|
||||||
|
brand: "Acme Inc",
|
||||||
|
links: [
|
||||||
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: "About", href: "/about" },
|
||||||
|
{ label: "Contact", href: "/contact" },
|
||||||
|
],
|
||||||
|
variant: "simple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Hero: {
|
||||||
|
props: z.object({
|
||||||
|
headline: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
primaryCta: ctaSchema.nullable(),
|
||||||
|
secondaryCta: ctaSchema.nullable(),
|
||||||
|
badge: z.string().nullable(),
|
||||||
|
variant: z.enum(["centered", "left-aligned"]).nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
"Landing hero section with headline, description, and call-to-action buttons.",
|
||||||
|
example: {
|
||||||
|
headline: "Build something great",
|
||||||
|
description: "A modern platform for modern teams.",
|
||||||
|
primaryCta: { label: "Get Started", href: "/contact" },
|
||||||
|
variant: "centered",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Features: {
|
||||||
|
props: z.object({
|
||||||
|
headline: z.string().nullable(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
columns: z.number().nullable(),
|
||||||
|
variant: z.enum(["cards", "simple"]).nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
"Feature highlights section. Cards: bordered cards per item. Simple: clean text layout.",
|
||||||
|
example: {
|
||||||
|
headline: "Why choose us",
|
||||||
|
items: [
|
||||||
|
{ title: "Fast", description: "Blazing performance." },
|
||||||
|
{ title: "Secure", description: "Enterprise-grade security." },
|
||||||
|
{ title: "Simple", description: "Easy to use." },
|
||||||
|
],
|
||||||
|
columns: 3,
|
||||||
|
variant: "cards",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Team: {
|
||||||
|
props: z.object({
|
||||||
|
headline: z.string().nullable(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
members: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
bio: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
variant: z.enum(["grid", "list"]).nullable(),
|
||||||
|
}),
|
||||||
|
description: "Team members section. Grid: card grid. List: vertical list.",
|
||||||
|
example: {
|
||||||
|
headline: "Our Team",
|
||||||
|
members: [
|
||||||
|
{ name: "Alice", role: "CEO", bio: null },
|
||||||
|
{ name: "Bob", role: "CTO", bio: null },
|
||||||
|
],
|
||||||
|
variant: "grid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Testimonials: {
|
||||||
|
props: z.object({
|
||||||
|
headline: z.string().nullable(),
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
quote: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
role: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
description: "Customer testimonials / quotes section.",
|
||||||
|
example: {
|
||||||
|
headline: "What people say",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quote: "Amazing product!",
|
||||||
|
author: "Jane",
|
||||||
|
role: "CEO at Startup",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ContactForm: {
|
||||||
|
props: z.object({
|
||||||
|
headline: z.string().nullable(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
fields: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
type: z.enum(["text", "email", "textarea"]),
|
||||||
|
placeholder: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
submitLabel: z.string(),
|
||||||
|
}),
|
||||||
|
description: "Contact form section with configurable fields.",
|
||||||
|
example: {
|
||||||
|
headline: "Get in Touch",
|
||||||
|
fields: [
|
||||||
|
{ label: "Name", type: "text", placeholder: "Your name" },
|
||||||
|
{ label: "Email", type: "email", placeholder: "you@example.com" },
|
||||||
|
{ label: "Message", type: "textarea", placeholder: "Your message" },
|
||||||
|
],
|
||||||
|
submitLabel: "Send",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Footer: {
|
||||||
|
props: z.object({
|
||||||
|
brand: z.string().nullable(),
|
||||||
|
links: z.array(linkSchema).nullable(),
|
||||||
|
copyright: z.string().nullable(),
|
||||||
|
variant: z.enum(["simple", "columns"]).nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
"Site footer. Simple: centered single row. Columns: multi-column layout.",
|
||||||
|
example: {
|
||||||
|
brand: "Acme Inc",
|
||||||
|
copyright: "2026 Acme Inc.",
|
||||||
|
variant: "simple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
CTA: {
|
||||||
|
props: z.object({
|
||||||
|
headline: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
buttonLabel: z.string(),
|
||||||
|
buttonHref: z.string(),
|
||||||
|
variant: z.enum(["banner", "centered"]).nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
"Call-to-action section. Banner: full-width colored background. Centered: clean centered layout.",
|
||||||
|
example: {
|
||||||
|
headline: "Ready to get started?",
|
||||||
|
buttonLabel: "Contact Us",
|
||||||
|
buttonHref: "/contact",
|
||||||
|
variant: "centered",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebsiteProps<K extends keyof typeof websiteComponentDefinitions> =
|
||||||
|
z.output<(typeof websiteComponentDefinitions)[K]["props"]>;
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { BaseComponentProps } from "@json-render/react";
|
||||||
|
import type { WebsiteProps } from "./website-catalog";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Header
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function HeaderComponent({
|
||||||
|
props,
|
||||||
|
}: BaseComponentProps<WebsiteProps<"Header">>) {
|
||||||
|
const variant = props.variant ?? "simple";
|
||||||
|
const links = props.links ?? [];
|
||||||
|
|
||||||
|
if (variant === "centered") {
|
||||||
|
return (
|
||||||
|
<header className="border-b border-border bg-background">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 py-6 flex flex-col items-center gap-4">
|
||||||
|
<span className="text-xl font-bold tracking-tight">
|
||||||
|
{props.brand}
|
||||||
|
</span>
|
||||||
|
<nav className="flex items-center gap-6">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="border-b border-border bg-background sticky top-0 z-50">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 h-16 flex items-center justify-between">
|
||||||
|
<span className="text-lg font-bold tracking-tight">{props.brand}</span>
|
||||||
|
<nav className="flex items-center gap-6">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Hero
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function HeroComponent({ props }: BaseComponentProps<WebsiteProps<"Hero">>) {
|
||||||
|
const variant = props.variant ?? "centered";
|
||||||
|
const isLeft = variant === "left-aligned";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-background">
|
||||||
|
<div
|
||||||
|
className={`mx-auto max-w-4xl px-6 py-24 md:py-32 ${isLeft ? "text-left" : "text-center"}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col gap-6 ${isLeft ? "items-start" : "items-center"}`}
|
||||||
|
>
|
||||||
|
{props.badge && (
|
||||||
|
<span className="inline-flex items-center rounded-full border border-border bg-muted px-3 py-1 text-xs font-medium text-muted-foreground">
|
||||||
|
{props.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl text-foreground">
|
||||||
|
{props.headline}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className={`text-lg text-muted-foreground ${isLeft ? "max-w-xl" : "max-w-2xl"}`}
|
||||||
|
>
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
|
{(props.primaryCta || props.secondaryCta) && (
|
||||||
|
<div className="flex flex-wrap gap-3 pt-2">
|
||||||
|
{props.primaryCta && (
|
||||||
|
<a
|
||||||
|
href={props.primaryCta.href}
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
{props.primaryCta.label}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{props.secondaryCta && (
|
||||||
|
<a
|
||||||
|
href={props.secondaryCta.href}
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-md border border-border bg-background px-6 text-sm font-medium text-foreground hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
{props.secondaryCta.label}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Features
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function FeaturesComponent({
|
||||||
|
props,
|
||||||
|
}: BaseComponentProps<WebsiteProps<"Features">>) {
|
||||||
|
const variant = props.variant ?? "cards";
|
||||||
|
const columns = props.columns ?? 3;
|
||||||
|
const items = props.items ?? [];
|
||||||
|
|
||||||
|
const colClass =
|
||||||
|
columns === 2
|
||||||
|
? "sm:grid-cols-2"
|
||||||
|
: columns === 4
|
||||||
|
? "sm:grid-cols-2 lg:grid-cols-4"
|
||||||
|
: "sm:grid-cols-2 lg:grid-cols-3";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-background">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 py-20">
|
||||||
|
{(props.headline || props.description) && (
|
||||||
|
<div className="mb-12 text-center">
|
||||||
|
{props.headline && (
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
||||||
|
{props.headline}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{props.description && (
|
||||||
|
<p className="mt-3 text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`grid grid-cols-1 gap-6 ${colClass}`}>
|
||||||
|
{items.map((item, i) =>
|
||||||
|
variant === "cards" ? (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="rounded-lg border border-border bg-card p-6 shadow-sm"
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-semibold text-card-foreground">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div key={i} className="py-2">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Team
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function TeamComponent({ props }: BaseComponentProps<WebsiteProps<"Team">>) {
|
||||||
|
const variant = props.variant ?? "grid";
|
||||||
|
const members = props.members ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-background">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 py-20">
|
||||||
|
{(props.headline || props.description) && (
|
||||||
|
<div className="mb-12 text-center">
|
||||||
|
{props.headline && (
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
||||||
|
{props.headline}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{props.description && (
|
||||||
|
<p className="mt-3 text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{variant === "list" ? (
|
||||||
|
<div className="space-y-6 max-w-2xl mx-auto">
|
||||||
|
{members.map((m, i) => (
|
||||||
|
<div key={i} className="flex flex-col gap-1">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">
|
||||||
|
{m.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{m.role}</p>
|
||||||
|
{m.bio && (
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">{m.bio}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{members.map((m, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="rounded-lg border border-border bg-card p-6 text-center"
|
||||||
|
>
|
||||||
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted text-lg font-bold text-muted-foreground">
|
||||||
|
{m.name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.slice(0, 2)
|
||||||
|
.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-base font-semibold text-card-foreground">
|
||||||
|
{m.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{m.role}</p>
|
||||||
|
{m.bio && (
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">{m.bio}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Testimonials
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function TestimonialsComponent({
|
||||||
|
props,
|
||||||
|
}: BaseComponentProps<WebsiteProps<"Testimonials">>) {
|
||||||
|
const items = props.items ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-muted/30">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 py-20">
|
||||||
|
{props.headline && (
|
||||||
|
<h2 className="mb-12 text-center text-3xl font-bold tracking-tight text-foreground">
|
||||||
|
{props.headline}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<blockquote
|
||||||
|
key={i}
|
||||||
|
className="rounded-lg border border-border bg-background p-6"
|
||||||
|
>
|
||||||
|
<p className="text-sm text-foreground leading-relaxed">
|
||||||
|
“{item.quote}”
|
||||||
|
</p>
|
||||||
|
<footer className="mt-4 flex flex-col">
|
||||||
|
<span className="text-sm font-semibold text-foreground">
|
||||||
|
{item.author}
|
||||||
|
</span>
|
||||||
|
{item.role && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{item.role}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
</blockquote>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ContactForm
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function ContactFormComponent({
|
||||||
|
props,
|
||||||
|
}: BaseComponentProps<WebsiteProps<"ContactForm">>) {
|
||||||
|
const fields = props.fields ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-background">
|
||||||
|
<div className="mx-auto max-w-xl px-6 py-20">
|
||||||
|
{(props.headline || props.description) && (
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
{props.headline && (
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
||||||
|
{props.headline}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{props.description && (
|
||||||
|
<p className="mt-3 text-muted-foreground">{props.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form
|
||||||
|
className="rounded-lg border border-border bg-card p-6 shadow-sm"
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{fields.map((field, i) =>
|
||||||
|
field.type === "textarea" ? (
|
||||||
|
<div key={i} className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-sm font-medium text-foreground">
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
placeholder={field.placeholder ?? ""}
|
||||||
|
rows={4}
|
||||||
|
className="rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div key={i} className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-sm font-medium text-foreground">
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={field.type}
|
||||||
|
placeholder={field.placeholder ?? ""}
|
||||||
|
className="h-10 rounded-md border border-input bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="mt-2 h-11 rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
{props.submitLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Footer
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function FooterComponent({
|
||||||
|
props,
|
||||||
|
}: BaseComponentProps<WebsiteProps<"Footer">>) {
|
||||||
|
const variant = props.variant ?? "simple";
|
||||||
|
const links = props.links ?? [];
|
||||||
|
|
||||||
|
if (variant === "columns") {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-border bg-muted/30">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 py-12">
|
||||||
|
<div className="flex flex-col gap-8 sm:flex-row sm:justify-between">
|
||||||
|
<div>
|
||||||
|
{props.brand && (
|
||||||
|
<span className="text-lg font-bold text-foreground">
|
||||||
|
{props.brand}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{links.length > 0 && (
|
||||||
|
<nav className="flex flex-col gap-2 sm:flex-row sm:gap-6">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{props.copyright && (
|
||||||
|
<p className="mt-8 text-xs text-muted-foreground">
|
||||||
|
{props.copyright}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-border bg-muted/30">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 py-10 flex flex-col items-center gap-4">
|
||||||
|
{props.brand && (
|
||||||
|
<span className="text-sm font-semibold text-foreground">
|
||||||
|
{props.brand}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{links.length > 0 && (
|
||||||
|
<nav className="flex items-center gap-4">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
{props.copyright && (
|
||||||
|
<p className="text-xs text-muted-foreground">{props.copyright}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CTA
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function CTAComponent({ props }: BaseComponentProps<WebsiteProps<"CTA">>) {
|
||||||
|
const variant = props.variant ?? "centered";
|
||||||
|
|
||||||
|
if (variant === "banner") {
|
||||||
|
return (
|
||||||
|
<section className="bg-primary text-primary-foreground">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 py-16 flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold">{props.headline}</h2>
|
||||||
|
{props.description && (
|
||||||
|
<p className="mt-1 text-sm opacity-90">{props.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={props.buttonHref}
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-md bg-background px-6 text-sm font-medium text-foreground shadow-sm hover:bg-background/90 transition-colors shrink-0"
|
||||||
|
>
|
||||||
|
{props.buttonLabel}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-muted/30">
|
||||||
|
<div className="mx-auto max-w-3xl px-6 py-20 text-center">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
||||||
|
{props.headline}
|
||||||
|
</h2>
|
||||||
|
{props.description && (
|
||||||
|
<p className="mt-3 text-lg text-muted-foreground">
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-8">
|
||||||
|
<a
|
||||||
|
href={props.buttonHref}
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-8 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
{props.buttonLabel}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Export map
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const websiteComponents = {
|
||||||
|
Header: HeaderComponent,
|
||||||
|
Hero: HeroComponent,
|
||||||
|
Features: FeaturesComponent,
|
||||||
|
Team: TeamComponent,
|
||||||
|
Testimonials: TestimonialsComponent,
|
||||||
|
ContactForm: ContactFormComponent,
|
||||||
|
Footer: FooterComponent,
|
||||||
|
CTA: CTAComponent,
|
||||||
|
};
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
allowedDevOrigins: ["next-website-builder-demo.json-render.localhost"],
|
||||||
|
transpilePackages: [
|
||||||
|
"@json-render/core",
|
||||||
|
"@json-render/react",
|
||||||
|
"@json-render/shadcn",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "example-next-website-builder",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"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 next-website-builder-demo.json-render next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"check-types": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@json-render/react": "workspace:*",
|
||||||
|
"@json-render/next": "workspace:*",
|
||||||
|
"@json-render/shadcn": "workspace:*",
|
||||||
|
"@visual-json/react": "0.3.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"geist": "^1.7.0",
|
||||||
|
"lucide-react": "^1.6.0",
|
||||||
|
"next": "16.2.1",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"react-resizable-panels": "^4.7.6",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"zod": "4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@internal/typescript-config": "workspace:*",
|
||||||
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
|
"@types/node": "^22.10.0",
|
||||||
|
"@types/react": "19.2.3",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../packages/typescript-config/nextjs.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-no-ai
|
# example-no-ai
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
- @json-render/shadcn@0.16.0
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
- @json-render/shadcn@0.15.0
|
||||||
|
|
||||||
## 0.1.9
|
## 0.1.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-no-ai",
|
"name": "example-no-ai",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-email
|
# example-react-email
|
||||||
|
|
||||||
|
## 0.1.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react-email@0.16.0
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react-email@0.15.0
|
||||||
|
|
||||||
## 0.1.5
|
## 0.1.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-email",
|
"name": "example-react-email",
|
||||||
"version": "0.1.5",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-native
|
# example-react-native
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react-native@0.16.0
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react-native@0.15.0
|
||||||
|
|
||||||
## 0.1.9
|
## 0.1.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-native",
|
"name": "example-react-native",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-pdf
|
# example-react-pdf
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react-pdf@0.16.0
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react-pdf@0.15.0
|
||||||
|
|
||||||
## 0.1.9
|
## 0.1.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-pdf",
|
"name": "example-react-pdf",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-react-three-fiber
|
# example-react-three-fiber
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
- @json-render/react-three-fiber@0.16.0
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
- @json-render/react-three-fiber@0.15.0
|
||||||
|
|
||||||
## 0.1.9
|
## 0.1.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-three-fiber",
|
"name": "example-react-three-fiber",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-remotion
|
# example-remotion
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/remotion@0.16.0
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/remotion@0.15.0
|
||||||
|
|
||||||
## 0.1.9
|
## 0.1.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-remotion",
|
"name": "example-remotion",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-solid
|
# example-solid
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/solid@0.16.0
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/solid@0.15.0
|
||||||
|
|
||||||
## 0.1.3
|
## 0.1.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-solid",
|
"name": "example-solid",
|
||||||
"version": "0.1.3",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# com.example.json-render-demo
|
# com.example.json-render-demo
|
||||||
|
|
||||||
|
## 0.0.12
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
|
||||||
|
## 0.0.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
|
||||||
## 0.0.10
|
## 0.0.10
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-demo",
|
"name": "com.example.json-render-demo",
|
||||||
"version": "0.0.10",
|
"version": "0.0.12",
|
||||||
"description": "Test",
|
"description": "Test",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# com.example.json-render-fullpage-demo
|
# com.example.json-render-fullpage-demo
|
||||||
|
|
||||||
|
## 0.0.12
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
|
||||||
|
## 0.0.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
|
||||||
## 0.0.10
|
## 0.0.10
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-fullpage-demo",
|
"name": "com.example.json-render-fullpage-demo",
|
||||||
"version": "0.0.10",
|
"version": "0.0.12",
|
||||||
"description": "Full-page Stripe App example (alpha)",
|
"description": "Full-page Stripe App example (alpha)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# svelte-chat
|
# svelte-chat
|
||||||
|
|
||||||
|
## 0.0.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/svelte@0.16.0
|
||||||
|
|
||||||
|
## 0.0.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/svelte@0.15.0
|
||||||
|
|
||||||
## 0.0.6
|
## 0.0.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "svelte-chat",
|
"name": "svelte-chat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.6",
|
"version": "0.0.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-svelte
|
# example-svelte
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/svelte@0.16.0
|
||||||
|
|
||||||
|
## 0.1.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/svelte@0.15.0
|
||||||
|
|
||||||
## 0.1.6
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-svelte",
|
"name": "example-svelte",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
# vite-renderers
|
# vite-renderers
|
||||||
|
|
||||||
|
## 0.1.9
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
|
- @json-render/solid@0.16.0
|
||||||
|
- @json-render/svelte@0.16.0
|
||||||
|
- @json-render/vue@0.16.0
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/react@0.15.0
|
||||||
|
- @json-render/solid@0.15.0
|
||||||
|
- @json-render/svelte@0.15.0
|
||||||
|
- @json-render/vue@0.15.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-renderers",
|
"name": "vite-renderers",
|
||||||
"version": "0.1.7",
|
"version": "0.1.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-vue
|
# example-vue
|
||||||
|
|
||||||
|
## 0.1.9
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/vue@0.16.0
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
- @json-render/vue@0.15.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-vue",
|
"name": "example-vue",
|
||||||
"version": "0.1.7",
|
"version": "0.1.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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)",
|
"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)",
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/codegen
|
# @json-render/codegen
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
|
||||||
## 0.14.1
|
## 0.14.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/codegen",
|
"name": "@json-render/codegen",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Utilities for generating code from json-render UI trees",
|
"description": "Utilities for generating code from json-render UI trees",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
# @json-render/core
|
# @json-render/core
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 519a538: Add Next.js renderer and shadcn-svelte component library.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/next`** -- Next.js renderer that turns JSON specs into full Next.js applications with routes, layouts, SSR, metadata, data loaders, and static generation. Client and server entry points at `@json-render/next` and `@json-render/next/server`. Includes built-in `Link`, `Slot`, error boundary, loading, and not-found components.
|
||||||
|
- **`@json-render/shadcn-svelte`** -- Pre-built shadcn-svelte components for json-render Svelte apps. 36 components built on Svelte 5 + Tailwind CSS with state binding, validation, and action support. Server-safe catalog at `@json-render/shadcn-svelte/catalog`.
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- bf3a7ec: Add Ink terminal renderer for interactive terminal UIs.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/ink`** -- Terminal UI renderer for json-render, built on Ink. Includes 20+ standard components (Box, Text, Heading, Card, Table, TextInput, Select, MultiSelect, Tabs, etc.), action/validation/focus contexts, two-way state binding, and streaming via `useUIStream`. Server-safe entry points at `@json-render/ink/schema`, `@json-render/ink/catalog`, and `@json-render/ink/server`.
|
||||||
|
|
||||||
|
### Improved:
|
||||||
|
- **Examples** -- new `ink-chat` terminal chat demo, `game-engine` 3D example using react-three-fiber, website examples page with live demos and search.
|
||||||
|
|
||||||
## 0.14.1
|
## 0.14.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/core",
|
"name": "@json-render/core",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/image
|
# @json-render/image
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
|
||||||
## 0.14.1
|
## 0.14.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/image",
|
"name": "@json-render/image",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# @json-render/ink
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- bf3a7ec: Add Ink terminal renderer for interactive terminal UIs.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/ink`** -- Terminal UI renderer for json-render, built on Ink. Includes 20+ standard components (Box, Text, Heading, Card, Table, TextInput, Select, MultiSelect, Tabs, etc.), action/validation/focus contexts, two-way state binding, and streaming via `useUIStream`. Server-safe entry points at `@json-render/ink/schema`, `@json-render/ink/catalog`, and `@json-render/ink/server`.
|
||||||
|
|
||||||
|
### Improved:
|
||||||
|
- **Examples** -- new `ink-chat` terminal chat demo, `game-engine` 3D example using react-three-fiber, website examples page with live demos and search.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/ink",
|
"name": "@json-render/ink",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Ink terminal renderer for @json-render/core. JSON becomes terminal UIs.",
|
"description": "Ink terminal renderer for @json-render/core. JSON becomes terminal UIs.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -57,7 +57,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@json-render/core": "workspace:*",
|
"@json-render/core": "workspace:*",
|
||||||
|
|||||||
+77
-11
@@ -259,11 +259,12 @@ export const standardComponentDefinitions = {
|
|||||||
borderStyle: z
|
borderStyle: z
|
||||||
.enum(["single", "double", "round", "bold", "classic"])
|
.enum(["single", "double", "round", "bold", "classic"])
|
||||||
.nullable(),
|
.nullable(),
|
||||||
|
backgroundColor: z.string().nullable(),
|
||||||
headerColor: z.string().nullable(),
|
headerColor: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Tabular data display with headers and rows. Each row is a record mapping column keys to string values.",
|
"Tabular data display with headers and rows. Each row is a record mapping column keys to string values. Set both borderStyle and backgroundColor together so borders share the same shading.",
|
||||||
example: {
|
example: {
|
||||||
columns: [
|
columns: [
|
||||||
{ header: "Name", key: "name", width: 20 },
|
{ header: "Name", key: "name", width: 20 },
|
||||||
@@ -306,7 +307,7 @@ export const standardComponentDefinitions = {
|
|||||||
example: {
|
example: {
|
||||||
title: "package.json",
|
title: "package.json",
|
||||||
subtitle: "Modified 2 hours ago",
|
subtitle: "Modified 2 hours ago",
|
||||||
leading: "📄",
|
leading: "*",
|
||||||
trailing: "2.1 KB",
|
trailing: "2.1 KB",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -314,16 +315,13 @@ export const standardComponentDefinitions = {
|
|||||||
Card: {
|
Card: {
|
||||||
props: z.object({
|
props: z.object({
|
||||||
title: z.string().nullable(),
|
title: z.string().nullable(),
|
||||||
borderStyle: z
|
backgroundColor: z.string().nullable(),
|
||||||
.enum(["single", "double", "round", "bold", "classic"])
|
|
||||||
.nullable(),
|
|
||||||
borderColor: z.string().nullable(),
|
|
||||||
padding: z.number().nullable(),
|
padding: z.number().nullable(),
|
||||||
}),
|
}),
|
||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description:
|
description:
|
||||||
"Bordered container with optional title. Use for grouping related content with a visual boundary.",
|
"Shaded container with optional title. Renders as a filled background area for grouping related content. Default background is a subtle dark shade.",
|
||||||
example: { title: "Details", borderStyle: "round", padding: 1 },
|
example: { title: "Details", padding: 1 },
|
||||||
},
|
},
|
||||||
|
|
||||||
KeyValue: {
|
KeyValue: {
|
||||||
@@ -363,6 +361,74 @@ export const standardComponentDefinitions = {
|
|||||||
example: { text: "Build completed successfully", status: "success" },
|
example: { text: "Build completed successfully", status: "success" },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Metric: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
detail: z.string().nullable(),
|
||||||
|
trend: z.enum(["up", "down", "neutral"]).nullable(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description:
|
||||||
|
"Key metric display with prominent value and optional trend indicator. Use for important numbers that deserve visual emphasis (price, temperature, stars, market cap).",
|
||||||
|
example: {
|
||||||
|
label: "Price",
|
||||||
|
value: "$70,686",
|
||||||
|
detail: "24h change",
|
||||||
|
trend: "up",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callout: {
|
||||||
|
props: z.object({
|
||||||
|
type: z.enum(["info", "tip", "warning", "important"]).nullable(),
|
||||||
|
title: z.string().nullable(),
|
||||||
|
content: z.string(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description:
|
||||||
|
"Highlighted callout block with colored left border. Use for key takeaways, tips, warnings, or important notes that should stand out from surrounding content.",
|
||||||
|
example: {
|
||||||
|
type: "tip",
|
||||||
|
title: "Key Takeaway",
|
||||||
|
content:
|
||||||
|
"Revenue peaked in FY2022 driven by iPhone 13/14 upgrade cycles.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Timeline: {
|
||||||
|
props: z.object({
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
date: z.string().nullable(),
|
||||||
|
status: z.enum(["completed", "current", "upcoming"]).nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description:
|
||||||
|
"Vertical timeline showing ordered events, steps, or milestones. Each item has a status-colored dot, title, optional date, and optional description.",
|
||||||
|
example: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Project Started",
|
||||||
|
description: "Initial commit and setup",
|
||||||
|
date: "Jan 2024",
|
||||||
|
status: "completed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Beta Release",
|
||||||
|
description: "Public beta launched",
|
||||||
|
date: "Mar 2024",
|
||||||
|
status: "current",
|
||||||
|
},
|
||||||
|
{ title: "v1.0", date: "Q2 2024", status: "upcoming" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Interactive Components
|
// Interactive Components
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -468,9 +534,9 @@ export const standardComponentDefinitions = {
|
|||||||
"Tab bar navigation. Navigate with left/right arrow keys. Use $bindState on value to bind the active tab to state. Place child content inside and use visible conditions on children to show content for the active tab.",
|
"Tab bar navigation. Navigate with left/right arrow keys. Use $bindState on value to bind the active tab to state. Place child content inside and use visible conditions on children to show content for the active tab.",
|
||||||
example: {
|
example: {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ label: "Overview", value: "overview", icon: "📊" },
|
{ label: "Overview", value: "overview" },
|
||||||
{ label: "Logs", value: "logs", icon: "📋" },
|
{ label: "Logs", value: "logs" },
|
||||||
{ label: "Settings", value: "settings", icon: "⚙" },
|
{ label: "Settings", value: "settings" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ function TableComponent({ element }: ComponentRenderProps) {
|
|||||||
}>;
|
}>;
|
||||||
rows?: Array<Record<string, string>>;
|
rows?: Array<Record<string, string>>;
|
||||||
borderStyle?: string;
|
borderStyle?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
headerColor?: string;
|
headerColor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -363,7 +364,12 @@ function TableComponent({ element }: ComponentRenderProps) {
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" borderStyle={borderStyleProp}>
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
alignSelf="flex-start"
|
||||||
|
borderStyle={borderStyleProp}
|
||||||
|
backgroundColor={p.backgroundColor}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box>
|
<Box>
|
||||||
{columns.map((col, i) => (
|
{columns.map((col, i) => (
|
||||||
@@ -375,15 +381,19 @@ function TableComponent({ element }: ComponentRenderProps) {
|
|||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
<Text dimColor>{colWidths.map((w) => "─".repeat(w)).join("─")}</Text>
|
<Text dimColor>{colWidths.map((w) => "─".repeat(w)).join("─")}</Text>
|
||||||
{/* Rows */}
|
{/* Rows */}
|
||||||
{rows.map((row, rowIdx) => (
|
{rows.map((row, rowIdx) => {
|
||||||
<Box key={rowIdx}>
|
const rowKey =
|
||||||
{columns.map((col, i) => (
|
columns.map((c) => row[c.key] ?? "").join("\0") || String(rowIdx);
|
||||||
<Text key={col.key}>
|
return (
|
||||||
{padCell(row[col.key] || "—", colWidths[i]!, col.align)}
|
<Box key={rowKey} gap={0}>
|
||||||
</Text>
|
{columns.map((col, i) => (
|
||||||
))}
|
<Text key={col.key}>
|
||||||
</Box>
|
{padCell(row[col.key] || "—", colWidths[i]!, col.align)}
|
||||||
))}
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -402,7 +412,7 @@ function ListComponent({ element }: ComponentRenderProps) {
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column" gap={p.spacing ?? 0}>
|
<Box flexDirection="column" gap={p.spacing ?? 0}>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<Box key={i} gap={1}>
|
<Box key={`${i}:${item}`} gap={1}>
|
||||||
<Text dimColor>{p.ordered ? `${i + 1}.` : bullet}</Text>
|
<Text dimColor>{p.ordered ? `${i + 1}.` : bullet}</Text>
|
||||||
<Text>{item}</Text>
|
<Text>{item}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -434,16 +444,15 @@ function ListItemComponent({ element }: ComponentRenderProps) {
|
|||||||
function CardComponent({ element, children }: ComponentRenderProps) {
|
function CardComponent({ element, children }: ComponentRenderProps) {
|
||||||
const p = element.props as {
|
const p = element.props as {
|
||||||
title?: string;
|
title?: string;
|
||||||
borderStyle?: "single" | "double" | "round" | "bold" | "classic";
|
backgroundColor?: string;
|
||||||
borderColor?: string;
|
|
||||||
padding?: number;
|
padding?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
borderStyle={p.borderStyle ?? "round"}
|
alignSelf="flex-start"
|
||||||
borderColor={p.borderColor}
|
backgroundColor={p.backgroundColor ?? "#1a1a1a"}
|
||||||
padding={p.padding ?? 1}
|
padding={p.padding ?? 1}
|
||||||
>
|
>
|
||||||
{p.title ? (
|
{p.title ? (
|
||||||
@@ -537,10 +546,136 @@ function StatusLineComponent({ element }: ComponentRenderProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Metric, Callout, Timeline
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const TREND_CONFIG: Record<string, { prefix: string; color: string }> = {
|
||||||
|
up: { prefix: "+", color: "green" },
|
||||||
|
down: { prefix: "", color: "red" },
|
||||||
|
neutral: { prefix: "~", color: "gray" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function MetricComponent({ element }: ComponentRenderProps) {
|
||||||
|
const p = element.props as {
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
detail?: string;
|
||||||
|
trend?: "up" | "down" | "neutral";
|
||||||
|
};
|
||||||
|
|
||||||
|
const trend = p.trend ? TREND_CONFIG[p.trend] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text dimColor>{p.label ?? ""}</Text>
|
||||||
|
<Box gap={1}>
|
||||||
|
<Text bold>{p.value ?? ""}</Text>
|
||||||
|
{trend && p.detail ? (
|
||||||
|
<Text color={trend.color}>
|
||||||
|
{trend.prefix}
|
||||||
|
{p.detail}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
{!trend && p.detail ? <Text dimColor>{p.detail}</Text> : null}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CALLOUT_CONFIG: Record<string, { label: string; color: string }> = {
|
||||||
|
info: { label: "INFO", color: "blue" },
|
||||||
|
tip: { label: "TIP", color: "green" },
|
||||||
|
warning: { label: "WARNING", color: "yellow" },
|
||||||
|
important: { label: "IMPORTANT", color: "magenta" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function CalloutComponent({ element }: ComponentRenderProps) {
|
||||||
|
const p = element.props as {
|
||||||
|
type?: "info" | "tip" | "warning" | "important";
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = CALLOUT_CONFIG[p.type ?? "info"] ?? CALLOUT_CONFIG.info!;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
borderStyle="bold"
|
||||||
|
borderLeft
|
||||||
|
borderRight={false}
|
||||||
|
borderTop={false}
|
||||||
|
borderBottom={false}
|
||||||
|
borderColor={config.color}
|
||||||
|
paddingLeft={1}
|
||||||
|
>
|
||||||
|
{p.title ? (
|
||||||
|
<Text bold color={config.color}>
|
||||||
|
{p.title}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text>{p.content ?? ""}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMELINE_DOT: Record<string, { dot: string; color: string }> = {
|
||||||
|
completed: { dot: "●", color: "green" },
|
||||||
|
current: { dot: "◆", color: "cyan" },
|
||||||
|
upcoming: { dot: "○", color: "gray" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function TimelineComponent({ element }: ComponentRenderProps) {
|
||||||
|
const p = element.props as {
|
||||||
|
items?: Array<{
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
date?: string;
|
||||||
|
status?: "completed" | "current" | "upcoming";
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = p.items ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
{items.map((item, i) => {
|
||||||
|
const cfg =
|
||||||
|
TIMELINE_DOT[item.status ?? "upcoming"] ?? TIMELINE_DOT.upcoming!;
|
||||||
|
return (
|
||||||
|
<Box key={i} flexDirection="column">
|
||||||
|
<Box gap={1}>
|
||||||
|
<Text color={cfg.color}>{cfg.dot}</Text>
|
||||||
|
<Text bold>{item.title ?? ""}</Text>
|
||||||
|
{item.date ? <Text dimColor>{item.date}</Text> : null}
|
||||||
|
</Box>
|
||||||
|
{item.description ? (
|
||||||
|
<Box paddingLeft={2}>
|
||||||
|
<Text dimColor>{item.description}</Text>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Interactive Components
|
// Interactive Components
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
/** Count code points instead of UTF-16 code units (handles emoji/surrogate pairs). */
|
||||||
|
function codePointLength(s: string): number {
|
||||||
|
return Array.from(s).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slice by code point index instead of UTF-16 offset. */
|
||||||
|
function codePointSlice(s: string, start: number, end?: number): string {
|
||||||
|
return Array.from(s).slice(start, end).join("");
|
||||||
|
}
|
||||||
|
|
||||||
function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
||||||
const p = element.props as {
|
const p = element.props as {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -556,11 +691,11 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const currentValue = value ?? "";
|
const currentValue = value ?? "";
|
||||||
const [cursorPos, setCursorPos] = useState(currentValue.length);
|
const [cursorPos, setCursorPos] = useState(codePointLength(currentValue));
|
||||||
|
|
||||||
// Keep cursor in bounds when value changes externally
|
// Keep cursor in bounds when value changes externally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCursorPos((prev) => Math.min(prev, currentValue.length));
|
setCursorPos((prev) => Math.min(prev, codePointLength(currentValue)));
|
||||||
}, [currentValue]);
|
}, [currentValue]);
|
||||||
|
|
||||||
useInput(
|
useInput(
|
||||||
@@ -573,8 +708,8 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
|||||||
if (key.backspace || key.delete) {
|
if (key.backspace || key.delete) {
|
||||||
if (cursorPos > 0) {
|
if (cursorPos > 0) {
|
||||||
const newVal =
|
const newVal =
|
||||||
currentValue.slice(0, cursorPos - 1) +
|
codePointSlice(currentValue, 0, cursorPos - 1) +
|
||||||
currentValue.slice(cursorPos);
|
codePointSlice(currentValue, cursorPos);
|
||||||
setValue(newVal);
|
setValue(newVal);
|
||||||
setCursorPos((prev) => prev - 1);
|
setCursorPos((prev) => prev - 1);
|
||||||
emit("change");
|
emit("change");
|
||||||
@@ -588,7 +723,9 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.rightArrow) {
|
if (key.rightArrow) {
|
||||||
setCursorPos((prev) => Math.min(currentValue.length, prev + 1));
|
setCursorPos((prev) =>
|
||||||
|
Math.min(codePointLength(currentValue), prev + 1),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,11 +735,11 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
|||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
const newVal =
|
const newVal =
|
||||||
currentValue.slice(0, cursorPos) +
|
codePointSlice(currentValue, 0, cursorPos) +
|
||||||
input +
|
input +
|
||||||
currentValue.slice(cursorPos);
|
codePointSlice(currentValue, cursorPos);
|
||||||
setValue(newVal);
|
setValue(newVal);
|
||||||
setCursorPos((prev) => prev + input.length);
|
setCursorPos((prev) => prev + codePointLength(input));
|
||||||
emit("change");
|
emit("change");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -611,13 +748,12 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
|||||||
|
|
||||||
const displayValue = currentValue
|
const displayValue = currentValue
|
||||||
? p.mask
|
? p.mask
|
||||||
? p.mask.repeat(currentValue.length)
|
? p.mask.repeat(codePointLength(currentValue))
|
||||||
: currentValue
|
: currentValue
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// Render with cursor position indicator
|
const before = codePointSlice(displayValue, 0, cursorPos);
|
||||||
const before = displayValue.slice(0, cursorPos);
|
const after = codePointSlice(displayValue, cursorPos);
|
||||||
const after = displayValue.slice(cursorPos);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box gap={1}>
|
<Box gap={1}>
|
||||||
@@ -1178,6 +1314,9 @@ export const standardComponents: ComponentRegistry = {
|
|||||||
KeyValue: KeyValueComponent,
|
KeyValue: KeyValueComponent,
|
||||||
Link: LinkComponent,
|
Link: LinkComponent,
|
||||||
StatusLine: StatusLineComponent,
|
StatusLine: StatusLineComponent,
|
||||||
|
Metric: MetricComponent,
|
||||||
|
Callout: CalloutComponent,
|
||||||
|
Timeline: TimelineComponent,
|
||||||
TextInput: TextInputComponent,
|
TextInput: TextInputComponent,
|
||||||
Select: SelectComponent,
|
Select: SelectComponent,
|
||||||
MultiSelect: MultiSelectComponent,
|
MultiSelect: MultiSelectComponent,
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ export interface PendingConfirmation {
|
|||||||
export interface ActionContextValue {
|
export interface ActionContextValue {
|
||||||
/** Registered action handlers */
|
/** Registered action handlers */
|
||||||
handlers: Record<string, ActionHandler>;
|
handlers: Record<string, ActionHandler>;
|
||||||
/** Actions currently executing (for loading indicators) */
|
/** Actions currently executing (count of in-flight executions per action name) */
|
||||||
loadingActions: Set<string>;
|
loadingActions: Map<string, number>;
|
||||||
/** Pending confirmation dialog */
|
/** Pending confirmation dialog */
|
||||||
pendingConfirmation: PendingConfirmation | null;
|
pendingConfirmation: PendingConfirmation | null;
|
||||||
/** Execute an action binding */
|
/** Execute an action binding */
|
||||||
@@ -141,7 +141,9 @@ export function ActionProvider({
|
|||||||
initialHandlersRef.current = initialHandlers;
|
initialHandlersRef.current = initialHandlers;
|
||||||
const navigateRef = useRef(navigate);
|
const navigateRef = useRef(navigate);
|
||||||
navigateRef.current = navigate;
|
navigateRef.current = navigate;
|
||||||
const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());
|
const [loadingActions, setLoadingActions] = useState<Map<string, number>>(
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
const [pendingConfirmation, setPendingConfirmation] =
|
const [pendingConfirmation, setPendingConfirmation] =
|
||||||
useState<PendingConfirmation | null>(null);
|
useState<PendingConfirmation | null>(null);
|
||||||
// Ref tracks current pending confirmation so overlapping confirms can
|
// Ref tracks current pending confirmation so overlapping confirms can
|
||||||
@@ -267,7 +269,11 @@ export function ActionProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actionName = resolved.action;
|
const actionName = resolved.action;
|
||||||
setLoadingActions((prev) => new Set(prev).add(actionName));
|
setLoadingActions((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(actionName, (next.get(actionName) ?? 0) + 1);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await executeAction({
|
await executeAction({
|
||||||
action: resolved,
|
action: resolved,
|
||||||
@@ -281,8 +287,13 @@ export function ActionProvider({
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingActions((prev) => {
|
setLoadingActions((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Map(prev);
|
||||||
next.delete(actionName);
|
const count = (next.get(actionName) ?? 1) - 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
next.delete(actionName);
|
||||||
|
} else {
|
||||||
|
next.set(actionName, count);
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -343,7 +354,7 @@ export function useAction(binding: ActionBinding): {
|
|||||||
} {
|
} {
|
||||||
const { execute, loadingActions } = useActions();
|
const { execute, loadingActions } = useActions();
|
||||||
const executeAction = useCallback(() => execute(binding), [execute, binding]);
|
const executeAction = useCallback(() => execute(binding), [execute, binding]);
|
||||||
const isLoading = loadingActions.has(binding.action);
|
const isLoading = (loadingActions.get(binding.action) ?? 0) > 0;
|
||||||
return { execute: executeAction, isLoading };
|
return { execute: executeAction, isLoading };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ function parsePatchLine(line: string): ParseResult {
|
|||||||
// Recovery: strip trailing extra braces/brackets one at a time
|
// Recovery: strip trailing extra braces/brackets one at a time
|
||||||
// LLMs commonly generate extra closing characters in nested JSON
|
// LLMs commonly generate extra closing characters in nested JSON
|
||||||
let attempt = trimmed;
|
let attempt = trimmed;
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
const last = attempt[attempt.length - 1];
|
const last = attempt[attempt.length - 1];
|
||||||
if (last === "}" || last === "]") {
|
if (last === "}" || last === "]") {
|
||||||
attempt = attempt.slice(0, -1);
|
attempt = attempt.slice(0, -1);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { Text } from "ink";
|
||||||
import type {
|
import type {
|
||||||
UIElement,
|
UIElement,
|
||||||
Spec,
|
Spec,
|
||||||
@@ -268,6 +269,9 @@ const ElementRenderer = React.memo(function ElementRenderer({
|
|||||||
console.warn(
|
console.warn(
|
||||||
`[json-render] Missing element "${childKey}" referenced as child of "${resolvedElement.type}". This element will not render.`,
|
`[json-render] Missing element "${childKey}" referenced as child of "${resolvedElement.type}". This element will not render.`,
|
||||||
);
|
);
|
||||||
|
return (
|
||||||
|
<Text key={childKey} color="red">{`[Missing: ${childKey}]`}</Text>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -346,6 +350,12 @@ function RepeatChildren({
|
|||||||
console.warn(
|
console.warn(
|
||||||
`[json-render] Missing element "${childKey}" referenced as child of "${element.type}" (repeat). This element will not render.`,
|
`[json-render] Missing element "${childKey}" referenced as child of "${element.type}" (repeat). This element will not render.`,
|
||||||
);
|
);
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={`${key}:${childKey}`}
|
||||||
|
color="red"
|
||||||
|
>{`[Missing: ${childKey}]`}</Text>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/jotai
|
# @json-render/jotai
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
|
||||||
## 0.14.1
|
## 0.14.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/jotai",
|
"name": "@json-render/jotai",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Jotai adapter for json-render StateStore",
|
"description": "Jotai adapter for json-render StateStore",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/mcp
|
# @json-render/mcp
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bf3a7ec]
|
||||||
|
- @json-render/core@0.15.0
|
||||||
|
|
||||||
## 0.14.1
|
## 0.14.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/mcp",
|
"name": "@json-render/mcp",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.",
|
"description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# @json-render/next
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 519a538: Add Next.js renderer and shadcn-svelte component library.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/next`** -- Next.js renderer that turns JSON specs into full Next.js applications with routes, layouts, SSR, metadata, data loaders, and static generation. Client and server entry points at `@json-render/next` and `@json-render/next/server`. Includes built-in `Link`, `Slot`, error boundary, loading, and not-found components.
|
||||||
|
- **`@json-render/shadcn-svelte`** -- Pre-built shadcn-svelte components for json-render Svelte apps. 36 components built on Svelte 5 + Tailwind CSS with state binding, validation, and action support. Server-safe catalog at `@json-render/shadcn-svelte/catalog`.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [519a538]
|
||||||
|
- @json-render/core@0.16.0
|
||||||
|
- @json-render/react@0.16.0
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# @json-render/next
|
||||||
|
|
||||||
|
Next.js renderer for [@json-render/core](https://json-render.dev). JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/core @json-render/react @json-render/next
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Define your application spec
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { NextAppSpec } from "@json-render/next";
|
||||||
|
|
||||||
|
const spec: NextAppSpec = {
|
||||||
|
metadata: {
|
||||||
|
title: { default: "My App", template: "%s | My App" },
|
||||||
|
},
|
||||||
|
layouts: {
|
||||||
|
main: {
|
||||||
|
root: "shell",
|
||||||
|
elements: {
|
||||||
|
shell: { type: "Container", props: {}, children: ["nav", "slot"] },
|
||||||
|
nav: { type: "NavBar", props: {}, children: [] },
|
||||||
|
slot: { type: "Slot", props: {}, children: [] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
"/": {
|
||||||
|
layout: "main",
|
||||||
|
metadata: { title: "Home" },
|
||||||
|
page: { root: "hero", elements: { hero: { type: "Card", props: { title: "Welcome" }, children: [] } } },
|
||||||
|
},
|
||||||
|
"/about": {
|
||||||
|
layout: "main",
|
||||||
|
metadata: { title: "About" },
|
||||||
|
page: { root: "content", elements: { content: { type: "Card", props: { title: "About" }, children: [] } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the app
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/app.ts
|
||||||
|
import { createNextApp } from "@json-render/next/server";
|
||||||
|
|
||||||
|
export const { Page, generateMetadata, generateStaticParams } = createNextApp({ spec });
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Wire up Next.js routes
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// app/[[...slug]]/page.tsx
|
||||||
|
export { Page as default, generateMetadata, generateStaticParams } from "@/lib/app";
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// app/[[...slug]]/layout.tsx
|
||||||
|
import { NextAppProvider } from "@json-render/next";
|
||||||
|
import { registry, handlers } from "@/lib/registry";
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<NextAppProvider registry={registry} handlers={handlers}>
|
||||||
|
{children}
|
||||||
|
</NextAppProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Pages as spec** -- Define entire multi-page apps in JSON
|
||||||
|
- **Route matching** -- Dynamic segments (`[slug]`), catch-all (`[...path]`), optional catch-all (`[[...path]]`)
|
||||||
|
- **Nested layouts** -- Reusable layouts with `Slot` component for content injection
|
||||||
|
- **SEO metadata** -- Per-route metadata with title templates, OpenGraph, Twitter cards
|
||||||
|
- **SSR** -- Server-side rendering via Next.js App Router
|
||||||
|
- **Data loaders** -- Server-side async data loading before page render
|
||||||
|
- **Static generation** -- `generateStaticParams` for pre-rendering
|
||||||
|
- **Client navigation** -- Built-in `Link` component wrapping `next/link`
|
||||||
|
- **Error/Loading/NotFound** -- Per-route error boundaries, loading states, and 404 pages
|
||||||
|
- **AI streaming** -- Generate entire apps with JSONL patches via `SpecStream`
|
||||||
|
|
||||||
|
## Entry Points
|
||||||
|
|
||||||
|
| Import | Description |
|
||||||
|
| -------------------------- | ------------------------------------------------ |
|
||||||
|
| `@json-render/next` | Client components (NextAppProvider, PageRenderer) |
|
||||||
|
| `@json-render/next/server` | Server utilities (createNextApp, matchRoute) |
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See the [json-render documentation](https://json-render.dev/docs/api/next) for full API reference.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "@json-render/next",
|
||||||
|
"version": "0.16.0",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"description": "Next.js renderer for @json-render/core. JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.",
|
||||||
|
"keywords": [
|
||||||
|
"json",
|
||||||
|
"ui",
|
||||||
|
"next",
|
||||||
|
"nextjs",
|
||||||
|
"ai",
|
||||||
|
"generative-ui",
|
||||||
|
"llm",
|
||||||
|
"renderer",
|
||||||
|
"streaming",
|
||||||
|
"ssr",
|
||||||
|
"pages",
|
||||||
|
"routing"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
|
"directory": "packages/next"
|
||||||
|
},
|
||||||
|
"homepage": "https://json-render.dev",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./server": {
|
||||||
|
"types": "./dist/server.d.ts",
|
||||||
|
"import": "./dist/server.mjs",
|
||||||
|
"require": "./dist/server.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"check-types": "tsc --noEmit",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@json-render/react": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@internal/typescript-config": "workspace:*",
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"next": "16.2.1",
|
||||||
|
"tsup": "^8.0.2",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": "^16.2.0",
|
||||||
|
"react": "^19.2.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Catalog-aware types for @json-render/next.
|
||||||
|
*
|
||||||
|
* Re-exports the React catalog types since Next.js uses the same
|
||||||
|
* component and action model as @json-render/react.
|
||||||
|
*/
|
||||||
|
export type {
|
||||||
|
EventHandle,
|
||||||
|
BaseComponentProps,
|
||||||
|
SetState,
|
||||||
|
StateModel,
|
||||||
|
ComponentContext,
|
||||||
|
ComponentFn,
|
||||||
|
Components,
|
||||||
|
ActionFn,
|
||||||
|
Actions,
|
||||||
|
} from "@json-render/react";
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import { Renderer, type ComponentRegistry } from "@json-render/react";
|
||||||
|
import { useNextApp } from "./provider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for NextErrorBoundary.
|
||||||
|
*/
|
||||||
|
export interface NextErrorBoundaryProps {
|
||||||
|
/** The error that occurred */
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
/** Function to attempt re-rendering the page */
|
||||||
|
reset: () => void;
|
||||||
|
/** Optional error UI spec from the matched route */
|
||||||
|
errorSpec?: Spec | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error boundary component for json-render Next.js apps.
|
||||||
|
*
|
||||||
|
* If the matched route defines an `error` spec, it renders that.
|
||||||
|
* Otherwise renders a minimal default error UI.
|
||||||
|
*
|
||||||
|
* Must be used in a file with `"use client"` directive (Next.js requirement).
|
||||||
|
*/
|
||||||
|
export function NextErrorBoundary({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
errorSpec,
|
||||||
|
}: NextErrorBoundaryProps) {
|
||||||
|
let registry: ComponentRegistry | undefined;
|
||||||
|
try {
|
||||||
|
const ctx = useNextApp();
|
||||||
|
registry = ctx.registry;
|
||||||
|
} catch {
|
||||||
|
// Provider may not be available in error boundaries
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorSpec && registry) {
|
||||||
|
const augmentedRegistry: ComponentRegistry = {
|
||||||
|
...registry,
|
||||||
|
Slot: () => null,
|
||||||
|
};
|
||||||
|
return <Renderer spec={errorSpec} registry={augmentedRegistry} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||||
|
<h2 style={{ marginBottom: "1rem" }}>Something went wrong</h2>
|
||||||
|
<p style={{ color: "#666", marginBottom: "1.5rem" }}>
|
||||||
|
{error.message || "An unexpected error occurred."}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={reset}
|
||||||
|
style={{
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
background: "#fff",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { type ReactNode } from "react";
|
||||||
|
import NextLink from "next/link";
|
||||||
|
import type { ComponentRenderProps } from "@json-render/react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props expected by the Link component in json-render specs.
|
||||||
|
*/
|
||||||
|
export interface LinkProps {
|
||||||
|
/** The target URL or route path */
|
||||||
|
href: string;
|
||||||
|
/** Whether to replace the current history entry */
|
||||||
|
replace?: boolean;
|
||||||
|
/** Whether to prefetch the linked route */
|
||||||
|
prefetch?: boolean;
|
||||||
|
/** CSS class name */
|
||||||
|
className?: string;
|
||||||
|
/** Inline styles */
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Built-in Link component for json-render Next.js specs.
|
||||||
|
*
|
||||||
|
* Wraps `next/link` for client-side navigation between routes.
|
||||||
|
*
|
||||||
|
* Usage in a spec:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "type": "Link",
|
||||||
|
* "props": { "href": "/about" },
|
||||||
|
* "children": ["link-text"]
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function Link({ element, children }: ComponentRenderProps<LinkProps>) {
|
||||||
|
const { href, replace, prefetch, className, style } =
|
||||||
|
element.props as LinkProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NextLink
|
||||||
|
href={href}
|
||||||
|
replace={replace}
|
||||||
|
prefetch={prefetch}
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NextLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for NextLoading.
|
||||||
|
*/
|
||||||
|
export interface NextLoadingProps {
|
||||||
|
/** Optional loading UI spec */
|
||||||
|
loadingSpec?: Spec | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading component for json-render Next.js apps.
|
||||||
|
*
|
||||||
|
* If a loading spec is provided, it will be rendered by the PageRenderer.
|
||||||
|
* Otherwise renders a minimal default loading indicator.
|
||||||
|
*
|
||||||
|
* Note: Since Next.js loading.tsx is static (no params), this renders
|
||||||
|
* a generic loading state. Route-specific loading can be achieved with
|
||||||
|
* Suspense boundaries in page content.
|
||||||
|
*/
|
||||||
|
export function NextLoading({ loadingSpec: _loadingSpec }: NextLoadingProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
minHeight: "200px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "2rem",
|
||||||
|
height: "2rem",
|
||||||
|
border: "2px solid #e5e7eb",
|
||||||
|
borderTopColor: "#3b82f6",
|
||||||
|
borderRadius: "50%",
|
||||||
|
animation: "jr-spin 0.6s linear infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<style>{`@keyframes jr-spin { to { transform: rotate(360deg) } }`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not-found component for json-render Next.js apps.
|
||||||
|
*
|
||||||
|
* Renders when the pathname does not match any route in the spec.
|
||||||
|
*/
|
||||||
|
export function NextNotFound() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minHeight: "400px",
|
||||||
|
padding: "2rem",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ fontSize: "4rem", fontWeight: 700, margin: 0 }}>404</h1>
|
||||||
|
<p style={{ color: "#666", marginTop: "0.5rem", fontSize: "1.125rem" }}>
|
||||||
|
This page could not be found.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useMemo, type ReactNode } from "react";
|
||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import {
|
||||||
|
Renderer,
|
||||||
|
StateProvider,
|
||||||
|
VisibilityProvider,
|
||||||
|
ValidationProvider,
|
||||||
|
ActionProvider,
|
||||||
|
type ComponentRegistry,
|
||||||
|
type ComponentRenderProps,
|
||||||
|
} from "@json-render/react";
|
||||||
|
import { useNextApp } from "./provider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for PageRenderer.
|
||||||
|
*/
|
||||||
|
export interface PageRendererProps {
|
||||||
|
/** Page element tree spec */
|
||||||
|
spec: Spec;
|
||||||
|
/** Initial state (merged from spec.state, global state, and loader data) */
|
||||||
|
initialState?: Record<string, unknown>;
|
||||||
|
/** Optional layout element tree spec */
|
||||||
|
layoutSpec?: Spec | null;
|
||||||
|
/** Whether the spec is currently loading/streaming */
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Built-in Slot component that renders children passed into layouts.
|
||||||
|
*/
|
||||||
|
function SlotComponent({ children }: ComponentRenderProps) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client component that renders a json-render page within the Next.js app.
|
||||||
|
*
|
||||||
|
* Reads the component registry and action handlers from NextAppProvider context.
|
||||||
|
* Handles layout rendering by injecting page content into a Slot component.
|
||||||
|
*/
|
||||||
|
export function PageRenderer({
|
||||||
|
spec,
|
||||||
|
initialState,
|
||||||
|
layoutSpec,
|
||||||
|
loading,
|
||||||
|
}: PageRendererProps) {
|
||||||
|
const { registry, handlers, navigate } = useNextApp();
|
||||||
|
|
||||||
|
const augmentedRegistry: ComponentRegistry = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...registry,
|
||||||
|
Slot: SlotComponent,
|
||||||
|
};
|
||||||
|
}, [registry]);
|
||||||
|
|
||||||
|
const actionHandlers = useMemo(() => {
|
||||||
|
const base: Record<
|
||||||
|
string,
|
||||||
|
(params: Record<string, unknown>) => Promise<unknown> | unknown
|
||||||
|
> = { ...handlers };
|
||||||
|
|
||||||
|
base.navigate = (params: Record<string, unknown>) => {
|
||||||
|
const href = params.href as string | undefined;
|
||||||
|
if (href) navigate(href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}, [handlers, navigate]);
|
||||||
|
|
||||||
|
const pageContent = (
|
||||||
|
<Renderer spec={spec} registry={augmentedRegistry} loading={loading} />
|
||||||
|
);
|
||||||
|
|
||||||
|
if (layoutSpec) {
|
||||||
|
return (
|
||||||
|
<StateProvider initialState={initialState}>
|
||||||
|
<VisibilityProvider>
|
||||||
|
<ValidationProvider>
|
||||||
|
<ActionProvider handlers={actionHandlers} navigate={navigate}>
|
||||||
|
<LayoutWithSlot
|
||||||
|
layoutSpec={layoutSpec}
|
||||||
|
registry={augmentedRegistry}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{pageContent}
|
||||||
|
</LayoutWithSlot>
|
||||||
|
</ActionProvider>
|
||||||
|
</ValidationProvider>
|
||||||
|
</VisibilityProvider>
|
||||||
|
</StateProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateProvider initialState={initialState}>
|
||||||
|
<VisibilityProvider>
|
||||||
|
<ValidationProvider>
|
||||||
|
<ActionProvider handlers={actionHandlers} navigate={navigate}>
|
||||||
|
{pageContent}
|
||||||
|
</ActionProvider>
|
||||||
|
</ValidationProvider>
|
||||||
|
</VisibilityProvider>
|
||||||
|
</StateProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a layout spec and injects children into the Slot.
|
||||||
|
*/
|
||||||
|
function LayoutWithSlot({
|
||||||
|
layoutSpec,
|
||||||
|
registry,
|
||||||
|
loading,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
layoutSpec: Spec;
|
||||||
|
registry: ComponentRegistry;
|
||||||
|
loading?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const layoutRegistry: ComponentRegistry = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...registry,
|
||||||
|
Slot: function LayoutSlot() {
|
||||||
|
return <>{children}</>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [registry, children]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Renderer spec={layoutSpec} registry={layoutRegistry} loading={loading} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, useContext, type ReactNode } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import type { ComponentRegistry } from "@json-render/react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context value holding the component registry, action handlers,
|
||||||
|
* and the Next.js router navigate function.
|
||||||
|
*/
|
||||||
|
export interface NextAppContextValue {
|
||||||
|
registry: ComponentRegistry;
|
||||||
|
handlers?: Record<
|
||||||
|
string,
|
||||||
|
(params: Record<string, unknown>) => Promise<unknown> | unknown
|
||||||
|
>;
|
||||||
|
navigate: (href: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NextAppContext = createContext<NextAppContextValue | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for NextAppProvider.
|
||||||
|
*/
|
||||||
|
export interface NextAppProviderProps {
|
||||||
|
/** Component registry for rendering */
|
||||||
|
registry: ComponentRegistry;
|
||||||
|
/** Action handlers */
|
||||||
|
handlers?: Record<
|
||||||
|
string,
|
||||||
|
(params: Record<string, unknown>) => Promise<unknown> | unknown
|
||||||
|
>;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider that holds the component registry and action handlers
|
||||||
|
* for the entire json-render Next.js application.
|
||||||
|
*
|
||||||
|
* Wrap your root layout with this provider:
|
||||||
|
*
|
||||||
|
* ```tsx
|
||||||
|
* // app/[[...slug]]/layout.tsx
|
||||||
|
* import { NextAppProvider } from "@json-render/next";
|
||||||
|
* import { registry, handlers } from "@/lib/registry";
|
||||||
|
*
|
||||||
|
* export default function Layout({ children }) {
|
||||||
|
* return (
|
||||||
|
* <html><body>
|
||||||
|
* <NextAppProvider registry={registry} handlers={handlers}>
|
||||||
|
* {children}
|
||||||
|
* </NextAppProvider>
|
||||||
|
* </body></html>
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function NextAppProvider({
|
||||||
|
registry,
|
||||||
|
handlers,
|
||||||
|
children,
|
||||||
|
}: NextAppProviderProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const navigate = React.useCallback(
|
||||||
|
(href: string) => router.push(href),
|
||||||
|
[router],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = React.useMemo(
|
||||||
|
() => ({ registry, handlers, navigate }),
|
||||||
|
[registry, handlers, navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NextAppContext.Provider value={value}>{children}</NextAppContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to access the NextApp context.
|
||||||
|
* Must be used within a NextAppProvider.
|
||||||
|
*/
|
||||||
|
export function useNextApp(): NextAppContextValue {
|
||||||
|
const ctx = useContext(NextAppContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error(
|
||||||
|
"[json-render/next] useNextApp must be used within a <NextAppProvider>. " +
|
||||||
|
"Wrap your root layout with <NextAppProvider registry={...} handlers={...}>.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user