Compare commits

..
Author SHA1 Message Date
Chris Tate dd0ba3fea9 update og font 2026-02-22 15:55:50 -06:00
Chris Tate fa47b08869 update header font (#136) 2026-02-22 15:51:54 -06:00
Chris Tate 5ccb109c08 use visual-json (#135)
* use visual-json

* fix lint
2026-02-22 15:42:29 -06:00
Chris Tate 62932f6516 fix gitignore (#134) 2026-02-22 15:20:33 -06:00
Chris Tate ee28d548c1 use portless (#133) 2026-02-22 15:15:17 -06:00
Chris Tate 0e6f2afc6c fix og (#128) 2026-02-20 02:41:16 -06:00
Chris Tate bccedc2459 add docs (#127) 2026-02-20 02:30:33 -06:00
57 changed files with 757 additions and 102 deletions
+2 -9
View File
@@ -6,11 +6,8 @@ node_modules
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env*
!.env.example
# Testing
coverage
@@ -44,10 +41,6 @@ yarn-error.log*
# opensrc - source code for packages
opensrc/
# json-studio (separate repo)
json-studio/
.env*.local
# Stripe apps (generated from template + build artifacts)
examples/stripe-app/*/stripe-app.json
examples/stripe-app/*/.build
+20
View File
@@ -27,6 +27,26 @@ This ensures we don't install outdated versions that may have incompatible types
- Do not use emojis in code or UI
- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`
## Dev Servers
All apps and examples with dev servers use [portless](https://github.com/vercel-labs/portless) to avoid hardcoded ports. Portless assigns random ports and exposes each app via `.localhost` URLs.
Naming convention:
- Main web app: `json-render` → `json-render.localhost:1355`
- Examples: `[name]-demo.json-render` → `[name]-demo.json-render.localhost:1355`
When adding a new example that runs a dev server, wrap its `dev` script with `portless <name>`:
```json
{
"scripts": {
"dev": "portless my-example-demo.json-render next dev --turbopack"
}
}
```
Do **not** add `--port` flags -- portless handles port assignment automatically. Do **not** add portless as a project dependency; it must be installed globally.
## Workflow
- Run `pnpm type-check` after each turn to ensure type safety
+3 -3
View File
@@ -343,9 +343,9 @@ pnpm install
pnpm dev
```
- http://localhost:3000 - Docs & Playground
- http://localhost:3001 - Example Dashboard
- http://localhost:3002 - Remotion Video Example
- http://json-render.localhost:1355 - Docs & Playground
- http://dashboard-demo.json-render.localhost:1355 - Example Dashboard
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
- Chat Example: run `pnpm dev` in `examples/chat`
- React Native example: run `npx expo start` in `examples/react-native`
+1 -1
View File
@@ -14,7 +14,7 @@ pnpm dev
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Open [http://json-render.localhost:1355](http://json-render.localhost:1355) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "A2UI Integration" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/a2ui")
# A2UI Integration
@@ -1,4 +1,5 @@
export const metadata = { title: "Adaptive Cards Integration" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/adaptive-cards")
# Adaptive Cards Integration
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "AG-UI Integration" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/ag-ui")
# AG-UI Integration
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "AI SDK Integration" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/ai-sdk")
# AI SDK Integration
@@ -1,4 +1,5 @@
export const metadata = { title: "@json-render/codegen API" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/codegen")
# @json-render/codegen
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "@json-render/core API" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/core")
# @json-render/core
@@ -1,4 +1,5 @@
export const metadata = { title: "@json-render/react-native API" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-native")
# @json-render/react-native
@@ -0,0 +1,423 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-pdf")
# @json-render/react-pdf
PDF document renderer. Turn JSON specs into PDFs using `@react-pdf/renderer`.
## Install
```bash
npm install @json-render/core @json-render/react-pdf
```
See the [React PDF example](https://github.com/vercel-labs/json-render/tree/main/examples/react-pdf) for a full working example.
## schema
The PDF element schema for document specs. Use with `defineCatalog` from core.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema, standardComponentDefinitions } from '@json-render/react-pdf';
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
```
## Render Functions
Server-side functions for producing PDF output. All accept a spec and optional `RenderOptions`.
```typescript
import { renderToBuffer, renderToStream, renderToFile } from '@json-render/react-pdf';
const buffer = await renderToBuffer(spec);
const stream = await renderToStream(spec);
stream.pipe(res);
await renderToFile(spec, './output.pdf');
```
### RenderOptions
```typescript
interface RenderOptions {
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
state?: Record<string, unknown>;
}
```
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>registry</code></td>
<td>Custom component map (merged with standard components)</td>
</tr>
<tr>
<td><code>includeStandard</code></td>
<td>Include built-in standard components (default: <code>true</code>)</td>
</tr>
<tr>
<td><code>state</code></td>
<td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>
</tr>
</tbody>
</table>
## defineRegistry
Create a type-safe component registry from a catalog. Components receive `{ props, children, emit, bindings, loading }`.
```tsx
import { defineRegistry } from '@json-render/react-pdf';
import { View, Text } from '@react-pdf/renderer';
const { registry } = defineRegistry(catalog, {
components: {
Badge: ({ props }) => (
<View style={{ backgroundColor: props.color ?? '#e5e7eb', padding: 4, borderRadius: 4 }}>
<Text style={{ fontSize: 10 }}>{props.label}</Text>
</View>
),
},
});
const buffer = await renderToBuffer(spec, { registry });
```
## createRenderer
Create a standalone renderer component wired to state, actions, and validation.
```typescript
import { createRenderer } from '@json-render/react-pdf';
const PDFRenderer = createRenderer(catalog, components);
```
```typescript
interface CreateRendererProps {
spec: Spec | null;
state?: Record<string, unknown>;
onAction?: (actionName: string, params?: Record<string, unknown>) => void;
onStateChange?: (path: string, value: unknown) => void;
loading?: boolean;
fallback?: ComponentRenderer;
}
```
## Renderer
The main component that renders a spec to `@react-pdf/renderer` elements.
```typescript
interface RendererProps {
spec: Spec | null;
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
loading?: boolean;
fallback?: ComponentRenderer;
}
```
## Standard Components
### Document Structure
#### Document
Top-level PDF wrapper. Must be the root element. Children must be `Page` components.
```typescript
{
title: string | null;
author: string | null;
subject: string | null;
}
```
#### Page
A page in the document with configurable size, orientation, and margins.
```typescript
{
size: "A4" | "A3" | "A5" | "LETTER" | "LEGAL" | "TABLOID" | null;
orientation: "portrait" | "landscape" | null;
marginTop: number | null;
marginBottom: number | null;
marginLeft: number | null;
marginRight: number | null;
backgroundColor: string | null;
}
```
### Layout
#### View
Generic container with padding, margin, background, border, and flex alignment.
```typescript
{
padding: number | null;
paddingTop: number | null;
paddingBottom: number | null;
paddingLeft: number | null;
paddingRight: number | null;
margin: number | null;
backgroundColor: string | null;
borderWidth: number | null;
borderColor: string | null;
borderRadius: number | null;
flex: number | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
}
```
#### Row
Horizontal flex layout with optional wrapping.
```typescript
{
gap: number | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
padding: number | null;
flex: number | null;
wrap: boolean | null;
}
```
#### Column
Vertical flex layout.
```typescript
{
gap: number | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
padding: number | null;
flex: number | null;
}
```
### Content
#### Heading
h1-h4 heading text with configurable color and alignment.
```typescript
{
text: string;
level: "h1" | "h2" | "h3" | "h4" | null;
color: string | null;
align: "left" | "center" | "right" | null;
}
```
#### Text
Body text with full styling control.
```typescript
{
text: string;
fontSize: number | null;
color: string | null;
align: "left" | "center" | "right" | null;
fontWeight: "normal" | "bold" | null;
fontStyle: "normal" | "italic" | null;
lineHeight: number | null;
}
```
#### Image
Image from a URL with optional dimensions and fit.
```typescript
{
src: string;
width: number | null;
height: number | null;
objectFit: "contain" | "cover" | "fill" | "none" | null;
}
```
#### Link
Hyperlink with visible text.
```typescript
{
text: string;
href: string;
fontSize: number | null;
color: string | null;
}
```
### Data
#### Table
Data table with typed columns and string rows. Supports header styling and striped rows.
```typescript
{
columns: { header: string; width?: string; align?: "left" | "center" | "right" }[];
rows: string[][];
headerBackgroundColor: string | null;
headerTextColor: string | null;
borderColor: string | null;
fontSize: number | null;
striped: boolean | null;
}
```
#### List
Ordered or unordered list.
```typescript
{
items: string[];
ordered: boolean | null;
fontSize: number | null;
color: string | null;
spacing: number | null;
}
```
### Decorative
#### Divider
Horizontal line separator.
```typescript
{
color: string | null;
thickness: number | null;
marginTop: number | null;
marginBottom: number | null;
}
```
#### Spacer
Empty vertical space.
```typescript
{
height: number | null;
}
```
### Page-Level
#### PageNumber
Renders current page number and total pages. Format uses `{pageNumber}` and `{totalPages}` placeholders.
```typescript
{
format: string | null; // default: "{pageNumber} / {totalPages}"
fontSize: number | null;
color: string | null;
align: "left" | "center" | "right" | null;
}
```
## Server-Safe Import
Import schema and catalog definitions without pulling in React or `@react-pdf/renderer`:
```typescript
import { schema, standardComponentDefinitions } from '@json-render/react-pdf/server';
```
## Sub-path Exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/react-pdf</code></td>
<td>Full package: schema, renderer, components, render functions</td>
</tr>
<tr>
<td><code>@json-render/react-pdf/server</code></td>
<td>Schema and catalog definitions only (no React)</td>
</tr>
<tr>
<td><code>@json-render/react-pdf/catalog</code></td>
<td>Standard component definitions and types</td>
</tr>
<tr>
<td><code>@json-render/react-pdf/render</code></td>
<td>Server-side render functions only</td>
</tr>
</tbody>
</table>
## Types
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ReactPdfSchema</code></td>
<td>Schema type for PDF specs</td>
</tr>
<tr>
<td><code>ReactPdfSpec</code></td>
<td>Spec type for PDF documents</td>
</tr>
<tr>
<td><code>RenderOptions</code></td>
<td>Options for render functions</td>
</tr>
<tr>
<td><code>ComponentContext</code></td>
<td>Typed component render function context</td>
</tr>
<tr>
<td><code>ComponentFn</code></td>
<td>Component render function type</td>
</tr>
<tr>
<td><code>StandardComponentDefinitions</code></td>
<td>Type of the standard component definitions object</td>
</tr>
<tr>
<td><code>StandardComponentProps&lt;K&gt;</code></td>
<td>Inferred props type for a standard component by name</td>
</tr>
</tbody>
</table>
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "@json-render/react API" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react")
# @json-render/react
@@ -1,4 +1,5 @@
export const metadata = { title: "@json-render/remotion API" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/remotion")
# @json-render/remotion
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "@json-render/shadcn API" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/shadcn")
# @json-render/shadcn
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Catalog" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/catalog")
# Catalog
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Changelog" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/changelog")
# Changelog
@@ -1,4 +1,5 @@
export const metadata = { title: "Code Export" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/code-export")
# Code Export
@@ -134,6 +135,6 @@ Run the dashboard example and click "Export Project" to see code generation in a
```bash
cd examples/dashboard
pnpm dev
# Open http://localhost:3001
# Open http://dashboard-demo.json-render.localhost:1355
# Generate a widget, then click "Export Project"
```
@@ -1,4 +1,5 @@
export const metadata = { title: "Custom Schema & Renderer" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/custom-schema")
# Custom Schema & Renderer
@@ -1,4 +1,5 @@
export const metadata = { title: "Data Binding" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/data-binding")
# Data Binding
@@ -1,4 +1,5 @@
export const metadata = { title: "Generation Modes" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/generation-modes")
# Generation Modes
@@ -1,4 +1,5 @@
export const metadata = { title: "Installation" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/installation")
# Installation
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Migration Guide" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/migration")
# Migration Guide
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "OpenAPI Integration" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/openapi")
# OpenAPI Integration
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Introduction" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs")
# Introduction
@@ -1,4 +1,5 @@
export const metadata = { title: "Quick Start" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/quick-start")
# Quick Start
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Registry" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/registry")
# Registry
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Schemas" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/schemas")
# Schemas
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Specs" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/specs")
# Specs
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Streaming" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/streaming")
# Streaming
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Validation" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/validation")
# Validation
+2 -1
View File
@@ -1,4 +1,5 @@
export const metadata = { title: "Visibility" }
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/visibility")
# Visibility
+15 -4
View File
@@ -154,23 +154,34 @@ button {
margin-bottom: 0.5em;
}
/* MDX table styles — fallback for GFM-generated tables */
/* MDX table styles — applies to both GFM pipe tables and raw HTML tables */
.mdx-table th,
.mdx-table td {
.mdx-table td,
article table th,
article table td {
border: 1px solid var(--border);
padding: 0.75rem 1rem;
text-align: left;
}
.mdx-table th {
.mdx-table th,
article table th {
font-weight: 600;
background-color: var(--muted);
}
.mdx-table td {
.mdx-table td,
article table td {
color: var(--muted-foreground);
}
article table {
width: 100%;
font-size: 0.875rem;
border-collapse: collapse;
margin: 1.5rem 0;
}
/* Shiki dual theme support */
.shiki,
.shiki span {
+4 -2
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import { GeistPixelSquare } from "geist/font/pixel";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { DocsChat } from "@/components/docs-chat";
@@ -61,7 +62,6 @@ export const metadata: Metadata = {
description:
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
images: ["/og"],
creator: "@verabornnot",
},
robots: {
index: true,
@@ -92,7 +92,9 @@ export default async function RootLayout({
/>
)}
</head>
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<body
className={`${geistSans.variable} ${geistMono.variable} ${GeistPixelSquare.variable}`}
>
<ThemeProvider>
{children}
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
+15 -8
View File
@@ -5,19 +5,20 @@ import { join } from "node:path";
export { getPageTitle } from "@/lib/page-titles";
// Cache font data in memory after first load
let fontCache: { geistRegular: Buffer } | null = null;
let fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null = null;
async function loadFonts() {
if (fontCache) return fontCache;
const geistRegular = await readFile(
join(process.cwd(), "public/Geist-Regular.ttf"),
);
fontCache = { geistRegular };
const [geistRegular, geistPixelSquare] = await Promise.all([
readFile(join(process.cwd(), "public/Geist-Regular.ttf")),
readFile(join(process.cwd(), "public/GeistPixel-Square.ttf")),
]);
fontCache = { geistRegular, geistPixelSquare };
return fontCache;
}
export async function renderOgImage(title: string) {
const { geistRegular } = await loadFonts();
const { geistRegular, geistPixelSquare } = await loadFonts();
return new ImageResponse(
<div
@@ -53,8 +54,8 @@ export async function renderOgImage(title: string) {
<span
style={{
fontSize: 36,
fontFamily: "Geist",
fontWeight: 400,
fontFamily: "Geist Pixel Square",
fontWeight: 500,
color: "white",
}}
>
@@ -99,6 +100,12 @@ export async function renderOgImage(title: string) {
style: "normal",
weight: 400,
},
{
name: "Geist Pixel Square",
data: geistPixelSquare.buffer as ArrayBuffer,
style: "normal",
weight: 500,
},
],
},
);
+1 -1
View File
@@ -57,7 +57,7 @@ export function Header() {
</svg>
</span>
<Link href="/">
<span className="font-medium tracking-tight text-lg">
<span className="font-medium tracking-tight text-lg font-(family-name:--font-geist-pixel-square)">
json-render
</span>
</Link>
+122 -31
View File
@@ -16,17 +16,20 @@ import { CopyButton } from "./copy-button";
import { Toaster } from "./ui/sonner";
import { Header } from "./header";
import { Sheet, SheetContent, SheetTitle } from "./ui/sheet";
import { JsonEditor } from "@visual-json/react";
import type { JsonValue } from "@visual-json/react";
import { PlaygroundRenderer } from "@/lib/render/renderer";
import { playgroundCatalog } from "@/lib/render/catalog";
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
type Tab = "json" | "nested" | "stream" | "catalog";
type Tab = "json" | "nested" | "stream" | "catalog" | "visual";
type RenderView = "preview" | "code";
type MobileView =
| "json"
| "nested"
| "stream"
| "catalog"
| "visual"
| "preview"
| "generated-code";
@@ -233,6 +236,20 @@ export function Playground() {
[handleSubmit],
);
const handleVisualChange = useCallback(
(value: JsonValue) => {
if (!selectedVersionId || isStreaming) return;
setVersions((prev) =>
prev.map((v) =>
v.id === selectedVersionId
? { ...v, tree: value as unknown as Spec }
: v,
),
);
},
[selectedVersionId, isStreaming],
);
const jsonCode = currentTree
? JSON.stringify(currentTree, null, 2)
: "// waiting...";
@@ -479,31 +496,69 @@ ${jsx}
? jsonCode
: activeTab === "nested"
? nestedCode
: "";
: activeTab === "visual"
? jsonCode
: "";
const codePane = (
<div className="h-full flex flex-col border-t border-border">
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
{(["json", "nested", "stream", "catalog"] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`text-xs font-mono transition-colors ${
activeTab === tab
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab}
</button>
))}
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
(tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`text-xs font-mono transition-colors ${
activeTab === tab
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab}
</button>
),
)}
<div className="flex-1" />
{activeTab !== "catalog" && (
{activeTab !== "catalog" && activeTab !== "visual" && (
<CopyButton text={copyText} className="text-muted-foreground" />
)}
</div>
<div className="flex-1 overflow-auto">
{activeTab === "catalog" ? (
{activeTab === "visual" ? (
currentTree ? (
<JsonEditor
value={currentTree as unknown as JsonValue}
onChange={handleVisualChange}
readOnly={isStreaming}
sidebarOpen={false}
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 className="text-muted-foreground/50 p-3 text-sm font-mono">
{"// generate a spec to edit visually"}
</div>
)
) : activeTab === "catalog" ? (
<div className="h-full flex flex-col text-sm">
<div className="flex items-center gap-3 px-3 h-9 border-b border-border">
{(
@@ -735,19 +790,21 @@ ${jsx}
: 0}
</button>
{/* Code tabs */}
{(["json", "nested", "stream", "catalog"] as const).map((tab) => (
<button
key={tab}
onClick={() => setMobileView(tab)}
className={`text-xs font-mono transition-colors shrink-0 ${
mobileView === tab
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab}
</button>
))}
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
(tab) => (
<button
key={tab}
onClick={() => setMobileView(tab)}
className={`text-xs font-mono transition-colors shrink-0 ${
mobileView === tab
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab}
</button>
),
)}
<div className="flex-1" />
{/* Preview / code toggle */}
{[
@@ -770,7 +827,41 @@ ${jsx}
{/* Main content area */}
<div className="flex-1 min-h-0 overflow-auto">
{mobileView === "catalog" ? (
{mobileView === "visual" ? (
currentTree ? (
<JsonEditor
value={currentTree as unknown as JsonValue}
onChange={handleVisualChange}
readOnly={isStreaming}
sidebarOpen={false}
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 className="text-muted-foreground/50 p-3 text-sm font-mono">
{"// generate a spec to edit visually"}
</div>
)
) : mobileView === "catalog" ? (
<div className="h-full flex flex-col text-sm">
<div className="flex items-center gap-3 px-3 h-9 border-b border-border">
{(
+6
View File
@@ -57,6 +57,11 @@ export const docsNavigation: NavSection[] = [
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-native",
external: true,
},
{
title: "React PDF",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-pdf",
external: true,
},
{
title: "Remotion",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion",
@@ -86,6 +91,7 @@ export const docsNavigation: NavSection[] = [
items: [
{ title: "@json-render/core", href: "/docs/api/core" },
{ title: "@json-render/react", href: "/docs/api/react" },
{ title: "@json-render/react-pdf", href: "/docs/api/react-pdf" },
{ title: "@json-render/shadcn", href: "/docs/api/shadcn" },
{ title: "@json-render/react-native", href: "/docs/api/react-native" },
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
+39
View File
@@ -0,0 +1,39 @@
import type { Metadata } from "next";
import { PAGE_TITLES } from "./page-titles";
const DESCRIPTION =
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.";
export function pageMetadata(slug: string): Metadata {
const title = PAGE_TITLES[slug];
if (!title) return {};
const displayTitle = title.replace(/\n/g, " ");
const fullTitle = `${displayTitle} | json-render`;
const ogImageUrl = slug ? `/og/${slug}` : "/og";
return {
title: displayTitle,
openGraph: {
type: "website",
locale: "en_US",
siteName: "json-render",
title: fullTitle,
description: DESCRIPTION,
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: `${displayTitle} - json-render`,
},
],
},
twitter: {
card: "summary_large_image",
title: fullTitle,
description: DESCRIPTION,
images: [ogImageUrl],
},
};
}
+2
View File
@@ -38,9 +38,11 @@ export const PAGE_TITLES: Record<string, string> = {
// API references
"docs/api/core": "@json-render/core API",
"docs/api/react": "@json-render/react API",
"docs/api/react-pdf": "@json-render/react-pdf API",
"docs/api/react-native": "@json-render/react-native API",
"docs/api/codegen": "@json-render/codegen API",
"docs/api/remotion": "@json-render/remotion API",
"docs/api/shadcn": "@json-render/shadcn API",
};
/**
+3 -1
View File
@@ -5,7 +5,7 @@
"private": true,
"license": "Apache-2.0",
"scripts": {
"dev": "next dev --turbopack",
"dev": "portless json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --max-warnings 0",
@@ -28,11 +28,13 @@
"@upstash/redis": "^1.36.1",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"@visual-json/react": "0.1.1",
"ai": "^6.0.33",
"bash-tool": "1.3.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"geist": "1.7.0",
"just-bash": "2.9.6",
"lucide-react": "^0.562.0",
"next": "16.1.1",
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
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.
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "portless chat-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --max-warnings 0",
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
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.
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "portless dashboard-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --max-warnings 0",
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
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.
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 3003",
"dev": "portless no-ai-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --max-warnings 0",
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
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.
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 3005",
"dev": "portless react-pdf-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"check-types": "tsc --noEmit"
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
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.
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 3002",
"dev": "portless remotion-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"check-types": "tsc --noEmit"
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
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.
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --port 3001",
"dev": "portless stripe-api-demo.json-render next dev",
"build": "next build",
"start": "next start --port 3001"
},
@@ -1 +1,2 @@
export const API_GENERATE_URL = "http://localhost:3001/api/generate";
export const API_GENERATE_URL =
"http://stripe-api-demo.json-render.localhost:1355/api/generate";
@@ -1 +1,2 @@
export const API_GENERATE_URL = "http://localhost:3001/api/generate";
export const API_GENERATE_URL =
"http://stripe-api-demo.json-render.localhost:1355/api/generate";
+30
View File
@@ -98,6 +98,9 @@ importers:
'@vercel/speed-insights':
specifier: ^1.3.1
version: 1.3.1(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
'@visual-json/react':
specifier: 0.1.1
version: 0.1.1(react@19.2.3)
ai:
specifier: ^6.0.33
version: 6.0.33(zod@4.3.5)
@@ -113,6 +116,9 @@ importers:
embla-carousel-react:
specifier: ^8.6.0
version: 8.6.0(react@19.2.3)
geist:
specifier: 1.7.0
version: 1.7.0(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
just-bash:
specifier: 2.9.6
version: 2.9.6
@@ -5009,6 +5015,14 @@ packages:
vue-router:
optional: true
'@visual-json/core@0.1.1':
resolution: {integrity: sha512-VLARpQnjJU1SnnYk1NiXScGfp2FyAwyZS2kA8hIJV5OzkaH2oS1dTHsaB8vOiyi14/L7jsbpA4g/EwzRWbxQmg==}
'@visual-json/react@0.1.1':
resolution: {integrity: sha512-KCpFYmBhH5F+xctfvxxVX7mVZK/iwvDwh/ITNkGPzEKXTIJQ16fau+hrG7m7QoTAOxvsdhHtHoCUiEjNHXboUQ==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
'@vitest/expect@4.0.17':
resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==}
@@ -6944,6 +6958,11 @@ packages:
fzf@0.5.2:
resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==}
geist@1.7.0:
resolution: {integrity: sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==}
peerDependencies:
next: '>=13.2.0'
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
@@ -16551,6 +16570,13 @@ snapshots:
next: 16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
'@visual-json/core@0.1.1': {}
'@visual-json/react@0.1.1(react@19.2.3)':
dependencies:
'@visual-json/core': 0.1.1
react: 19.2.3
'@vitest/expect@4.0.17':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -18947,6 +18973,10 @@ snapshots:
fzf@0.5.2: {}
geist@1.7.0(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)):
dependencies:
next: 16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
generator-function@2.0.1: {}
gensync@1.0.0-beta.2: {}