Compare commits

...
Author SHA1 Message Date
Chris Tate 5f2ecc1118 update release process (#267)
* update release process

* fix lock
2026-04-10 00:17:21 -05:00
Chris Tate 6e5ea186de feat(web): fetch GitHub star count dynamically (#266)
* feat(web): fetch GitHub star count dynamically

Replace the hardcoded "12k" star count in the header with a live value
from the GitHub API, revalidated every 24 hours. Gracefully hides the
count if the fetch fails.

* fix: add GITHUB_TOKEN to turbo.json globalEnv

* fix: remove GITHUB_TOKEN usage from star count fetch
2026-04-09 02:07:27 -05:00
Chris Tateandctate a9858918f8 Update official links to use json-render.dev domain (#265)
* Update official links to use json-render.dev domain

- Change dashboard example Docs link from json-render.vercel.app to json-render.dev
- Change Svelte Chat demo URL from json-render-svelte-chat-demo.labs.vercel.dev to svelte-chat-demo.json-render.dev

Fixes #264

* Revert svelte-chat demo URL change

Keep the original labs.vercel.dev URL for the Svelte Chat demo.

---------

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-04-08 17:08:10 -05:00
Chris Tate eebc6e4f4f better ignore (#263) 2026-04-08 10:08:51 -05:00
vzt7 a123f56fe3 fix: resolve image demo /api/image 500 on Vercel (#253)
Add outputFileTracingIncludes so the Geist font TTF is bundled at the
symlink path the code reads from. In a pnpm monorepo the file was only
traced at the .pnpm store path, which Vercel does not map back to the
node_modules/geist/ symlink.
2026-04-07 01:43:04 -05:00
Rayan Salhab 753c1d1109 fix(remotion): include props in generatePrompt output (#249)
The Remotion schema defines props on composition entries, but generatePrompt
did not include those props in its output. As a result, the model had no
knowledge of the expected prop shape when generating JSON, causing runtime
failures when prop names didn't match the component's expected interface.

Changes:
- Import zod type for TypeScript support
- Extract formatZodType from PromptContext
- Add props field to component type definition
- Include formatted props string in component description

Fixes #224
2026-04-07 01:39:32 -05:00
github-actions[bot] 9adcc09204 chore: version packages (#248)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-27 18:17:20 -07:00
Chris Tate 519a538aae v0.16.0 (#247) 2026-03-27 16:19:38 -07:00
Chris Tate a99d68c481 next (#246)
* next

* next example

* component catalog

* fixes

* fixes
2026-03-27 16:10:13 -07:00
maxffarrellandClaude Opus 4.6 40892a6af0 feat: shadcn-svelte renderer (#227)
* init

* fix: resolve all type errors, runtime bugs, and test failures in @json-render/shadcn-svelte

- Add /index.js extensions to all ../ui/* imports (NodeNext moduleResolution)
- Type shadcnComponents as Record<string, Component<any>> to fix d.ts generation
- Call getBoundProp() once at top level instead of inside a binding() function
  called from event handlers (getContext() only works during initialization)
- Move getOptionalValidationContext() call to top level; update createValidation()
  to accept the context as first param instead of calling getContext() itself
- Wrap validation.register() in untrack() to break the fieldConfigs read-write
  cycle that caused effect_update_depth_exceeded in tests
- Fix implicit any on Input onkeydown handler (KeyboardEvent type)
- Use untrack() for $state() initializers that read reactive props values
- Call getStateValue() once at top level in Dialog/Drawer (not in a function
  called from event handlers)
- Fix test fixtures: use named imports { StateProvider, ValidationProvider }
- Add server.deps.inline for bits-ui and @lucide/svelte so vitest can process
  their .svelte source files; add root svelte.config.js with runes: true
- Add role/aria-modal/tabindex/onkeydown a11y attributes to Dialog and Drawer
- Fix self-closing non-void elements in Skeleton, Spinner, Textarea

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* update shadcn-svelte components

* fixes

* fixes

* fix: address PR review issues for shadcn-svelte renderer

- Fix missing comma in .changeset/config.json (invalid JSON)
- Fix Slider.svelte type errors (single-mode expects number, not array)
- Replace raw <button> with shadcn Button in Pagination.svelte
- Remove global font import and body/html overrides from app.css
- Remove root svelte.config.js (duplicates package-level config)
- Revert unrelated packageManager version bump

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 10:38:11 -05:00
355 changed files with 11548 additions and 801 deletions
-13
View File
@@ -1,13 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in the repository](https://github.com/changesets/changesets).
## Adding a changeset
To add a changeset, run `pnpm changeset` in the root of the repository. This will prompt you to select
which packages have changed and what type of version bump (major, minor, or patch) should be applied.
All `@json-render/*` packages are versioned together -- a changeset for any one of them will bump all
packages to the same version.
-37
View File
@@ -1,37 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [
[
"@json-render/core",
"@json-render/react",
"@json-render/react-email",
"@json-render/react-pdf",
"@json-render/shadcn",
"@json-render/react-native",
"@json-render/remotion",
"@json-render/codegen",
"@json-render/zustand",
"@json-render/redux",
"@json-render/jotai",
"@json-render/vue",
"@json-render/xstate",
"@json-render/image",
"@json-render/mcp",
"@json-render/svelte",
"@json-render/solid",
"@json-render/react-three-fiber",
"@json-render/yaml",
"@json-render/ink"
]
],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"privatePackages": {
"version": true,
"tag": false
}
}
+12
View File
@@ -13,6 +13,18 @@ concurrency:
cancel-in-progress: true
jobs:
version-sync:
name: Version Sync Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Check version sync
run: node scripts/check-version-sync.js
lint:
name: Lint
runs-on: ubuntu-latest
+127 -17
View File
@@ -8,19 +8,62 @@ on:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
jobs:
release:
name: Release
check-release:
name: Check for new version
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
should_release: ${{ steps.check.outputs.should_release }}
needs_github_release: ${{ steps.check.outputs.needs_github_release }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Compare package.json version to npm and check GitHub release
id: check
run: |
LOCAL_VERSION=$(node -p "require('./packages/core/package.json').version")
echo "Local version: $LOCAL_VERSION"
NPM_VERSION=$(npm view @json-render/core version 2>/dev/null || echo "0.0.0")
echo "npm version: $NPM_VERSION"
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "needs_github_release=true" >> "$GITHUB_OUTPUT"
else
echo "Version unchanged on npm, skipping build and publish"
echo "should_release=false" >> "$GITHUB_OUTPUT"
TAG="v$LOCAL_VERSION"
if gh release view "$TAG" &>/dev/null; then
echo "GitHub release $TAG exists"
echo "needs_github_release=false" >> "$GITHUB_OUTPUT"
else
echo "GitHub release $TAG is missing, will create it"
echo "needs_github_release=true" >> "$GITHUB_OUTPUT"
fi
fi
echo "version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
name: Build
needs: check-release
if: needs.check-release.outputs.should_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -28,20 +71,87 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm run build
publish:
name: Publish to npm
needs: [check-release, build]
if: needs.check-release.outputs.should_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
version: pnpm ci:version
publish: pnpm ci:publish
title: "chore: version packages"
commit: "chore: version packages"
- name: Build packages
run: pnpm run build
- name: Publish all public packages
run: pnpm -r publish --no-git-checks --filter '@json-render/*'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_VERCEL_TOKEN_ELEVATED }}
github-release:
name: Create GitHub Release
needs: [check-release, publish]
if: >-
always()
&& needs.check-release.outputs.needs_github_release == 'true'
&& (needs.publish.result == 'success' || needs.publish.result == 'skipped')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract changelog entry
run: |
VERSION="${{ needs.check-release.outputs.version }}"
awk '/<!-- release:start -->/{found=1; next} /<!-- release:end -->/{found=0} found{print}' CHANGELOG.md > /tmp/release-notes.md
LINES=$(wc -l < /tmp/release-notes.md | tr -d ' ')
if [ "$LINES" -lt 2 ]; then
echo "Error: No release notes found between <!-- release:start --> and <!-- release:end --> markers in CHANGELOG.md"
exit 1
fi
echo "Extracted release notes for $VERSION ($LINES lines)"
- name: Create GitHub Release
run: |
VERSION="${{ needs.check-release.outputs.version }}"
TAG="v$VERSION"
if gh release view "$TAG" &>/dev/null; then
echo "Release $TAG already exists"
else
echo "Creating release $TAG..."
gh release create "$TAG" \
--title "$TAG" \
--notes-file /tmp/release-notes.md
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_VERCEL_TOKEN_ELEVATED }}
+2
View File
@@ -29,6 +29,8 @@ build
dist
*.tsbuildinfo
.svelte-kit/
tsup.config.bundled_*.mjs
next-env.d.ts
# Debug
+15 -17
View File
@@ -73,25 +73,20 @@ Do **not** add `--port` flags -- portless handles port assignment automatically.
- Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)
- `AGENTS.md` (if workflow or conventions change)
## Releases
## Releasing
This monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format.
### Fixed version group
All public `@json-render/*` packages are in a **fixed** group (see `.changeset/config.json`). A changeset that bumps any one of them bumps all of them to the same version. You only need to list the packages that actually changed in the changeset front matter — the fixed group handles the rest.
All public `@json-render/*` packages share the same version. The canonical version lives in `packages/core/package.json`.
### Preparing a release
When asked to prepare a release (e.g. "prepare v0.12.0"):
When asked to prepare a release (e.g. "prepare v0.17.0"):
1. **Create a changeset file** at `.changeset/v0-<N>-release.md` following the existing pattern:
- YAML front matter listing changed packages with bump type (`minor` for feature releases, `patch` for bug-fix-only releases)
- A one-line summary, then `### New:` / `### Improved:` / `### Fixed:` sections describing each change
- Always list `@json-render/core` plus any packages with actual code changes
2. **Do NOT bump versions** in `package.json` files — CI runs `pnpm ci:version` (which calls `changeset version`) to do that automatically
3. **Do NOT manually write `CHANGELOG.md`** entries — `changeset version` generates them from the changeset file
4. **Add new packages to the fixed group** in `.changeset/config.json` if they should be versioned together with the rest
1. Create a branch (e.g. `prepare-v0.17.0`)
2. Bump the version in `packages/core/package.json`
3. Run `pnpm run version:sync` to update all other `@json-render/*` packages
4. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers (move the markers from the previous entry to the new one)
5. **Fill documentation gaps** — every public package should have:
- A row in the root `README.md` packages table
- A renderer section in the root `README.md` (if it's a renderer)
@@ -101,12 +96,15 @@ When asked to prepare a release (e.g. "prepare v0.12.0"):
- A skill at `skills/<name>/SKILL.md`
- A `packages/<name>/README.md`
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
7. Open a PR and merge to `main`
### CI scripts
CI compares the `@json-render/core` version to what's on npm. If it differs, it builds, publishes all public packages, and creates the GitHub release automatically. The release body is extracted from the content between the markers.
- `pnpm changeset` — interactively create a new changeset
- `pnpm ci:version` — run `changeset version` + lockfile update (CI only)
- `pnpm ci:publish` — build all packages and publish to npm (CI only)
### Scripts
- `pnpm run version:sync` — sync all `@json-render/*` package versions to match `@json-render/core`
- `pnpm run version:check` — verify all versions are in sync (runs in CI)
- `pnpm run ci:publish` — build all packages and publish to npm (CI only)
<!-- opensrc:start -->
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## 0.16.0
<!-- release:start -->
### Improved
- **Release process** — Switched from Changesets to a manual single-PR release workflow with changelog markers and automatic npm publish on version bump
<!-- release:end -->
+78
View File
@@ -25,6 +25,8 @@ npm install @json-render/core @json-render/svelte
npm install @json-render/core @json-render/solid
# or for terminal UIs
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
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/solid` | SolidJS renderer with fine-grained reactive contexts |
| `@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-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/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 |
@@ -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)
```tsx
+10
View File
@@ -1,5 +1,15 @@
# 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
+280
View File
@@ -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`
+5 -2
View File
@@ -1,13 +1,16 @@
import { Header } from "@/components/header";
import { getStarCount } from "@/lib/github";
export default function MainLayout({
export default async function MainLayout({
children,
}: {
children: React.ReactNode;
}) {
const stars = await getStarCount();
return (
<div className="min-h-screen flex flex-col">
<Header />
<Header stars={stars} />
<main className="flex-1">{children}</main>
</div>
);
+2 -2
View File
@@ -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
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
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.
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, 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.
+11 -5
View File
@@ -19,7 +19,13 @@ const navLinks = [
{ href: "/docs", label: "Docs" },
];
function GitHubLink({ className }: { className?: string }) {
function GitHubLink({
className,
stars,
}: {
className?: string;
stars?: string;
}) {
return (
<a
href="https://github.com/vercel-labs/json-render"
@@ -38,12 +44,12 @@ function GitHubLink({ className }: { className?: string }) {
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
<span>12k</span>
{stars && <span>{stars}</span>}
</a>
);
}
export function Header() {
export function Header({ stars }: { stars?: string }) {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
@@ -122,14 +128,14 @@ export function Header() {
</Link>
))}
<Search />
<GitHubLink />
<GitHubLink stars={stars} />
<ThemeToggle />
</nav>
{/* Mobile nav */}
<div className="flex sm:hidden items-center gap-3">
<Search />
<GitHubLink />
<GitHubLink stars={stars} />
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTrigger
className="flex items-center justify-center"
+2
View File
@@ -69,9 +69,11 @@ export const docsNavigation: NavSection[] = [
items: [
{ title: "@json-render/core", href: "/docs/api/core" },
{ 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-email", href: "/docs/api/react-email" },
{ 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/image", href: "/docs/api/image" },
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
+20
View File
@@ -0,0 +1,20 @@
const REPO = "vercel-labs/json-render";
const REVALIDATE = 86400;
export async function getStarCount(): Promise<string> {
try {
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
headers: { Accept: "application/vnd.github.v3+json" },
next: { revalidate: REVALIDATE },
});
if (!res.ok) return "";
const data = await res.json();
const count = data.stargazers_count;
if (typeof count !== "number") return "";
if (count >= 1000)
return `${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k`;
return String(count);
} catch {
return "";
}
}
+2
View File
@@ -43,6 +43,7 @@ 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/next": "@json-render/next API",
"docs/api/vue": "@json-render/vue API",
"docs/api/solid": "@json-render/solid 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/remotion": "@json-render/remotion 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/redux": "@json-render/redux API",
"docs/api/zustand": "@json-render/zustand API",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.1.10",
"version": "0.1.11",
"type": "module",
"private": true,
"license": "Apache-2.0",
+9
View File
@@ -1,5 +1,14 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
/// <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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-chat",
"version": "0.1.10",
"version": "0.1.11",
"type": "module",
"private": true,
"scripts": {
+9
View File
@@ -1,5 +1,14 @@
# 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
+1 -1
View File
@@ -47,7 +47,7 @@ export function Header() {
</div>
<nav className="flex items-center gap-4">
<a
href="https://json-render.vercel.app"
href="https://json-render.dev"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
-6
View File
@@ -1,6 +0,0 @@
/// <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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-dashboard",
"version": "0.1.10",
"version": "0.1.11",
"type": "module",
"private": true,
"scripts": {
+10
View File
@@ -1,5 +1,15 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
/// <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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-game-engine",
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+5
View File
@@ -2,6 +2,11 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ["@resvg/resvg-js", "satori"],
outputFileTracingIncludes: {
"/api/image": [
"./node_modules/geist/dist/fonts/geist-sans/Geist-Regular.ttf",
],
},
};
export default nextConfig;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-image",
"version": "0.1.7",
"version": "0.1.8",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-ink-chat",
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"private": true,
"scripts": {
+10
View File
@@ -1,5 +1,15 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-mcp",
"version": "0.1.6",
"version": "0.1.7",
"type": "module",
"private": true,
"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 };
+7
View File
@@ -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">
&ldquo;{item.quote}&rdquo;
</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,
};
@@ -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"]
}
+9
View File
@@ -1,5 +1,14 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
/// <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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-no-ai",
"version": "0.1.10",
"version": "0.1.11",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-email",
"version": "0.1.6",
"version": "0.1.7",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-native",
"version": "0.1.10",
"version": "0.1.11",
"private": true,
"main": "expo-router/entry",
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
/// <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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-pdf",
"version": "0.1.10",
"version": "0.1.11",
"type": "module",
"private": true,
"scripts": {
+9
View File
@@ -1,5 +1,14 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
/// <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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-three-fiber",
"version": "0.1.10",
"version": "0.1.11",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
/// <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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-remotion",
"version": "0.1.10",
"version": "0.1.11",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-solid",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"type": "module",
"scripts": {
-6
View File
@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,5 +1,13 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-demo",
"version": "0.0.11",
"version": "0.0.12",
"description": "Test",
"private": true,
"license": "~~proprietary~~",
@@ -1,5 +1,13 @@
# 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
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-fullpage-demo",
"version": "0.0.11",
"version": "0.0.12",
"description": "Full-page Stripe App example (alpha)",
"private": true,
"license": "~~proprietary~~",
+8
View File
@@ -1,5 +1,13 @@
# 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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "svelte-chat",
"private": true,
"version": "0.0.7",
"version": "0.0.8",
"type": "module",
"scripts": {
"dev": "vite dev",
+8
View File
@@ -1,5 +1,13 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-svelte",
"version": "0.1.7",
"version": "0.1.8",
"private": true,
"type": "module",
"scripts": {
+11
View File
@@ -1,5 +1,16 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vite-renderers",
"version": "0.1.8",
"version": "0.1.9",
"private": true,
"type": "module",
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-vue",
"version": "0.1.8",
"version": "0.1.9",
"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)",
+4 -5
View File
@@ -13,7 +13,7 @@
"scripts": {
"build": "turbo run build",
"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": "turbo run dev --concurrency 20",
"dev": "turbo run dev --concurrency 50",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx}\"",
"type-check": "turbo run check-types",
@@ -23,13 +23,12 @@
"test:coverage": "vitest run --coverage",
"test:e2e": "pnpm --filter e2e-tests test",
"prepare": "husky",
"changeset": "changeset",
"ci:version": "changeset version && pnpm install --no-frozen-lockfile",
"ci:publish": "pnpm run build && changeset publish",
"version:sync": "node scripts/sync-version.js",
"version:check": "node scripts/check-version-sync.js",
"ci:publish": "pnpm run build && pnpm -r publish --no-git-checks --filter '@json-render/*'",
"generate:og": "npx tsx scripts/generate-og-images.mts"
},
"devDependencies": {
"@changesets/cli": "2.29.8",
"@resvg/resvg-js": "2.6.2",
"@solidjs/testing-library": "^0.8.10",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
+7
View File
@@ -1,5 +1,12 @@
# @json-render/codegen
## 0.16.0
### Patch Changes
- Updated dependencies [519a538]
- @json-render/core@0.16.0
## 0.15.0
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/codegen",
"version": "0.15.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Utilities for generating code from json-render UI trees",
"keywords": [
+10
View File
@@ -1,5 +1,15 @@
# @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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/core",
"version": "0.15.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/image
## 0.16.0
### Patch Changes
- Updated dependencies [519a538]
- @json-render/core@0.16.0
## 0.15.0
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/image",
"version": "0.15.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/ink
## 0.16.0
### Patch Changes
- Updated dependencies [519a538]
- @json-render/core@0.16.0
## 0.15.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/ink",
"version": "0.15.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Ink terminal renderer for @json-render/core. JSON becomes terminal UIs.",
"keywords": [

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