mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-02 12:00:58 +08:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fba630223f | ||
|
|
35986f457a | ||
|
|
6e5ea186de | ||
|
|
a9858918f8 | ||
|
|
eebc6e4f4f | ||
|
|
a123f56fe3 | ||
|
|
753c1d1109 | ||
|
|
9adcc09204 | ||
|
|
519a538aae | ||
|
|
a99d68c481 | ||
|
|
40892a6af0 | ||
|
|
7628640984 | ||
|
|
bf3a7ec61d | ||
|
|
453484985a |
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 }}
|
||||
|
||||
@@ -29,6 +29,8 @@ build
|
||||
dist
|
||||
*.tsbuildinfo
|
||||
.svelte-kit/
|
||||
tsup.config.bundled_*.mjs
|
||||
next-env.d.ts
|
||||
|
||||
|
||||
# Debug
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# web
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/codegen@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
- @json-render/yaml@0.16.0
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/codegen@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/yaml@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/next")
|
||||
|
||||
# @json-render/next
|
||||
|
||||
Next.js renderer. JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @json-render/core @json-render/react @json-render/next
|
||||
```
|
||||
|
||||
## schema
|
||||
|
||||
The Next.js app schema for multi-page specs. Use with `defineCatalog` from core.
|
||||
|
||||
```typescript
|
||||
import { defineCatalog } from '@json-render/core';
|
||||
import { schema } from '@json-render/next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
const catalog = defineCatalog(schema, {
|
||||
components: {
|
||||
Card: {
|
||||
props: z.object({ title: z.string() }),
|
||||
description: 'Card container',
|
||||
},
|
||||
NavBar: {
|
||||
props: z.object({ links: z.array(z.object({ href: z.string(), label: z.string() })) }),
|
||||
description: 'Navigation bar',
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
```
|
||||
|
||||
## createNextApp
|
||||
|
||||
Create all exports needed for a Next.js `[[...slug]]` catch-all route.
|
||||
|
||||
```typescript
|
||||
import { createNextApp } from '@json-render/next/server';
|
||||
|
||||
const { Page, generateMetadata, generateStaticParams } = createNextApp({
|
||||
spec: myAppSpec,
|
||||
loaders: {
|
||||
loadPost: async ({ slug }) => {
|
||||
const post = await db.post.findUnique({ where: { slug } });
|
||||
return { post };
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>spec</code></td>
|
||||
<td><code>{'NextAppSpec | (() => NextAppSpec | Promise<NextAppSpec>)'}</code></td>
|
||||
<td>The application spec (static or dynamic)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>loaders</code></td>
|
||||
<td><code>{'Record<string, LoaderFn>'}</code></td>
|
||||
<td>Server-side data loaders keyed by name</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Returns
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Export</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Page</code></td>
|
||||
<td>Async Server Component for <code>page.tsx</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>generateMetadata</code></td>
|
||||
<td>Metadata generator for Next.js SEO</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>generateStaticParams</code></td>
|
||||
<td>Static params for pre-rendering at build time</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## NextAppSpec
|
||||
|
||||
The top-level spec defining an entire Next.js application.
|
||||
|
||||
```typescript
|
||||
interface NextAppSpec {
|
||||
metadata?: NextMetadata;
|
||||
routes: Record<string, NextRouteSpec>;
|
||||
layouts?: Record<string, Spec>;
|
||||
state?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
### Route Patterns
|
||||
|
||||
Routes use Next.js URL conventions:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pattern</th>
|
||||
<th>Example Match</th>
|
||||
<th>Params</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>/</code></td>
|
||||
<td><code>/</code></td>
|
||||
<td><code>{'{}'}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/about</code></td>
|
||||
<td><code>/about</code></td>
|
||||
<td><code>{'{}'}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/blog/[slug]</code></td>
|
||||
<td><code>/blog/hello</code></td>
|
||||
<td><code>{'{ slug: "hello" }'}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/docs/[...path]</code></td>
|
||||
<td><code>/docs/a/b/c</code></td>
|
||||
<td><code>{'{ path: ["a","b","c"] }'}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/app/[[...path]]</code></td>
|
||||
<td><code>/app</code> or <code>/app/x/y</code></td>
|
||||
<td><code>{'{ path: [] }'}</code> or <code>{'{ path: ["x","y"] }'}</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## NextAppProvider
|
||||
|
||||
Client component that provides the component registry and action handlers to all pages.
|
||||
|
||||
```tsx
|
||||
import { NextAppProvider } from '@json-render/next';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<NextAppProvider registry={registry} handlers={handlers}>
|
||||
{children}
|
||||
</NextAppProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Components
|
||||
|
||||
### Slot
|
||||
|
||||
Placeholder in layouts where page content is rendered. Every layout MUST include a Slot.
|
||||
|
||||
```json
|
||||
{ "type": "Slot", "props": {}, "children": [] }
|
||||
```
|
||||
|
||||
### Link
|
||||
|
||||
Client-side navigation wrapping `next/link`.
|
||||
|
||||
```json
|
||||
{ "type": "Link", "props": { "href": "/about" }, "children": ["link-text"] }
|
||||
```
|
||||
|
||||
## Built-in Actions
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Params</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>setState</code></td>
|
||||
<td><code>{'{ statePath, value }'}</code></td>
|
||||
<td>Update a value in state</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pushState</code></td>
|
||||
<td><code>{'{ statePath, value, clearStatePath? }'}</code></td>
|
||||
<td>Append to array in state</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>removeState</code></td>
|
||||
<td><code>{'{ statePath, index }'}</code></td>
|
||||
<td>Remove from array by index</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>navigate</code></td>
|
||||
<td><code>{'{ href }'}</code></td>
|
||||
<td>Client-side navigation</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Server Utilities
|
||||
|
||||
### matchRoute
|
||||
|
||||
Match a pathname against a spec's routes.
|
||||
|
||||
```typescript
|
||||
import { matchRoute } from '@json-render/next/server';
|
||||
|
||||
const matched = matchRoute(spec, '/blog/hello-world');
|
||||
// { route: NextRouteSpec, pattern: '/blog/[slug]', params: { slug: 'hello-world' } }
|
||||
```
|
||||
|
||||
### resolveMetadata
|
||||
|
||||
Resolve merged metadata for a route.
|
||||
|
||||
```typescript
|
||||
import { resolveMetadata } from '@json-render/next/server';
|
||||
|
||||
const metadata = resolveMetadata(spec, matchedRoute?.route);
|
||||
```
|
||||
|
||||
### slugToPath
|
||||
|
||||
Convert catch-all slug array to pathname.
|
||||
|
||||
```typescript
|
||||
import { slugToPath } from '@json-render/next/server';
|
||||
|
||||
slugToPath(undefined); // "/"
|
||||
slugToPath(['blog', 'hello']); // "/blog/hello"
|
||||
```
|
||||
|
||||
## Entry Points
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Import</th>
|
||||
<th>Contents</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>@json-render/next</code></td>
|
||||
<td>Client components (NextAppProvider, PageRenderer, Link)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>@json-render/next/server</code></td>
|
||||
<td>Server utilities (createNextApp, matchRoute, schema)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,320 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/shadcn-svelte")
|
||||
|
||||
# @json-render/shadcn-svelte
|
||||
|
||||
Pre-built [shadcn-svelte](https://www.shadcn-svelte.com/) components for json-render. 36 components built on Svelte 5 + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @json-render/shadcn-svelte @json-render/core @json-render/svelte zod
|
||||
```
|
||||
|
||||
Your app must have Tailwind CSS configured.
|
||||
|
||||
## Entry Points
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Entry Point</th>
|
||||
<th>Exports</th>
|
||||
<th>Use For</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>@json-render/shadcn-svelte</code></td>
|
||||
<td><code>shadcnComponents</code>, <code>shadcnComponentDefinitions</code></td>
|
||||
<td>Svelte implementations + catalog schemas</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>@json-render/shadcn-svelte/catalog</code></td>
|
||||
<td><code>shadcnComponentDefinitions</code></td>
|
||||
<td>Catalog schemas only (no Svelte dependency, safe for server)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Usage
|
||||
|
||||
Pick the components you need from the standard definitions:
|
||||
|
||||
```typescript
|
||||
import { defineCatalog } from "@json-render/core";
|
||||
import { schema } from "@json-render/svelte/schema";
|
||||
import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog";
|
||||
import { defineRegistry } from "@json-render/svelte";
|
||||
import { shadcnComponents } from "@json-render/shadcn-svelte";
|
||||
|
||||
// Catalog: pick definitions
|
||||
const catalog = defineCatalog(schema, {
|
||||
components: {
|
||||
Card: shadcnComponentDefinitions.Card,
|
||||
Stack: shadcnComponentDefinitions.Stack,
|
||||
Heading: shadcnComponentDefinitions.Heading,
|
||||
Button: shadcnComponentDefinitions.Button,
|
||||
Input: shadcnComponentDefinitions.Input,
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
|
||||
// Registry: pick matching implementations
|
||||
const { registry } = defineRegistry(catalog, {
|
||||
components: {
|
||||
Card: shadcnComponents.Card,
|
||||
Stack: shadcnComponents.Stack,
|
||||
Heading: shadcnComponents.Heading,
|
||||
Button: shadcnComponents.Button,
|
||||
Input: shadcnComponents.Input,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then render in your Svelte component:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Renderer, JsonUIProvider } from "@json-render/svelte";
|
||||
|
||||
export let spec;
|
||||
export let registry;
|
||||
</script>
|
||||
|
||||
<JsonUIProvider initialState={spec?.state ?? {}}>
|
||||
<Renderer {spec} {registry} />
|
||||
</JsonUIProvider>
|
||||
```
|
||||
|
||||
## Available Components
|
||||
|
||||
### Layout
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Card</code></td>
|
||||
<td>Container card with optional title, description, maxWidth, centered</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Stack</code></td>
|
||||
<td>Flex container with direction, gap, align, justify</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Grid</code></td>
|
||||
<td>Grid layout with columns (1-6) and gap</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Separator</code></td>
|
||||
<td>Visual separator line with orientation</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Navigation
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Tabs</code></td>
|
||||
<td>Tabbed navigation with tabs array, defaultValue, value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Accordion</code></td>
|
||||
<td>Collapsible sections with items array and type (single/multiple)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Collapsible</code></td>
|
||||
<td>Single collapsible section with title and defaultOpen</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Pagination</code></td>
|
||||
<td>Page navigation with totalPages and page</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Overlay
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Dialog</code></td>
|
||||
<td>Modal dialog with title, description, openPath</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Drawer</code></td>
|
||||
<td>Bottom drawer with title, description, openPath</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Tooltip</code></td>
|
||||
<td>Hover tooltip with content and text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Popover</code></td>
|
||||
<td>Click-triggered popover with trigger and content</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DropdownMenu</code></td>
|
||||
<td>Dropdown menu with label and items array</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Content
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Heading</code></td>
|
||||
<td>Heading text with level (h1-h4)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Text</code></td>
|
||||
<td>Paragraph with variant (body, caption, muted, lead, code)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Image</code></td>
|
||||
<td>Image with alt, width, height</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Avatar</code></td>
|
||||
<td>User avatar with src, name, size</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Badge</code></td>
|
||||
<td>Status badge with text and variant</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Alert</code></td>
|
||||
<td>Alert banner with title, message, type</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Carousel</code></td>
|
||||
<td>Horizontally scrollable carousel with items</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Table</code></td>
|
||||
<td>Data table with columns and rows</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Feedback
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Progress</code></td>
|
||||
<td>Progress bar with value, max, label</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Skeleton</code></td>
|
||||
<td>Loading placeholder with width, height, rounded</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Spinner</code></td>
|
||||
<td>Loading spinner with size and label</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Input
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Button</code></td>
|
||||
<td>Clickable button with label, variant, disabled</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Link</code></td>
|
||||
<td>Anchor link with label and href</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Input</code></td>
|
||||
<td>Text input with label, name, type, placeholder, value, checks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Textarea</code></td>
|
||||
<td>Multi-line text input with label, name, placeholder, rows, value, checks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Select</code></td>
|
||||
<td>Dropdown select with label, name, options, value, checks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Checkbox</code></td>
|
||||
<td>Checkbox with label, name, checked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Radio</code></td>
|
||||
<td>Radio button group with label, name, options, value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Switch</code></td>
|
||||
<td>Toggle switch with label, name, checked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Slider</code></td>
|
||||
<td>Range slider with label, min, max, step, value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Toggle</code></td>
|
||||
<td>Toggle button with label, pressed, variant</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ToggleGroup</code></td>
|
||||
<td>Group of toggle buttons with items, type, value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ButtonGroup</code></td>
|
||||
<td>Group of buttons with buttons array and selected</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Notes
|
||||
|
||||
- The `/catalog` entry point has no Svelte dependency -- use it for server-side prompt generation
|
||||
- Components use Tailwind CSS classes -- your app must have Tailwind configured
|
||||
- Component implementations use bundled shadcn-svelte primitives (not your app's `$lib/components/ui/`)
|
||||
- Form inputs support `checks` for validation (type + message pairs) and `validateOn` for timing
|
||||
- Events: inputs emit `change`/`submit`/`focus`/`blur`; buttons emit `press`; selects emit `change`/`select`
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# example-chat
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
- @json-render/shadcn@0.16.0
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/shadcn@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
-6
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-chat",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# example-dashboard
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/codegen@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/codegen@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
-6
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-dashboard",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# example-game-engine
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
- @json-render/react-three-fiber@0.16.0
|
||||
- @json-render/yaml@0.16.0
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/react-three-fiber@0.15.0
|
||||
- @json-render/yaml@0.15.0
|
||||
Vendored
-6
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-game-engine",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# example-image
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/image@0.16.0
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/image@0.15.0
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
-6
@@ -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.
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-image",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# example-ink-chat
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/ink@0.16.0
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/ink@0.15.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-ink-chat",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -85,29 +85,72 @@ function getStepHint(type: string, isLast: boolean): string {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System prompt — embeds catalog documentation so the AI knows what components
|
||||
// are available and how to output JSONL patches.
|
||||
// System prompt — handwritten design guidance + catalog documentation.
|
||||
// Follows the same pattern as examples/chat/lib/agent.ts: a rich
|
||||
// AGENT_INSTRUCTIONS string with catalog.prompt() appended at the end.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SYSTEM_PROMPT = catalog.prompt({
|
||||
system:
|
||||
"You are a terminal chat assistant. Use tools for real-time data (web_search, get_weather, get_hacker_news, get_github_repo, get_crypto_price), then render results as rich terminal UIs.",
|
||||
const AGENT_INSTRUCTIONS = `You are a terminal assistant that renders polished, information-dense UIs. You call tools for real-time data, then build clean terminal dashboards.
|
||||
|
||||
WORKFLOW:
|
||||
1. Call the appropriate tools to gather real data. Use web_search for topics not covered by the specialized tools (get_weather, get_hacker_news, get_github_repo, get_crypto_price).
|
||||
2. While tools run, output a single short status line (e.g. "Looking up weather data..."). This is the ONLY text allowed outside the spec fence.
|
||||
3. After tools return, output ALL content inside a \`\`\`spec fence. Never write paragraphs of prose outside the fence.
|
||||
4. For simple text replies (greetings, clarifications), still use a \`\`\`spec with a Markdown component.
|
||||
|
||||
DESIGN PRINCIPLES:
|
||||
- HIERARCHY: Every response needs clear visual structure. Start with an h1 Heading for the topic. Use h2 Headings for subsections. Use Card to group related content into shaded areas — Cards render as subtle background fills, not bordered boxes.
|
||||
- LEAD WITH THE STORY: Open with a brief Markdown paragraph (2-3 sentences) that tells the user the key insight or takeaway. Don't just dump data — frame it.
|
||||
- SUMMARY METRICS: After the narrative, show 2-4 Metric components for the most important numbers. Metric displays a dim label, bold value, and optional colored trend (up=green, down=red). Group them in a horizontal Box (flexDirection: row, gap: 3) so they read like a dashboard header. Use KeyValue only for simple label:value pairs that don't need emphasis.
|
||||
- DETAIL SECTIONS: Below the summary, use h2 Headings to introduce each section, followed by a single focused visualization (Table, BarChart, or set of KeyValues).
|
||||
- ONE REPRESENTATION PER DATA POINT: Never show the same value as both a number and a percentage and a bar. Pick the most meaningful format. Use BarChart with showValues:true OR showPercentage:true, not both.
|
||||
- TABLES: Always set explicit column widths so columns don't collapse. Use headerColor:"cyan". Keep column headers short (abbreviate if needed). Right-align numeric columns.
|
||||
- CHARTS: Use distinct colors per bar in BarChart. Good palette: cyan, green, yellow, magenta, blue, red. Use Sparkline for compact inline trends alongside other content.
|
||||
- COLOR STRATEGY: Use color with intention, not decoration. cyan for labels and headers. green for positive values, growth, success. red for negative values, decline, errors. yellow for warnings or neutral highlights. dimColor:true for secondary/supporting text. Avoid coloring everything — contrast comes from restraint.
|
||||
- TABLES: Use borderStyle:"single" on Tables for a clean outline. Do NOT put Tables inside Cards — Tables have their own border and don't need additional wrapping.
|
||||
- SPACING: Use gap:1 between sections. Don't over-pad. Keep the UI compact and scannable. NEVER add padding to the root element — the app already provides outer padding.
|
||||
- WIDTH: Target 80 columns. Set explicit widths on Tables (total columns should sum to ~70-75). Use wrap:"truncate-end" on Text in tight spaces.
|
||||
- CALLOUTS: Use Callout for key takeaways, important notes, tips, and warnings. Set type (info/tip/warning/important) for a colored left border accent. Keep content concise — one key point per Callout.
|
||||
- TIMELINES: Use Timeline for historical events, step-by-step processes, and milestones. Set status per item (completed/current/upcoming) for colored dots. Include dates when available.
|
||||
- NEVER use emojis anywhere — not in text, labels, titles, table cells, Heading text, or component props. Plain text only.
|
||||
|
||||
DASHBOARD PATTERN (use for data-heavy responses):
|
||||
Root Box (column, gap:1) >
|
||||
Heading (h1, topic title)
|
||||
Markdown (2-3 sentence summary with key takeaway)
|
||||
Box (row, gap:3) > [Metric, Metric, Metric] (top-line metrics, no Card)
|
||||
Heading (h2, section title)
|
||||
Table (borderStyle:"single")
|
||||
Card (title:"Section Name") > BarChart (bar charts go in a titled Card — the Card title replaces h2)
|
||||
Callout (type:"tip", key takeaway or closing note)
|
||||
Card wrapping rules: Wrap BarCharts in a Card with a title. Do NOT wrap Metrics or Tables in Cards — Metrics stand alone, Tables have their own border.
|
||||
|
||||
COMPARISON PATTERN:
|
||||
Use BarChart when you want the user to see relative magnitudes at a glance.
|
||||
Use Table when there are 3+ columns of mixed data types.
|
||||
Never use both for the same data.
|
||||
|
||||
TREND PATTERN:
|
||||
Use Sparkline for compact inline trend next to a KeyValue.
|
||||
Use BarChart with year/period labels for detailed time-series.
|
||||
|
||||
INTERACTIVITY:
|
||||
- You can create interactive forms, surveys, and selection interfaces. The user navigates with arrow keys, selects with Space/Enter, and types into text fields.
|
||||
- ALWAYS include a submit action on interactive UIs. Add a Text or StatusLine telling the user how to submit. Wire submit events to a "submit" action — the app collects form state automatically.
|
||||
- ALWAYS populate the state field with sensible defaults for all bound values.
|
||||
- Use $bindState on interactive components for two-way binding. Example: { "value": { "$state": "/choice" }, "$bindState": { "value": "/choice" } }.
|
||||
- Use Tabs for multi-section surveys. Use ConfirmInput for yes/no prompts.
|
||||
- After receiving form data, acknowledge the user's choices meaningfully — don't just echo them back.
|
||||
|
||||
${catalog.prompt({
|
||||
mode: "inline",
|
||||
customRules: [
|
||||
"ALL text MUST go inside the spec using the Markdown component. NEVER write prose, summaries, or explanations outside the ```spec fence. The ONLY text allowed outside the fence is a single short status like 'Searching…' while tools run.",
|
||||
"For text-only answers (greetings, clarifications), still output a ```spec with a Markdown component.",
|
||||
"Prefer Table for structured data (forecasts, leaderboards, language breakdowns) and KeyValue for label-value pairs (stats, metadata).",
|
||||
|
||||
// Interactive components
|
||||
"INTERACTIVE UIs: You can create interactive forms, surveys, and selection interfaces that the user actually interacts with in the terminal. The user navigates with arrow keys, selects with Space/Enter, and types into text fields.",
|
||||
"When creating interactive UIs, ALWAYS include a submit action. Add an element (usually a Text or StatusLine) that tells the user to press Enter or a specific key to submit. Wire up the submit event to a 'submit' action — the app will collect the form state and send it back to you automatically.",
|
||||
"For interactive specs, ALWAYS populate the state field with sensible defaults for all bound values. Example: if a Select binds to /framework, include state: { framework: 'react' }.",
|
||||
'IMPORTANT: Use $bindState on interactive components so their values are written back to state. Example for Select: { "value": { "$state": "/choice" }, "$bindState": { "value": "/choice" } }.',
|
||||
"For surveys/quizzes with multiple sections, use Tabs to organize them. Bind the active tab to state and use visible conditions on children.",
|
||||
"For confirmation prompts, use ConfirmInput with a clear message. The confirm/deny events can trigger actions.",
|
||||
"After receiving submitted form data, acknowledge the user's choices and respond to them — don't just repeat what they selected.",
|
||||
"ALL text MUST go inside the spec using the Markdown component. The ONLY text outside the fence is a short tool-status line.",
|
||||
"For text-only answers, still output a spec with a Markdown component.",
|
||||
"Prefer Table for structured data and KeyValue for label-value pairs.",
|
||||
"NEVER use emojis anywhere in your output. Plain text only.",
|
||||
],
|
||||
});
|
||||
})}`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -236,7 +279,6 @@ function MessageView({ message }: { message: Message }) {
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>AI:</Text>
|
||||
{message.spec ? (
|
||||
<JSONUIProvider initialState={message.spec.state ?? {}}>
|
||||
<DisableFocus />
|
||||
@@ -317,9 +359,6 @@ function LiveInteractiveSpec({
|
||||
if (interactiveKeys.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color="green">
|
||||
AI:
|
||||
</Text>
|
||||
<JSONUIProvider store={store} handlers={handlers}>
|
||||
<Renderer spec={spec} />
|
||||
</JSONUIProvider>
|
||||
@@ -331,9 +370,6 @@ function LiveInteractiveSpec({
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color="green">
|
||||
AI:
|
||||
</Text>
|
||||
{interactiveKeys.length > 1 && (
|
||||
<Text dimColor>
|
||||
Step {step + 1} of {interactiveKeys.length}
|
||||
@@ -417,7 +453,7 @@ export function App() {
|
||||
try {
|
||||
const result = streamText({
|
||||
model: gateway(process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
|
||||
system: SYSTEM_PROMPT,
|
||||
system: AGENT_INSTRUCTIONS,
|
||||
messages: history,
|
||||
temperature: 0.7,
|
||||
abortSignal: controller.signal,
|
||||
@@ -535,11 +571,26 @@ export function App() {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1} minHeight={stdout.rows}>
|
||||
{/* Header */}
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>json-render terminal chat</Text>
|
||||
<Text dimColor> (Ctrl+C to exit)</Text>
|
||||
<Box marginBottom={1} gap={1}>
|
||||
<Text bold color="cyan">
|
||||
json-render
|
||||
</Text>
|
||||
<Text dimColor>Ctrl+C to exit</Text>
|
||||
</Box>
|
||||
|
||||
{/* Empty state — show example prompts when no conversation yet */}
|
||||
{messages.length === 0 && !isStreaming && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text dimColor>Try asking:</Text>
|
||||
<Box flexDirection="column" paddingLeft={2} marginTop={1} gap={0}>
|
||||
<Text dimColor>{" weather in tokyo"}</Text>
|
||||
<Text dimColor>{" top hacker news stories"}</Text>
|
||||
<Text dimColor>{" tell me about vercel/next.js"}</Text>
|
||||
<Text dimColor>{" bitcoin price"}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Message history — collapsed when interactive wizard is active */}
|
||||
{liveSpec && !isStreaming ? (
|
||||
<>
|
||||
@@ -568,10 +619,9 @@ export function App() {
|
||||
{/* Live spec preview while streaming */}
|
||||
{isStreaming && streamingSpec && streamingSpec.root && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>AI:</Text>
|
||||
<JSONUIProvider initialState={streamingSpec.state ?? {}}>
|
||||
<DisableFocus />
|
||||
<Renderer spec={streamingSpec} />
|
||||
<Renderer spec={streamingSpec} loading />
|
||||
</JSONUIProvider>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -275,7 +275,8 @@ export const getGitHubRepo = tool({
|
||||
const languageBreakdown = Object.entries(languages)
|
||||
.map(([lang, bytes]) => ({
|
||||
language: lang,
|
||||
percentage: Math.round((bytes / totalBytes) * 100),
|
||||
percentage:
|
||||
totalBytes > 0 ? Math.round((bytes / totalBytes) * 100) : 0,
|
||||
}))
|
||||
.sort((a, b) => b.percentage - a.percentage)
|
||||
.slice(0, 6);
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# example-mcp
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/mcp@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
- @json-render/shadcn@0.16.0
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/mcp@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/shadcn@0.15.0
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-mcp",
|
||||
"version": "0.1.5",
|
||||
"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 };
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createNextApp } from "@json-render/next/server";
|
||||
import { getSpec } from "./spec-store";
|
||||
|
||||
export const { getPageData, generateMetadata, generateStaticParams } =
|
||||
createNextApp({
|
||||
spec: () => getSpec(),
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineCatalog } from "@json-render/core";
|
||||
import { schema } from "@json-render/next/server";
|
||||
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||
import { websiteComponentDefinitions } from "./website-catalog";
|
||||
|
||||
export const catalog = defineCatalog(schema, {
|
||||
components: {
|
||||
...shadcnComponentDefinitions,
|
||||
...websiteComponentDefinitions,
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
import type { NextAppSpec } from "@json-render/next";
|
||||
|
||||
export const defaultSpec: NextAppSpec = {
|
||||
metadata: {
|
||||
title: {
|
||||
default: "Acme Inc",
|
||||
template: "%s | Acme Inc",
|
||||
},
|
||||
description: "We build the future of software.",
|
||||
icons: "/icon.svg",
|
||||
},
|
||||
|
||||
state: {},
|
||||
|
||||
layouts: {
|
||||
main: {
|
||||
root: "wrapper",
|
||||
elements: {
|
||||
wrapper: {
|
||||
type: "Stack",
|
||||
props: {
|
||||
direction: "vertical",
|
||||
gap: "none",
|
||||
align: "stretch",
|
||||
className: "min-h-screen",
|
||||
},
|
||||
children: ["header", "content", "footer"],
|
||||
},
|
||||
header: {
|
||||
type: "Header",
|
||||
props: {
|
||||
brand: "Acme Inc",
|
||||
links: [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
],
|
||||
variant: "simple",
|
||||
},
|
||||
},
|
||||
content: {
|
||||
type: "Slot",
|
||||
props: {},
|
||||
},
|
||||
footer: {
|
||||
type: "Footer",
|
||||
props: {
|
||||
brand: "Acme Inc",
|
||||
links: [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
],
|
||||
copyright: "2026 Acme Inc. All rights reserved.",
|
||||
variant: "simple",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
routes: {
|
||||
"/": {
|
||||
layout: "main",
|
||||
metadata: {
|
||||
title: "Home",
|
||||
description: "Welcome to Acme Inc - we build the future of software.",
|
||||
},
|
||||
page: {
|
||||
root: "page",
|
||||
elements: {
|
||||
page: {
|
||||
type: "Stack",
|
||||
props: {
|
||||
direction: "vertical",
|
||||
gap: "none",
|
||||
align: "stretch",
|
||||
},
|
||||
children: ["hero", "features", "testimonials", "cta"],
|
||||
},
|
||||
hero: {
|
||||
type: "Hero",
|
||||
props: {
|
||||
headline: "Build the future with Acme",
|
||||
description:
|
||||
"We help companies ship better software, faster. Our platform provides the tools, infrastructure, and insights you need to build world-class products.",
|
||||
primaryCta: { label: "Get Started", href: "/contact" },
|
||||
secondaryCta: { label: "Learn More", href: "/about" },
|
||||
badge: "Now in Beta",
|
||||
variant: "centered",
|
||||
},
|
||||
},
|
||||
features: {
|
||||
type: "Features",
|
||||
props: {
|
||||
headline: "Why Acme?",
|
||||
description:
|
||||
"Everything you need to build, deploy, and scale your applications.",
|
||||
items: [
|
||||
{
|
||||
title: "Lightning Fast",
|
||||
description:
|
||||
"Optimized for speed with edge-first architecture and smart caching.",
|
||||
},
|
||||
{
|
||||
title: "Secure by Default",
|
||||
description:
|
||||
"Enterprise-grade security with end-to-end encryption and SOC 2 compliance.",
|
||||
},
|
||||
{
|
||||
title: "Developer First",
|
||||
description:
|
||||
"APIs, SDKs, and CLI tools designed to fit your workflow perfectly.",
|
||||
},
|
||||
],
|
||||
columns: 3,
|
||||
variant: "cards",
|
||||
},
|
||||
},
|
||||
testimonials: {
|
||||
type: "Testimonials",
|
||||
props: {
|
||||
headline: "Trusted by industry leaders",
|
||||
items: [
|
||||
{
|
||||
quote:
|
||||
"Acme has completely transformed how we ship products. Our deployment time went from hours to minutes.",
|
||||
author: "Jordan Lee",
|
||||
role: "VP Engineering at Globex",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"The developer experience is unmatched. Our team onboarded in a single afternoon.",
|
||||
author: "Priya Patel",
|
||||
role: "CTO at Initech",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"We cut our infrastructure costs by 40% within the first quarter of switching to Acme.",
|
||||
author: "Sam Ortiz",
|
||||
role: "Head of Platform at Hooli",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
cta: {
|
||||
type: "CTA",
|
||||
props: {
|
||||
headline: "Ready to get started?",
|
||||
description:
|
||||
"Join thousands of teams building better software with Acme.",
|
||||
buttonLabel: "Contact Us",
|
||||
buttonHref: "/contact",
|
||||
variant: "banner",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"/about": {
|
||||
layout: "main",
|
||||
metadata: {
|
||||
title: "About",
|
||||
description:
|
||||
"Learn about our mission, values, and the team behind Acme.",
|
||||
},
|
||||
page: {
|
||||
root: "page",
|
||||
elements: {
|
||||
page: {
|
||||
type: "Stack",
|
||||
props: {
|
||||
direction: "vertical",
|
||||
gap: "none",
|
||||
align: "stretch",
|
||||
},
|
||||
children: ["hero", "features", "team", "cta"],
|
||||
},
|
||||
hero: {
|
||||
type: "Hero",
|
||||
props: {
|
||||
headline: "About Acme Inc",
|
||||
description:
|
||||
"Founded in 2024, we are on a mission to make software development accessible to everyone. We believe the best tools should be simple, powerful, and delightful to use.",
|
||||
variant: "centered",
|
||||
primaryCta: null,
|
||||
secondaryCta: null,
|
||||
badge: null,
|
||||
},
|
||||
},
|
||||
features: {
|
||||
type: "Features",
|
||||
props: {
|
||||
headline: "Our Values",
|
||||
items: [
|
||||
{
|
||||
title: "Simplicity",
|
||||
description:
|
||||
"We remove complexity so you can focus on building great products.",
|
||||
},
|
||||
{
|
||||
title: "Transparency",
|
||||
description:
|
||||
"Open communication, honest pricing, and clear documentation.",
|
||||
},
|
||||
{
|
||||
title: "Craftsmanship",
|
||||
description:
|
||||
"Every detail matters. We sweat the small stuff so you don't have to.",
|
||||
},
|
||||
],
|
||||
columns: 3,
|
||||
variant: "simple",
|
||||
description: null,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
type: "Team",
|
||||
props: {
|
||||
headline: "Meet the Team",
|
||||
description:
|
||||
"The people behind Acme who are passionate about building great developer tools.",
|
||||
members: [
|
||||
{
|
||||
name: "Alex Chen",
|
||||
role: "CEO & Co-founder",
|
||||
bio: "Previously at Vercel and Google. Passionate about developer experience.",
|
||||
},
|
||||
{
|
||||
name: "Sarah Kim",
|
||||
role: "CTO & Co-founder",
|
||||
bio: "Built distributed systems at AWS. Loves solving hard infrastructure problems.",
|
||||
},
|
||||
{
|
||||
name: "Marcus Rivera",
|
||||
role: "Head of Design",
|
||||
bio: "Former design lead at Figma and Stripe. Obsessed with clarity and craft.",
|
||||
},
|
||||
],
|
||||
variant: "grid",
|
||||
},
|
||||
},
|
||||
cta: {
|
||||
type: "CTA",
|
||||
props: {
|
||||
headline: "Want to join us?",
|
||||
description:
|
||||
"We are always looking for talented people to join our team.",
|
||||
buttonLabel: "Get in Touch",
|
||||
buttonHref: "/contact",
|
||||
variant: "centered",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"/contact": {
|
||||
layout: "main",
|
||||
metadata: {
|
||||
title: "Contact",
|
||||
description: "Get in touch with the Acme team.",
|
||||
},
|
||||
page: {
|
||||
root: "page",
|
||||
elements: {
|
||||
page: {
|
||||
type: "Stack",
|
||||
props: {
|
||||
direction: "vertical",
|
||||
gap: "none",
|
||||
align: "stretch",
|
||||
},
|
||||
children: ["form"],
|
||||
},
|
||||
form: {
|
||||
type: "ContactForm",
|
||||
props: {
|
||||
headline: "Get in Touch",
|
||||
description:
|
||||
"Have a question or want to work with us? Fill out the form below and we'll get back to you within 24 hours.",
|
||||
fields: [
|
||||
{
|
||||
label: "Name",
|
||||
type: "text" as const,
|
||||
placeholder: "Your name",
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
type: "email" as const,
|
||||
placeholder: "you@example.com",
|
||||
},
|
||||
{
|
||||
label: "Message",
|
||||
type: "textarea" as const,
|
||||
placeholder: "Tell us about your project...",
|
||||
},
|
||||
],
|
||||
submitLabel: "Send Message",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { defineRegistry } from "@json-render/react";
|
||||
import { shadcnComponents } from "@json-render/shadcn";
|
||||
import { catalog } from "./catalog";
|
||||
import { websiteComponents } from "./website-components";
|
||||
|
||||
export const { registry } = defineRegistry(catalog, {
|
||||
components: {
|
||||
...shadcnComponents,
|
||||
...websiteComponents,
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { NextAppSpec } from "@json-render/next";
|
||||
import { defaultSpec } from "./default-spec";
|
||||
|
||||
let currentSpec: NextAppSpec = defaultSpec;
|
||||
|
||||
export function getSpec(): NextAppSpec {
|
||||
return currentSpec;
|
||||
}
|
||||
|
||||
export function setSpec(spec: NextAppSpec): void {
|
||||
currentSpec = spec;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const linkSchema = z.object({
|
||||
label: z.string(),
|
||||
href: z.string(),
|
||||
});
|
||||
|
||||
const ctaSchema = z.object({
|
||||
label: z.string(),
|
||||
href: z.string(),
|
||||
});
|
||||
|
||||
export const websiteComponentDefinitions = {
|
||||
Header: {
|
||||
props: z.object({
|
||||
brand: z.string(),
|
||||
links: z.array(linkSchema),
|
||||
variant: z.enum(["simple", "centered"]).nullable(),
|
||||
}),
|
||||
description:
|
||||
"Site header / navigation bar. Simple: brand left, links right. Centered: brand centered above links.",
|
||||
example: {
|
||||
brand: "Acme Inc",
|
||||
links: [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
],
|
||||
variant: "simple",
|
||||
},
|
||||
},
|
||||
|
||||
Hero: {
|
||||
props: z.object({
|
||||
headline: z.string(),
|
||||
description: z.string(),
|
||||
primaryCta: ctaSchema.nullable(),
|
||||
secondaryCta: ctaSchema.nullable(),
|
||||
badge: z.string().nullable(),
|
||||
variant: z.enum(["centered", "left-aligned"]).nullable(),
|
||||
}),
|
||||
description:
|
||||
"Landing hero section with headline, description, and call-to-action buttons.",
|
||||
example: {
|
||||
headline: "Build something great",
|
||||
description: "A modern platform for modern teams.",
|
||||
primaryCta: { label: "Get Started", href: "/contact" },
|
||||
variant: "centered",
|
||||
},
|
||||
},
|
||||
|
||||
Features: {
|
||||
props: z.object({
|
||||
headline: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
items: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
columns: z.number().nullable(),
|
||||
variant: z.enum(["cards", "simple"]).nullable(),
|
||||
}),
|
||||
description:
|
||||
"Feature highlights section. Cards: bordered cards per item. Simple: clean text layout.",
|
||||
example: {
|
||||
headline: "Why choose us",
|
||||
items: [
|
||||
{ title: "Fast", description: "Blazing performance." },
|
||||
{ title: "Secure", description: "Enterprise-grade security." },
|
||||
{ title: "Simple", description: "Easy to use." },
|
||||
],
|
||||
columns: 3,
|
||||
variant: "cards",
|
||||
},
|
||||
},
|
||||
|
||||
Team: {
|
||||
props: z.object({
|
||||
headline: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
role: z.string(),
|
||||
bio: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
variant: z.enum(["grid", "list"]).nullable(),
|
||||
}),
|
||||
description: "Team members section. Grid: card grid. List: vertical list.",
|
||||
example: {
|
||||
headline: "Our Team",
|
||||
members: [
|
||||
{ name: "Alice", role: "CEO", bio: null },
|
||||
{ name: "Bob", role: "CTO", bio: null },
|
||||
],
|
||||
variant: "grid",
|
||||
},
|
||||
},
|
||||
|
||||
Testimonials: {
|
||||
props: z.object({
|
||||
headline: z.string().nullable(),
|
||||
items: z.array(
|
||||
z.object({
|
||||
quote: z.string(),
|
||||
author: z.string(),
|
||||
role: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
description: "Customer testimonials / quotes section.",
|
||||
example: {
|
||||
headline: "What people say",
|
||||
items: [
|
||||
{
|
||||
quote: "Amazing product!",
|
||||
author: "Jane",
|
||||
role: "CEO at Startup",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
ContactForm: {
|
||||
props: z.object({
|
||||
headline: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
type: z.enum(["text", "email", "textarea"]),
|
||||
placeholder: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
submitLabel: z.string(),
|
||||
}),
|
||||
description: "Contact form section with configurable fields.",
|
||||
example: {
|
||||
headline: "Get in Touch",
|
||||
fields: [
|
||||
{ label: "Name", type: "text", placeholder: "Your name" },
|
||||
{ label: "Email", type: "email", placeholder: "you@example.com" },
|
||||
{ label: "Message", type: "textarea", placeholder: "Your message" },
|
||||
],
|
||||
submitLabel: "Send",
|
||||
},
|
||||
},
|
||||
|
||||
Footer: {
|
||||
props: z.object({
|
||||
brand: z.string().nullable(),
|
||||
links: z.array(linkSchema).nullable(),
|
||||
copyright: z.string().nullable(),
|
||||
variant: z.enum(["simple", "columns"]).nullable(),
|
||||
}),
|
||||
description:
|
||||
"Site footer. Simple: centered single row. Columns: multi-column layout.",
|
||||
example: {
|
||||
brand: "Acme Inc",
|
||||
copyright: "2026 Acme Inc.",
|
||||
variant: "simple",
|
||||
},
|
||||
},
|
||||
|
||||
CTA: {
|
||||
props: z.object({
|
||||
headline: z.string(),
|
||||
description: z.string().nullable(),
|
||||
buttonLabel: z.string(),
|
||||
buttonHref: z.string(),
|
||||
variant: z.enum(["banner", "centered"]).nullable(),
|
||||
}),
|
||||
description:
|
||||
"Call-to-action section. Banner: full-width colored background. Centered: clean centered layout.",
|
||||
example: {
|
||||
headline: "Ready to get started?",
|
||||
buttonLabel: "Contact Us",
|
||||
buttonHref: "/contact",
|
||||
variant: "centered",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type WebsiteProps<K extends keyof typeof websiteComponentDefinitions> =
|
||||
z.output<(typeof websiteComponentDefinitions)[K]["props"]>;
|
||||
@@ -0,0 +1,501 @@
|
||||
"use client";
|
||||
|
||||
import type { BaseComponentProps } from "@json-render/react";
|
||||
import type { WebsiteProps } from "./website-catalog";
|
||||
|
||||
// =============================================================================
|
||||
// Header
|
||||
// =============================================================================
|
||||
|
||||
function HeaderComponent({
|
||||
props,
|
||||
}: BaseComponentProps<WebsiteProps<"Header">>) {
|
||||
const variant = props.variant ?? "simple";
|
||||
const links = props.links ?? [];
|
||||
|
||||
if (variant === "centered") {
|
||||
return (
|
||||
<header className="border-b border-border bg-background">
|
||||
<div className="mx-auto max-w-6xl px-6 py-6 flex flex-col items-center gap-4">
|
||||
<span className="text-xl font-bold tracking-tight">
|
||||
{props.brand}
|
||||
</span>
|
||||
<nav className="flex items-center gap-6">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="border-b border-border bg-background sticky top-0 z-50">
|
||||
<div className="mx-auto max-w-6xl px-6 h-16 flex items-center justify-between">
|
||||
<span className="text-lg font-bold tracking-tight">{props.brand}</span>
|
||||
<nav className="flex items-center gap-6">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hero
|
||||
// =============================================================================
|
||||
|
||||
function HeroComponent({ props }: BaseComponentProps<WebsiteProps<"Hero">>) {
|
||||
const variant = props.variant ?? "centered";
|
||||
const isLeft = variant === "left-aligned";
|
||||
|
||||
return (
|
||||
<section className="bg-background">
|
||||
<div
|
||||
className={`mx-auto max-w-4xl px-6 py-24 md:py-32 ${isLeft ? "text-left" : "text-center"}`}
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col gap-6 ${isLeft ? "items-start" : "items-center"}`}
|
||||
>
|
||||
{props.badge && (
|
||||
<span className="inline-flex items-center rounded-full border border-border bg-muted px-3 py-1 text-xs font-medium text-muted-foreground">
|
||||
{props.badge}
|
||||
</span>
|
||||
)}
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl text-foreground">
|
||||
{props.headline}
|
||||
</h1>
|
||||
<p
|
||||
className={`text-lg text-muted-foreground ${isLeft ? "max-w-xl" : "max-w-2xl"}`}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
{(props.primaryCta || props.secondaryCta) && (
|
||||
<div className="flex flex-wrap gap-3 pt-2">
|
||||
{props.primaryCta && (
|
||||
<a
|
||||
href={props.primaryCta.href}
|
||||
className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
{props.primaryCta.label}
|
||||
</a>
|
||||
)}
|
||||
{props.secondaryCta && (
|
||||
<a
|
||||
href={props.secondaryCta.href}
|
||||
className="inline-flex h-11 items-center justify-center rounded-md border border-border bg-background px-6 text-sm font-medium text-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
{props.secondaryCta.label}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Features
|
||||
// =============================================================================
|
||||
|
||||
function FeaturesComponent({
|
||||
props,
|
||||
}: BaseComponentProps<WebsiteProps<"Features">>) {
|
||||
const variant = props.variant ?? "cards";
|
||||
const columns = props.columns ?? 3;
|
||||
const items = props.items ?? [];
|
||||
|
||||
const colClass =
|
||||
columns === 2
|
||||
? "sm:grid-cols-2"
|
||||
: columns === 4
|
||||
? "sm:grid-cols-2 lg:grid-cols-4"
|
||||
: "sm:grid-cols-2 lg:grid-cols-3";
|
||||
|
||||
return (
|
||||
<section className="bg-background">
|
||||
<div className="mx-auto max-w-6xl px-6 py-20">
|
||||
{(props.headline || props.description) && (
|
||||
<div className="mb-12 text-center">
|
||||
{props.headline && (
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
||||
{props.headline}
|
||||
</h2>
|
||||
)}
|
||||
{props.description && (
|
||||
<p className="mt-3 text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={`grid grid-cols-1 gap-6 ${colClass}`}>
|
||||
{items.map((item, i) =>
|
||||
variant === "cards" ? (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-lg border border-border bg-card p-6 shadow-sm"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-card-foreground">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div key={i} className="py-2">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Team
|
||||
// =============================================================================
|
||||
|
||||
function TeamComponent({ props }: BaseComponentProps<WebsiteProps<"Team">>) {
|
||||
const variant = props.variant ?? "grid";
|
||||
const members = props.members ?? [];
|
||||
|
||||
return (
|
||||
<section className="bg-background">
|
||||
<div className="mx-auto max-w-6xl px-6 py-20">
|
||||
{(props.headline || props.description) && (
|
||||
<div className="mb-12 text-center">
|
||||
{props.headline && (
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
||||
{props.headline}
|
||||
</h2>
|
||||
)}
|
||||
{props.description && (
|
||||
<p className="mt-3 text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{variant === "list" ? (
|
||||
<div className="space-y-6 max-w-2xl mx-auto">
|
||||
{members.map((m, i) => (
|
||||
<div key={i} className="flex flex-col gap-1">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{m.name}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">{m.role}</p>
|
||||
{m.bio && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">{m.bio}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{members.map((m, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-lg border border-border bg-card p-6 text-center"
|
||||
>
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted text-lg font-bold text-muted-foreground">
|
||||
{m.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.slice(0, 2)
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-card-foreground">
|
||||
{m.name}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">{m.role}</p>
|
||||
{m.bio && (
|
||||
<p className="mt-2 text-sm text-muted-foreground">{m.bio}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Testimonials
|
||||
// =============================================================================
|
||||
|
||||
function TestimonialsComponent({
|
||||
props,
|
||||
}: BaseComponentProps<WebsiteProps<"Testimonials">>) {
|
||||
const items = props.items ?? [];
|
||||
|
||||
return (
|
||||
<section className="bg-muted/30">
|
||||
<div className="mx-auto max-w-6xl px-6 py-20">
|
||||
{props.headline && (
|
||||
<h2 className="mb-12 text-center text-3xl font-bold tracking-tight text-foreground">
|
||||
{props.headline}
|
||||
</h2>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map((item, i) => (
|
||||
<blockquote
|
||||
key={i}
|
||||
className="rounded-lg border border-border bg-background p-6"
|
||||
>
|
||||
<p className="text-sm text-foreground leading-relaxed">
|
||||
“{item.quote}”
|
||||
</p>
|
||||
<footer className="mt-4 flex flex-col">
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{item.author}
|
||||
</span>
|
||||
{item.role && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{item.role}
|
||||
</span>
|
||||
)}
|
||||
</footer>
|
||||
</blockquote>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ContactForm
|
||||
// =============================================================================
|
||||
|
||||
function ContactFormComponent({
|
||||
props,
|
||||
}: BaseComponentProps<WebsiteProps<"ContactForm">>) {
|
||||
const fields = props.fields ?? [];
|
||||
|
||||
return (
|
||||
<section className="bg-background">
|
||||
<div className="mx-auto max-w-xl px-6 py-20">
|
||||
{(props.headline || props.description) && (
|
||||
<div className="mb-8 text-center">
|
||||
{props.headline && (
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
||||
{props.headline}
|
||||
</h2>
|
||||
)}
|
||||
{props.description && (
|
||||
<p className="mt-3 text-muted-foreground">{props.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
className="rounded-lg border border-border bg-card p-6 shadow-sm"
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{fields.map((field, i) =>
|
||||
field.type === "textarea" ? (
|
||||
<div key={i} className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{field.label}
|
||||
</label>
|
||||
<textarea
|
||||
placeholder={field.placeholder ?? ""}
|
||||
rows={4}
|
||||
className="rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div key={i} className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{field.label}
|
||||
</label>
|
||||
<input
|
||||
type={field.type}
|
||||
placeholder={field.placeholder ?? ""}
|
||||
className="h-10 rounded-md border border-input bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-2 h-11 rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
{props.submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Footer
|
||||
// =============================================================================
|
||||
|
||||
function FooterComponent({
|
||||
props,
|
||||
}: BaseComponentProps<WebsiteProps<"Footer">>) {
|
||||
const variant = props.variant ?? "simple";
|
||||
const links = props.links ?? [];
|
||||
|
||||
if (variant === "columns") {
|
||||
return (
|
||||
<footer className="border-t border-border bg-muted/30">
|
||||
<div className="mx-auto max-w-6xl px-6 py-12">
|
||||
<div className="flex flex-col gap-8 sm:flex-row sm:justify-between">
|
||||
<div>
|
||||
{props.brand && (
|
||||
<span className="text-lg font-bold text-foreground">
|
||||
{props.brand}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{links.length > 0 && (
|
||||
<nav className="flex flex-col gap-2 sm:flex-row sm:gap-6">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
{props.copyright && (
|
||||
<p className="mt-8 text-xs text-muted-foreground">
|
||||
{props.copyright}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<footer className="border-t border-border bg-muted/30">
|
||||
<div className="mx-auto max-w-6xl px-6 py-10 flex flex-col items-center gap-4">
|
||||
{props.brand && (
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{props.brand}
|
||||
</span>
|
||||
)}
|
||||
{links.length > 0 && (
|
||||
<nav className="flex items-center gap-4">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
{props.copyright && (
|
||||
<p className="text-xs text-muted-foreground">{props.copyright}</p>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CTA
|
||||
// =============================================================================
|
||||
|
||||
function CTAComponent({ props }: BaseComponentProps<WebsiteProps<"CTA">>) {
|
||||
const variant = props.variant ?? "centered";
|
||||
|
||||
if (variant === "banner") {
|
||||
return (
|
||||
<section className="bg-primary text-primary-foreground">
|
||||
<div className="mx-auto max-w-6xl px-6 py-16 flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">{props.headline}</h2>
|
||||
{props.description && (
|
||||
<p className="mt-1 text-sm opacity-90">{props.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href={props.buttonHref}
|
||||
className="inline-flex h-11 items-center justify-center rounded-md bg-background px-6 text-sm font-medium text-foreground shadow-sm hover:bg-background/90 transition-colors shrink-0"
|
||||
>
|
||||
{props.buttonLabel}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="bg-muted/30">
|
||||
<div className="mx-auto max-w-3xl px-6 py-20 text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
||||
{props.headline}
|
||||
</h2>
|
||||
{props.description && (
|
||||
<p className="mt-3 text-lg text-muted-foreground">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-8">
|
||||
<a
|
||||
href={props.buttonHref}
|
||||
className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-8 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
{props.buttonLabel}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Export map
|
||||
// =============================================================================
|
||||
|
||||
export const websiteComponents = {
|
||||
Header: HeaderComponent,
|
||||
Hero: HeroComponent,
|
||||
Features: FeaturesComponent,
|
||||
Team: TeamComponent,
|
||||
Testimonials: TestimonialsComponent,
|
||||
ContactForm: ContactFormComponent,
|
||||
Footer: FooterComponent,
|
||||
CTA: CTAComponent,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
allowedDevOrigins: ["next-website-builder-demo.json-render.localhost"],
|
||||
transpilePackages: [
|
||||
"@json-render/core",
|
||||
"@json-render/react",
|
||||
"@json-render/shadcn",
|
||||
],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "example-next-website-builder",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
|
||||
"dev": "portless next-website-builder-demo.json-render next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@json-render/core": "workspace:*",
|
||||
"@json-render/react": "workspace:*",
|
||||
"@json-render/next": "workspace:*",
|
||||
"@json-render/shadcn": "workspace:*",
|
||||
"@visual-json/react": "0.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.7.0",
|
||||
"lucide-react": "^1.6.0",
|
||||
"next": "16.2.1",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-resizable-panels": "^4.7.6",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"zod": "4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internal/typescript-config": "workspace:*",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "19.2.3",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../packages/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [{ "name": "next" }],
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,5 +1,23 @@
|
||||
# example-no-ai
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
- @json-render/shadcn@0.16.0
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/shadcn@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
-6
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-no-ai",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# example-react-email
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react-email@0.16.0
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react-email@0.15.0
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
-6
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-email",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# example-react-native
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react-native@0.16.0
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react-native@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-native",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# example-react-pdf
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react-pdf@0.16.0
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react-pdf@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
-6
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-pdf",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# example-react-three-fiber
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
- @json-render/react-three-fiber@0.16.0
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/react-three-fiber@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
-6
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-three-fiber",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# example-remotion
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/remotion@0.16.0
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/remotion@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
-6
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-remotion",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# example-solid
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/solid@0.16.0
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/solid@0.15.0
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-solid",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
-6
@@ -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,21 @@
|
||||
# com.example.json-render-demo
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.example.json-render-demo",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.12",
|
||||
"description": "Test",
|
||||
"private": true,
|
||||
"license": "~~proprietary~~",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# com.example.json-render-fullpage-demo
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.example.json-render-fullpage-demo",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.12",
|
||||
"description": "Full-page Stripe App example (alpha)",
|
||||
"private": true,
|
||||
"license": "~~proprietary~~",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# svelte-chat
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/svelte@0.16.0
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/svelte@0.15.0
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "svelte-chat",
|
||||
"private": true,
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# example-svelte
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/svelte@0.16.0
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/svelte@0.15.0
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-svelte",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# vite-renderers
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/react@0.16.0
|
||||
- @json-render/solid@0.16.0
|
||||
- @json-render/svelte@0.16.0
|
||||
- @json-render/vue@0.16.0
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/solid@0.15.0
|
||||
- @json-render/svelte@0.15.0
|
||||
- @json-render/vue@0.15.0
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vite-renderers",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# example-vue
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
- @json-render/vue@0.16.0
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/vue@0.15.0
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-vue",
|
||||
"version": "0.1.7",
|
||||
"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
@@ -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",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @json-render/codegen
|
||||
|
||||
## 0.16.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/codegen",
|
||||
"version": "0.14.1",
|
||||
"version": "0.16.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Utilities for generating code from json-render UI trees",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# @json-render/core
|
||||
|
||||
## 0.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 519a538: Add Next.js renderer and shadcn-svelte component library.
|
||||
|
||||
### New:
|
||||
- **`@json-render/next`** -- Next.js renderer that turns JSON specs into full Next.js applications with routes, layouts, SSR, metadata, data loaders, and static generation. Client and server entry points at `@json-render/next` and `@json-render/next/server`. Includes built-in `Link`, `Slot`, error boundary, loading, and not-found components.
|
||||
- **`@json-render/shadcn-svelte`** -- Pre-built shadcn-svelte components for json-render Svelte apps. 36 components built on Svelte 5 + Tailwind CSS with state binding, validation, and action support. Server-safe catalog at `@json-render/shadcn-svelte/catalog`.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- bf3a7ec: Add Ink terminal renderer for interactive terminal UIs.
|
||||
|
||||
### New:
|
||||
- **`@json-render/ink`** -- Terminal UI renderer for json-render, built on Ink. Includes 20+ standard components (Box, Text, Heading, Card, Table, TextInput, Select, MultiSelect, Tabs, etc.), action/validation/focus contexts, two-way state binding, and streaming via `useUIStream`. Server-safe entry points at `@json-render/ink/schema`, `@json-render/ink/catalog`, and `@json-render/ink/server`.
|
||||
|
||||
### Improved:
|
||||
- **Examples** -- new `ink-chat` terminal chat demo, `game-engine` 3D example using react-three-fiber, website examples page with live demos and search.
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/core",
|
||||
"version": "0.14.1",
|
||||
"version": "0.16.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @json-render/image
|
||||
|
||||
## 0.16.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [519a538]
|
||||
- @json-render/core@0.16.0
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/image",
|
||||
"version": "0.14.1",
|
||||
"version": "0.16.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
||||
"keywords": [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user