mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-02 12:00:58 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa90b8b84 | ||
|
|
54a1ecf817 | ||
|
|
dc9e9d17f3 | ||
|
|
1977fb3a11 | ||
|
|
4606c01085 | ||
|
|
c1a700d719 | ||
|
|
6f15faaae0 | ||
|
|
63c339b4bb | ||
|
|
1cc87310c9 | ||
|
|
5b929dffa6 | ||
|
|
512f7fe5c5 |
@@ -17,7 +17,9 @@
|
|||||||
"@json-render/jotai",
|
"@json-render/jotai",
|
||||||
"@json-render/vue",
|
"@json-render/vue",
|
||||||
"@json-render/xstate",
|
"@json-render/xstate",
|
||||||
"@json-render/image"
|
"@json-render/image",
|
||||||
|
"@json-render/mcp",
|
||||||
|
"@json-render/svelte"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"json-render": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.pnp
|
.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
# Local env files
|
# Local env files
|
||||||
.env*
|
.env*
|
||||||
@@ -27,6 +28,7 @@ out/
|
|||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
|
|||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"json-render": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ When asked to prepare a release (e.g. "prepare v0.12.0"):
|
|||||||
- An API reference page at `apps/web/app/(main)/docs/api/<name>/page.mdx`
|
- An API reference page at `apps/web/app/(main)/docs/api/<name>/page.mdx`
|
||||||
- An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`
|
- An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`
|
||||||
- An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)
|
- An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)
|
||||||
- A skill at `skills/json-render-<name>/SKILL.md`
|
- A skill at `skills/<name>/SKILL.md`
|
||||||
- A `packages/<name>/README.md`
|
- A `packages/<name>/README.md`
|
||||||
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
|
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ npm install @json-render/core @json-render/react-pdf
|
|||||||
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
|
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
|
||||||
# or for Vue
|
# or for Vue
|
||||||
npm install @json-render/core @json-render/vue
|
npm install @json-render/core @json-render/vue
|
||||||
|
# or for Svelte
|
||||||
|
npm install @json-render/core @json-render/svelte
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why json-render?
|
## Why json-render?
|
||||||
@@ -28,7 +30,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur
|
|||||||
- **Guardrailed** - AI can only use components in your catalog
|
- **Guardrailed** - AI can only use components in your catalog
|
||||||
- **Predictable** - JSON output matches your schema, every time
|
- **Predictable** - JSON output matches your schema, every time
|
||||||
- **Fast** - Stream and render progressively as the model responds
|
- **Fast** - Stream and render progressively as the model responds
|
||||||
- **Cross-Platform** - React, Vue (web), React Native (mobile) from the same catalog
|
- **Cross-Platform** - React, Vue, Svelte (web), React Native (mobile) from the same catalog
|
||||||
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
|
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -116,16 +118,19 @@ function Dashboard({ spec }) {
|
|||||||
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
||||||
| `@json-render/react` | React renderer, contexts, hooks |
|
| `@json-render/react` | React renderer, contexts, hooks |
|
||||||
| `@json-render/vue` | Vue 3 renderer, composables, providers |
|
| `@json-render/vue` | Vue 3 renderer, composables, providers |
|
||||||
|
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
|
||||||
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
||||||
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
||||||
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
||||||
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
|
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
|
||||||
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
|
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
|
||||||
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
|
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
|
||||||
|
| `@json-render/codegen` | Utilities for generating code from json-render UI trees |
|
||||||
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
|
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
|
||||||
| `@json-render/zustand` | Zustand adapter for `StateStore` |
|
| `@json-render/zustand` | Zustand adapter for `StateStore` |
|
||||||
| `@json-render/jotai` | Jotai adapter for `StateStore` |
|
| `@json-render/jotai` | Jotai adapter for `StateStore` |
|
||||||
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
|
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
|
||||||
|
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
|
||||||
|
|
||||||
## Renderers
|
## Renderers
|
||||||
|
|
||||||
@@ -177,6 +182,23 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
// <Renderer :spec="spec" :registry="registry" />
|
// <Renderer :spec="spec" :registry="registry" />
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Svelte (UI)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/svelte";
|
||||||
|
import { schema } from "@json-render/svelte/schema";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: ({ props, children }) => /* Svelte 5 snippet */,
|
||||||
|
Button: ({ props, emit }) => /* Svelte 5 snippet */,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// In your Svelte component:
|
||||||
|
// <Renderer spec={spec} registry={registry} />
|
||||||
|
```
|
||||||
|
|
||||||
### shadcn/ui (Web)
|
### shadcn/ui (Web)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -462,8 +484,9 @@ pnpm dev
|
|||||||
- http://react-email-demo.json-render.localhost:1355 - React Email Example
|
- http://react-email-demo.json-render.localhost:1355 - React Email Example
|
||||||
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
|
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
|
||||||
- Chat Example: run `pnpm dev` in `examples/chat`
|
- Chat Example: run `pnpm dev` in `examples/chat`
|
||||||
|
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
|
||||||
- Vue Example: run `pnpm dev` in `examples/vue`
|
- Vue Example: run `pnpm dev` in `examples/vue`
|
||||||
- Vite Renderers (React + Vue): run `pnpm dev` in `examples/vite-renderers`
|
- Vite Renderers (React + Vue + Svelte): run `pnpm dev` in `examples/vite-renderers`
|
||||||
- React Native example: run `npx expo start` in `examples/react-native`
|
- React Native example: run `npx expo start` in `examples/react-native`
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# web
|
# web
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/codegen@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/codegen@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export const metadata = pageMetadata("docs/ai-sdk")
|
|||||||
|
|
||||||
# AI SDK Integration
|
# AI SDK Integration
|
||||||
|
|
||||||
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Generate** (standalone UI) and **Chat** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
|
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Standalone** (standalone UI) and **Inline** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -11,9 +11,9 @@ Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless str
|
|||||||
npm install ai @ai-sdk/react
|
npm install ai @ai-sdk/react
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generate Mode
|
## Standalone Mode
|
||||||
|
|
||||||
In generate mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
|
In standalone mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
|
||||||
|
|
||||||
### API Route
|
### API Route
|
||||||
|
|
||||||
@@ -73,9 +73,9 @@ function GenerativeUI() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Chat Mode
|
## Inline Mode
|
||||||
|
|
||||||
In chat mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
|
In inline mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
|
||||||
|
|
||||||
### API Route
|
### API Route
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: yourModel,
|
model: yourModel,
|
||||||
system: catalog.prompt({ mode: "chat" }),
|
system: catalog.prompt({ mode: "inline" }),
|
||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,13 +182,13 @@ const systemPrompt = catalog.prompt({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Chat Mode Prompt
|
### Inline Mode Prompt
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const chatPrompt = catalog.prompt({ mode: "chat" });
|
const inlinePrompt = catalog.prompt({ mode: "inline" });
|
||||||
```
|
```
|
||||||
|
|
||||||
In chat mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
|
In inline mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
|
||||||
|
|
||||||
## Which Mode?
|
## Which Mode?
|
||||||
|
|
||||||
@@ -197,8 +197,8 @@ In chat mode, the prompt instructs the AI to respond conversationally first, the
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Generate</th>
|
<th>Standalone</th>
|
||||||
<th>Chat</th>
|
<th>Inline</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -215,7 +215,7 @@ In chat mode, the prompt instructs the AI to respond conversationally first, the
|
|||||||
<tr>
|
<tr>
|
||||||
<td>System prompt</td>
|
<td>System prompt</td>
|
||||||
<td><code>catalog.prompt()</code></td>
|
<td><code>catalog.prompt()</code></td>
|
||||||
<td><code>{"catalog.prompt({ mode: \"chat\" })"}</code></td>
|
<td><code>{"catalog.prompt({ mode: \"inline\" })"}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Stream utility</td>
|
<td>Stream utility</td>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ interface Catalog {
|
|||||||
interface PromptOptions {
|
interface PromptOptions {
|
||||||
system?: string; // Custom system message intro
|
system?: string; // Custom system message intro
|
||||||
customRules?: string[]; // Additional rules to append
|
customRules?: string[]; // Additional rules to append
|
||||||
mode?: "generate" | "chat"; // Output mode (default: "generate")
|
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpecValidationResult<T> {
|
interface SpecValidationResult<T> {
|
||||||
@@ -370,7 +370,7 @@ Most users should use `pipeJsonRender()` instead, which wraps this transform for
|
|||||||
|
|
||||||
### createMixedStreamParser
|
### createMixedStreamParser
|
||||||
|
|
||||||
Parse a mixed stream of text and JSONL patches (used for Chat + GenUI mode):
|
Parse a mixed stream of text and JSONL patches (used for Inline mode):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createMixedStreamParser } from '@json-render/core';
|
import { createMixedStreamParser } from '@json-render/core';
|
||||||
@@ -389,7 +389,7 @@ parser.flush();
|
|||||||
|
|
||||||
### pipeJsonRender
|
### pipeJsonRender
|
||||||
|
|
||||||
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Chat mode API routes.
|
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Inline mode API routes.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { pipeJsonRender } from '@json-render/core';
|
import { pipeJsonRender } from '@json-render/core';
|
||||||
@@ -403,7 +403,7 @@ const stream = createUIMessageStream({
|
|||||||
return createUIMessageStreamResponse({ stream });
|
return createUIMessageStreamResponse({ stream });
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Generation Modes](/docs/generation-modes) for full Chat mode setup.
|
See [Generation Modes](/docs/generation-modes) for full Inline mode setup.
|
||||||
|
|
||||||
### SpecStream Types
|
### SpecStream Types
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/jotai")
|
||||||
|
|
||||||
|
# @json-render/jotai
|
||||||
|
|
||||||
|
Jotai adapter for json-render's `StateStore` interface.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/jotai @json-render/core @json-render/react jotai
|
||||||
|
```
|
||||||
|
|
||||||
|
## jotaiStateStore
|
||||||
|
|
||||||
|
Create a `StateStore` backed by a Jotai atom.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { jotaiStateStore } from "@json-render/jotai";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Option</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Required</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>atom</code></td>
|
||||||
|
<td><code>{'WritableAtom<StateModel, [StateModel], void>'}</code></td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>A writable atom holding the state model.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>store</code></td>
|
||||||
|
<td>Jotai <code>Store</code></td>
|
||||||
|
<td>No</td>
|
||||||
|
<td>The Jotai store instance. Defaults to a new store created internally. Pass your own to share state with <code>{'<Provider>'}</code>.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { atom } from "jotai";
|
||||||
|
import { jotaiStateStore } from "@json-render/jotai";
|
||||||
|
import { StateProvider } from "@json-render/react";
|
||||||
|
|
||||||
|
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
|
||||||
|
const store = jotaiStateStore({ atom: uiAtom });
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StateProvider store={store}>
|
||||||
|
{/* json-render reads/writes go through Jotai */}
|
||||||
|
</StateProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared Jotai Store
|
||||||
|
|
||||||
|
If your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { atom, createStore } from "jotai";
|
||||||
|
import { Provider as JotaiProvider } from "jotai/react";
|
||||||
|
import { jotaiStateStore } from "@json-render/jotai";
|
||||||
|
import { StateProvider } from "@json-render/react";
|
||||||
|
|
||||||
|
const jStore = createStore();
|
||||||
|
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
|
||||||
|
const store = jotaiStateStore({ atom: uiAtom, store: jStore });
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<JotaiProvider store={jStore}>
|
||||||
|
<StateProvider store={store}>
|
||||||
|
{/* Both json-render and useAtom() see the same state */}
|
||||||
|
</StateProvider>
|
||||||
|
</JotaiProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Re-exports
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Export</th>
|
||||||
|
<th>Source</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>StateStore</code></td>
|
||||||
|
<td><code>@json-render/core</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/mcp")
|
||||||
|
|
||||||
|
# @json-render/mcp
|
||||||
|
|
||||||
|
MCP Apps integration for json-render. Serve json-render UIs as interactive [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
For the iframe-side React UI, also install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/react react react-dom
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [MCP example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp) for a full working example.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
MCP Apps let MCP servers return interactive HTML UIs that render directly inside chat conversations. `@json-render/mcp` bridges json-render catalogs with the MCP Apps protocol:
|
||||||
|
|
||||||
|
1. Your **catalog** defines which components and actions the AI can use
|
||||||
|
2. The **MCP server** exposes the catalog as a tool with the spec schema
|
||||||
|
3. The **bundled HTML** renders json-render specs inside the host's sandboxed iframe
|
||||||
|
4. The AI generates a spec, the host renders it, and users interact with the live UI
|
||||||
|
|
||||||
|
## Server API
|
||||||
|
|
||||||
|
### createMcpApp
|
||||||
|
|
||||||
|
Create a fully-configured MCP server. This is the main entry point.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMcpApp } from "@json-render/mcp";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const server = createMcpApp({
|
||||||
|
name: "My Dashboard",
|
||||||
|
version: "1.0.0",
|
||||||
|
catalog: myCatalog,
|
||||||
|
html: fs.readFileSync("dist/index.html", "utf-8"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.connect(new StdioServerTransport());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CreateMcpAppOptions
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Option</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>name</code></td>
|
||||||
|
<td><code>string</code></td>
|
||||||
|
<td>Server name shown in client UIs</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>version</code></td>
|
||||||
|
<td><code>string</code></td>
|
||||||
|
<td>Server version</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>catalog</code></td>
|
||||||
|
<td><code>Catalog</code></td>
|
||||||
|
<td>json-render catalog defining available components</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>html</code></td>
|
||||||
|
<td><code>string</code></td>
|
||||||
|
<td>Self-contained HTML for the iframe UI</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>tool</code></td>
|
||||||
|
<td><code>McpToolOptions</code></td>
|
||||||
|
<td>Optional tool name/title/description overrides</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### registerJsonRenderTool
|
||||||
|
|
||||||
|
Register a json-render tool on an existing `McpServer`. Use this when you need to add json-render to a server that has other tools.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { registerJsonRenderTool } from "@json-render/mcp";
|
||||||
|
|
||||||
|
registerJsonRenderTool(server, {
|
||||||
|
catalog,
|
||||||
|
name: "render-ui",
|
||||||
|
title: "Render UI",
|
||||||
|
description: "Render an interactive UI",
|
||||||
|
resourceUri: "ui://render-ui/view.html",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### registerJsonRenderResource
|
||||||
|
|
||||||
|
Register the UI resource that serves the bundled HTML.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { registerJsonRenderResource } from "@json-render/mcp";
|
||||||
|
|
||||||
|
registerJsonRenderResource(server, {
|
||||||
|
resourceUri: "ui://render-ui/view.html",
|
||||||
|
html: bundledHtml,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client API (`@json-render/mcp/app`)
|
||||||
|
|
||||||
|
These exports run inside the sandboxed iframe rendered by the MCP host.
|
||||||
|
|
||||||
|
### useJsonRenderApp
|
||||||
|
|
||||||
|
React hook that connects to the MCP host, listens for tool results, and maintains the current json-render spec.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||||
|
import { JSONUIProvider, Renderer } from "@json-render/react";
|
||||||
|
|
||||||
|
function McpAppView({ registry }) {
|
||||||
|
const { spec, loading, connected, error } = useJsonRenderApp({
|
||||||
|
name: "my-app",
|
||||||
|
version: "1.0.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) return <div>Error: {error.message}</div>;
|
||||||
|
if (!spec) return <div>Waiting...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
|
||||||
|
<Renderer spec={spec} registry={registry} loading={loading} />
|
||||||
|
</JSONUIProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UseJsonRenderAppReturn
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>spec</code></td>
|
||||||
|
<td><code>{'Spec | null'}</code></td>
|
||||||
|
<td>Current json-render spec</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>loading</code></td>
|
||||||
|
<td><code>boolean</code></td>
|
||||||
|
<td>Whether the spec is still being received</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>connected</code></td>
|
||||||
|
<td><code>boolean</code></td>
|
||||||
|
<td>Whether connected to the host</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>connecting</code></td>
|
||||||
|
<td><code>boolean</code></td>
|
||||||
|
<td>Whether currently connecting</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>error</code></td>
|
||||||
|
<td><code>{'Error | null'}</code></td>
|
||||||
|
<td>Connection error, if any</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>app</code></td>
|
||||||
|
<td><code>{'App | null'}</code></td>
|
||||||
|
<td>The underlying MCP App instance</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>callServerTool</code></td>
|
||||||
|
<td><code>{'(name, args?) => Promise<void>'}</code></td>
|
||||||
|
<td>Call an MCP server tool and update spec from result</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### buildAppHtml
|
||||||
|
|
||||||
|
Generate a self-contained HTML page from bundled JavaScript and CSS.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { buildAppHtml } from "@json-render/mcp/app";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const html = buildAppHtml({
|
||||||
|
title: "Dashboard",
|
||||||
|
js: fs.readFileSync("dist/app.js", "utf-8"),
|
||||||
|
css: fs.readFileSync("dist/app.css", "utf-8"),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Configuration
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
|
||||||
|
Add to `.cursor/mcp.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"json-render": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "path/to/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Desktop
|
||||||
|
|
||||||
|
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"json-render": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "/absolute/path/to/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Clients
|
||||||
|
|
||||||
|
MCP Apps are supported by Claude (web and desktop), ChatGPT, VS Code (GitHub Copilot), Cursor, Goose, and Postman.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/redux")
|
||||||
|
|
||||||
|
# @json-render/redux
|
||||||
|
|
||||||
|
Redux / Redux Toolkit adapter for json-render's `StateStore` interface.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/redux @json-render/core @json-render/react redux
|
||||||
|
# or with Redux Toolkit (recommended):
|
||||||
|
npm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit
|
||||||
|
```
|
||||||
|
|
||||||
|
## reduxStateStore
|
||||||
|
|
||||||
|
Create a `StateStore` backed by a Redux store.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { reduxStateStore } from "@json-render/redux";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Option</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Required</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>store</code></td>
|
||||||
|
<td><code>Store</code></td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>The Redux store instance.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>selector</code></td>
|
||||||
|
<td><code>{'(state: S) => StateModel'}</code></td>
|
||||||
|
<td>No</td>
|
||||||
|
<td>Select the json-render slice from the Redux state tree. Defaults to <code>{'(state) => state'}</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>dispatch</code></td>
|
||||||
|
<td><code>{'(nextState: StateModel, store: Store) => void'}</code></td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>Dispatch an action that replaces the selected slice with the next state.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { configureStore, createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { reduxStateStore } from "@json-render/redux";
|
||||||
|
import { StateProvider } from "@json-render/react";
|
||||||
|
|
||||||
|
const uiSlice = createSlice({
|
||||||
|
name: "ui",
|
||||||
|
initialState: { count: 0 } as Record<string, unknown>,
|
||||||
|
reducers: {
|
||||||
|
replaceUiState: (_state, action) => action.payload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const reduxStore = configureStore({
|
||||||
|
reducer: { ui: uiSlice.reducer },
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = reduxStateStore({
|
||||||
|
store: reduxStore,
|
||||||
|
selector: (state) => state.ui,
|
||||||
|
dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StateProvider store={store}>
|
||||||
|
{/* json-render reads/writes go through Redux */}
|
||||||
|
</StateProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Re-exports
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Export</th>
|
||||||
|
<th>Source</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>StateStore</code></td>
|
||||||
|
<td><code>@json-render/core</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/svelte")
|
||||||
|
|
||||||
|
# @json-render/svelte
|
||||||
|
|
||||||
|
Svelte 5 components, providers, and helpers for rendering json-render specs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<PackageInstall packages="@json-render/core @json-render/svelte" />
|
||||||
|
|
||||||
|
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
|
||||||
|
|
||||||
|
<PackageInstall packages="svelte zod" />
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Renderer
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<Renderer
|
||||||
|
spec={spec} // Spec | null
|
||||||
|
registry={registry}
|
||||||
|
loading={false}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders a spec with your component registry. If `spec` is `null`, it renders nothing.
|
||||||
|
|
||||||
|
### JsonUIProvider
|
||||||
|
|
||||||
|
Convenience wrapper around `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<JsonUIProvider
|
||||||
|
initialState={{}}
|
||||||
|
handlers={handlers}
|
||||||
|
validationFunctions={validationFunctions}
|
||||||
|
>
|
||||||
|
<Renderer {spec} {registry} />
|
||||||
|
</JsonUIProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## defineRegistry
|
||||||
|
|
||||||
|
Create a typed component registry and action handlers from a catalog.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineRegistry } from "@json-render/svelte";
|
||||||
|
|
||||||
|
const { registry, handlers, executeAction } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
submit: async (params, setState, state) => {
|
||||||
|
// custom action logic
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`handlers` is designed for `JsonUIProvider`/`ActionProvider`. `executeAction` is an imperative helper.
|
||||||
|
|
||||||
|
## Component Props
|
||||||
|
|
||||||
|
Registry components receive `BaseComponentProps<TProps>`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BaseComponentProps<TProps> {
|
||||||
|
props: TProps;
|
||||||
|
children?: Snippet;
|
||||||
|
emit: (event: string) => void;
|
||||||
|
bindings?: Record<string, string>;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `emit("eventName")` to trigger handlers declared in the spec `on` bindings.
|
||||||
|
|
||||||
|
## Context Helpers
|
||||||
|
|
||||||
|
Use these helpers inside Svelte components:
|
||||||
|
|
||||||
|
- `getStateValue(path)` - read/write state via `.current`
|
||||||
|
- `getBoundProp(() => value, () => bindingPath)` - write back resolved `$bindState` / `$bindItem` values
|
||||||
|
- `isVisible(condition)` - evaluate visibility via `.current`
|
||||||
|
- `getAction(name)` - read a registered action handler via `.current`
|
||||||
|
- `getFieldValidation(ctx, path, config)` - get field validation state + actions
|
||||||
|
|
||||||
|
For advanced usage, access full contexts:
|
||||||
|
|
||||||
|
- `getStateContext()`
|
||||||
|
- `getActionContext()`
|
||||||
|
- `getVisibilityContext()`
|
||||||
|
- `getValidationContext()`
|
||||||
|
- `getOptionalValidationContext()`
|
||||||
|
|
||||||
|
## Streaming
|
||||||
|
|
||||||
|
### createUIStream
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const stream = createUIStream({
|
||||||
|
api: "/api/generate-ui",
|
||||||
|
onComplete: (spec) => console.log(spec),
|
||||||
|
});
|
||||||
|
|
||||||
|
await stream.send("Create a login form");
|
||||||
|
|
||||||
|
console.log(stream.spec);
|
||||||
|
console.log(stream.isStreaming);
|
||||||
|
```
|
||||||
|
|
||||||
|
### createChatUI
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const chat = createChatUI({ api: "/api/chat-ui" });
|
||||||
|
await chat.send("Build a settings panel");
|
||||||
|
console.log(chat.messages, chat.isStreaming);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Export
|
||||||
|
|
||||||
|
Use `schema` from `@json-render/svelte` when defining catalogs for Svelte specs.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/xstate")
|
||||||
|
|
||||||
|
# @json-render/xstate
|
||||||
|
|
||||||
|
[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface.
|
||||||
|
|
||||||
|
Requires `@xstate/store` v3+.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/xstate @json-render/core @json-render/react @xstate/store
|
||||||
|
```
|
||||||
|
|
||||||
|
## xstateStoreStateStore
|
||||||
|
|
||||||
|
Create a `StateStore` backed by an `@xstate/store` atom.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { xstateStoreStateStore } from "@json-render/xstate";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Option</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Required</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>atom</code></td>
|
||||||
|
<td><code>{'Atom<StateModel>'}</code></td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>An <code>@xstate/store</code> atom (from <code>createAtom</code>) holding the json-render state model.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createAtom } from "@xstate/store";
|
||||||
|
import { xstateStoreStateStore } from "@json-render/xstate";
|
||||||
|
import { StateProvider } from "@json-render/react";
|
||||||
|
|
||||||
|
const uiAtom = createAtom({ count: 0 });
|
||||||
|
const store = xstateStoreStateStore({ atom: uiAtom });
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StateProvider store={store}>
|
||||||
|
{/* json-render reads/writes go through @xstate/store */}
|
||||||
|
</StateProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Re-exports
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Export</th>
|
||||||
|
<th>Source</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>StateStore</code></td>
|
||||||
|
<td><code>@json-render/core</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/zustand")
|
||||||
|
|
||||||
|
# @json-render/zustand
|
||||||
|
|
||||||
|
Zustand adapter for json-render's `StateStore` interface.
|
||||||
|
|
||||||
|
Requires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/zustand @json-render/core @json-render/react zustand
|
||||||
|
```
|
||||||
|
|
||||||
|
## zustandStateStore
|
||||||
|
|
||||||
|
Create a `StateStore` backed by a Zustand vanilla store.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { zustandStateStore } from "@json-render/zustand";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Option</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Required</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>store</code></td>
|
||||||
|
<td><code>{'StoreApi<S>'}</code></td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>A Zustand vanilla store (from <code>createStore</code> in <code>zustand/vanilla</code>).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>selector</code></td>
|
||||||
|
<td><code>{'(state: S) => StateModel'}</code></td>
|
||||||
|
<td>No</td>
|
||||||
|
<td>Select the json-render slice from the store state. Defaults to the entire state.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>updater</code></td>
|
||||||
|
<td><code>{'(nextState: StateModel, store: StoreApi<S>) => void'}</code></td>
|
||||||
|
<td>No</td>
|
||||||
|
<td>Apply a state change back to the store. Defaults to a shallow merge.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createStore } from "zustand/vanilla";
|
||||||
|
import { zustandStateStore } from "@json-render/zustand";
|
||||||
|
import { StateProvider } from "@json-render/react";
|
||||||
|
|
||||||
|
const bearStore = createStore(() => ({
|
||||||
|
count: 0,
|
||||||
|
name: "Bear",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const store = zustandStateStore({ store: bearStore });
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StateProvider store={store}>
|
||||||
|
{/* json-render reads/writes go through Zustand */}
|
||||||
|
</StateProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Slice
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const appStore = createStore(() => ({
|
||||||
|
ui: { count: 0 },
|
||||||
|
auth: { token: null },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const store = zustandStateStore({
|
||||||
|
store: appStore,
|
||||||
|
selector: (s) => s.ui,
|
||||||
|
updater: (next, s) => s.setState({ ui: next }),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Re-exports
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Export</th>
|
||||||
|
<th>Source</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>StateStore</code></td>
|
||||||
|
<td><code>@json-render/core</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -341,6 +341,8 @@ February 2026
|
|||||||
|
|
||||||
### New: Chat Mode (Inline GenUI)
|
### New: Chat Mode (Inline GenUI)
|
||||||
|
|
||||||
|
> **Note:** These modes were renamed in v0.12.1 — "Generate" is now "Standalone" and "Chat" is now "Inline". The old names are accepted as deprecated aliases.
|
||||||
|
|
||||||
json-render now supports two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets the AI respond conversationally with embedded UI specs, ideal for chatbots and copilot experiences.
|
json-render now supports two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets the AI respond conversationally with embedded UI specs, ideal for chatbots and copilot experiences.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ export const metadata = pageMetadata("docs/generation-modes")
|
|||||||
|
|
||||||
# Generation Modes
|
# Generation Modes
|
||||||
|
|
||||||
json-render supports two modes for AI-generated UI: **Generate mode** for standalone UI and **Chat mode** for inline UI within a conversation.
|
json-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.
|
||||||
|
|
||||||
The mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.
|
The mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.
|
||||||
|
|
||||||
<GenerationModesDiagram />
|
<GenerationModesDiagram />
|
||||||
|
|
||||||
## Generate Mode (Standalone)
|
## Standalone Mode
|
||||||
|
|
||||||
In generate mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
|
In standalone mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
|
||||||
|
|
||||||
This is the default mode and is ideal for:
|
This is the default mode and is ideal for:
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ This is the default mode and is ideal for:
|
|||||||
```typescript
|
```typescript
|
||||||
import { streamText } from "ai";
|
import { streamText } from "ai";
|
||||||
|
|
||||||
// Generate mode is the default (no mode option needed)
|
// Standalone mode is the default (no mode option needed)
|
||||||
const systemPrompt = catalog.prompt({
|
const systemPrompt = catalog.prompt({
|
||||||
customRules: [
|
customRules: [
|
||||||
"Use Card as root for forms and small UIs.",
|
"Use Card as root for forms and small UIs.",
|
||||||
@@ -74,9 +74,9 @@ The AI outputs only JSONL — one patch per line, no surrounding text:
|
|||||||
{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Sign In"}}}
|
{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Sign In"}}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Chat Mode (Inline)
|
## Inline Mode
|
||||||
|
|
||||||
In chat mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
|
In inline mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
|
||||||
|
|
||||||
This is ideal for:
|
This is ideal for:
|
||||||
|
|
||||||
@@ -92,8 +92,8 @@ import { streamText } from "ai";
|
|||||||
import { pipeJsonRender } from "@json-render/core";
|
import { pipeJsonRender } from "@json-render/core";
|
||||||
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
|
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
|
||||||
|
|
||||||
// Enable chat mode
|
// Enable inline mode
|
||||||
const systemPrompt = catalog.prompt({ mode: "chat" });
|
const systemPrompt = catalog.prompt({ mode: "inline" });
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: yourModel,
|
model: yourModel,
|
||||||
@@ -180,8 +180,8 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th />
|
<th />
|
||||||
<th>Generate</th>
|
<th>Standalone</th>
|
||||||
<th>Chat</th>
|
<th>Inline</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -198,7 +198,7 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
|
|||||||
<tr>
|
<tr>
|
||||||
<td>System prompt</td>
|
<td>System prompt</td>
|
||||||
<td><code>{"catalog.prompt()"}</code></td>
|
<td><code>{"catalog.prompt()"}</code></td>
|
||||||
<td><code>{'catalog.prompt({ mode: "chat" })'}</code></td>
|
<td><code>{'catalog.prompt({ mode: "inline" })'}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Stream utility</td>
|
<td>Stream utility</td>
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.
|
|||||||
|
|
||||||
<PackageInstall packages="vue zod" />
|
<PackageInstall packages="vue zod" />
|
||||||
|
|
||||||
|
## For Svelte
|
||||||
|
|
||||||
|
<PackageInstall packages="@json-render/core @json-render/svelte" />
|
||||||
|
|
||||||
|
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
|
||||||
|
|
||||||
|
<PackageInstall packages="svelte zod" />
|
||||||
|
|
||||||
## For React UI with shadcn/ui
|
## For React UI with shadcn/ui
|
||||||
|
|
||||||
Pre-built components for fast prototyping and production use:
|
Pre-built components for fast prototyping and production use:
|
||||||
|
|||||||
@@ -309,10 +309,35 @@ const catalog = defineCatalog(schema, {
|
|||||||
|
|
||||||
const prompt = catalog.prompt();
|
const prompt = catalog.prompt();
|
||||||
|
|
||||||
// Chat mode prompt
|
// Inline mode prompt (formerly "chat")
|
||||||
|
const inlinePrompt = catalog.prompt({ mode: "inline" });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generation Modes
|
||||||
|
|
||||||
|
The generation mode values passed to `catalog.prompt()` have been renamed for clarity:
|
||||||
|
|
||||||
|
- `"generate"` is now `"standalone"`
|
||||||
|
- `"chat"` is now `"inline"`
|
||||||
|
|
||||||
|
The old names are accepted as deprecated aliases, so existing code will continue to work. Update when convenient.
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const prompt = catalog.prompt({ mode: "generate" });
|
||||||
const chatPrompt = catalog.prompt({ mode: "chat" });
|
const chatPrompt = catalog.prompt({ mode: "chat" });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const prompt = catalog.prompt({ mode: "standalone" });
|
||||||
|
const inlinePrompt = catalog.prompt({ mode: "inline" });
|
||||||
|
```
|
||||||
|
|
||||||
|
The default mode (when no `mode` option is provided) is `"standalone"`, which behaves identically to the previous `"generate"` default.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.
|
`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ The result is a native UI built from your own components — not an iframe, not
|
|||||||
- **[Streaming](/docs/streaming)** — Render progressively as the AI responds. Each JSONL patch adds to the spec and the UI updates in real time.
|
- **[Streaming](/docs/streaming)** — Render progressively as the AI responds. Each JSONL patch adds to the spec and the UI updates in real time.
|
||||||
- **[Data Binding](/docs/data-binding)** — Bind props to runtime data with `$state` paths, repeat elements over arrays, and wire two-way input bindings.
|
- **[Data Binding](/docs/data-binding)** — Bind props to runtime data with `$state` paths, repeat elements over arrays, and wire two-way input bindings.
|
||||||
- **[Visibility](/docs/visibility)** — Show or hide elements based on state conditions. The AI can generate conditional UIs without writing logic.
|
- **[Visibility](/docs/visibility)** — Show or hide elements based on state conditions. The AI can generate conditional UIs without writing logic.
|
||||||
- **[Generation Modes](/docs/generation-modes)** — Generate standalone UI (playground/builder) or inline UI within a chat conversation.
|
- **[Generation Modes](/docs/generation-modes)** — Standalone mode for full-page generated UI or inline mode for UI embedded in a conversation.
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
|
||||||
|
export const metadata = pageMetadata("docs/skills")
|
||||||
|
|
||||||
|
# Skills
|
||||||
|
|
||||||
|
json-render ships with skills that teach AI coding agents how to use each package. Install a skill and your agent in Cursor, Claude Code, or Codex can generate json-render UIs without manual guidance.
|
||||||
|
|
||||||
|
## Available Skills
|
||||||
|
|
||||||
|
- **core** — Core schemas, catalogs, and AI prompt generation.
|
||||||
|
- **react** — React renderer that turns JSON specs into React component trees.
|
||||||
|
- **react-pdf** — PDF renderer using `@react-pdf/renderer`.
|
||||||
|
- **react-email** — Email renderer that produces HTML or plain-text emails.
|
||||||
|
- **react-native** — React Native renderer for native mobile UIs.
|
||||||
|
- **shadcn** — Pre-built shadcn/ui components (Radix UI + Tailwind).
|
||||||
|
- **image** — Image renderer that turns JSON specs into SVG and PNG via Satori.
|
||||||
|
- **remotion** — Remotion renderer for video generation from JSON timeline specs.
|
||||||
|
- **vue** — Vue 3 renderer for Vue component trees.
|
||||||
|
- **svelte** — Svelte 5 renderer for Svelte component trees.
|
||||||
|
- **codegen** — Code generation utilities for building custom exporters.
|
||||||
|
- **mcp** — MCP Apps integration for Claude, ChatGPT, Cursor, and VS Code.
|
||||||
|
- **redux** — Redux adapter for json-render's `StateStore` interface.
|
||||||
|
- **zustand** — Zustand adapter for json-render's `StateStore` interface.
|
||||||
|
- **jotai** — Jotai adapter for json-render's `StateStore` interface.
|
||||||
|
- **xstate** — XState Store adapter for json-render's `StateStore` interface.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills add vercel-labs/json-render --skill core
|
||||||
|
npx skills add vercel-labs/json-render --skill react
|
||||||
|
npx skills add vercel-labs/json-render --skill react-pdf
|
||||||
|
npx skills add vercel-labs/json-render --skill react-email
|
||||||
|
npx skills add vercel-labs/json-render --skill react-native
|
||||||
|
npx skills add vercel-labs/json-render --skill shadcn
|
||||||
|
npx skills add vercel-labs/json-render --skill image
|
||||||
|
npx skills add vercel-labs/json-render --skill remotion
|
||||||
|
npx skills add vercel-labs/json-render --skill vue
|
||||||
|
npx skills add vercel-labs/json-render --skill svelte
|
||||||
|
npx skills add vercel-labs/json-render --skill codegen
|
||||||
|
npx skills add vercel-labs/json-render --skill mcp
|
||||||
|
npx skills add vercel-labs/json-render --skill redux
|
||||||
|
npx skills add vercel-labs/json-render --skill zustand
|
||||||
|
npx skills add vercel-labs/json-render --skill jotai
|
||||||
|
npx skills add vercel-labs/json-render --skill xstate
|
||||||
|
```
|
||||||
|
|
||||||
|
After installing, your AI agent will automatically activate the right skill when it encounters a matching request.
|
||||||
|
|
||||||
|
## core
|
||||||
|
|
||||||
|
The foundational skill. Teaches agents how to define catalogs, create schemas, build specs, and generate AI prompts. This is the starting point for any json-render project and covers `defineCatalog`, `defineSchema`, `specSchema`, `toPrompt`, and the full spec format.
|
||||||
|
|
||||||
|
## react
|
||||||
|
|
||||||
|
Teaches agents how to render JSON specs as React component trees using `JsonRender`, `JsonRenderClient`, and `useJsonRender`. Covers custom component registries, client-side interactivity, state management, and streaming integration.
|
||||||
|
|
||||||
|
## react-pdf
|
||||||
|
|
||||||
|
Teaches agents how to generate PDFs from JSON specs using `@react-pdf/renderer`. Covers the PDF-specific component registry, page layout, and styling.
|
||||||
|
|
||||||
|
## react-email
|
||||||
|
|
||||||
|
Teaches agents how to render JSON specs as HTML or plain-text emails using React Email components. Covers the email-specific registry and rendering pipeline.
|
||||||
|
|
||||||
|
## react-native
|
||||||
|
|
||||||
|
Teaches agents how to render JSON specs as native mobile UIs with React Native. Covers the native component registry and platform-specific considerations.
|
||||||
|
|
||||||
|
## shadcn
|
||||||
|
|
||||||
|
Teaches agents how to use the pre-built shadcn/ui component registry with json-render. Includes Radix UI primitives, Tailwind styling, and the full set of available shadcn components.
|
||||||
|
|
||||||
|
## image
|
||||||
|
|
||||||
|
Teaches agents how to turn JSON specs into SVG and PNG images using Satori. Covers the image-specific registry, dimensions, fonts, and rendering options.
|
||||||
|
|
||||||
|
## remotion
|
||||||
|
|
||||||
|
Teaches agents how to generate videos from JSON timeline specs using Remotion. Covers compositions, sequences, timeline structure, and video rendering.
|
||||||
|
|
||||||
|
## vue
|
||||||
|
|
||||||
|
Teaches agents how to render JSON specs as Vue 3 component trees. Covers the Vue renderer API, custom component registries, and reactivity integration.
|
||||||
|
|
||||||
|
## svelte
|
||||||
|
|
||||||
|
Teaches agents how to render JSON specs as Svelte 5 component trees. Covers the Svelte renderer API and component registration.
|
||||||
|
|
||||||
|
## codegen
|
||||||
|
|
||||||
|
Teaches agents how to use code generation utilities to export UI specs as framework-specific source code. Covers the codegen pipeline and custom exporter creation.
|
||||||
|
|
||||||
|
## mcp
|
||||||
|
|
||||||
|
Teaches agents how to build MCP Apps that serve json-render UIs inside AI tools like Claude, ChatGPT, Cursor, and VS Code. Covers MCP server setup, tool definitions, and UI streaming.
|
||||||
|
|
||||||
|
## redux
|
||||||
|
|
||||||
|
Teaches agents how to connect a Redux store to json-render's `StateStore` interface for state-driven UIs.
|
||||||
|
|
||||||
|
## zustand
|
||||||
|
|
||||||
|
Teaches agents how to connect a Zustand store to json-render's `StateStore` interface for lightweight state management.
|
||||||
|
|
||||||
|
## jotai
|
||||||
|
|
||||||
|
Teaches agents how to connect Jotai atoms to json-render's `StateStore` interface for atomic state management.
|
||||||
|
|
||||||
|
## xstate
|
||||||
|
|
||||||
|
Teaches agents how to connect an XState Store to json-render's `StateStore` interface for state-machine-driven UIs.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
All skill files are in the [`skills/`](https://github.com/vercel-labs/json-render/tree/main/skills) directory of the repository.
|
||||||
@@ -16,7 +16,8 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
|
|||||||
|
|
||||||
GitHub repository: https://github.com/vercel-labs/json-render
|
GitHub repository: https://github.com/vercel-labs/json-render
|
||||||
Documentation: https://json-render.dev/docs
|
Documentation: https://json-render.dev/docs
|
||||||
npm packages: @json-render/core, @json-render/react, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen
|
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/shadcn, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate
|
||||||
|
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, image, remotion, vue, svelte, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
|
||||||
|
|
||||||
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
|
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ function FormUI() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatModeDiagram() {
|
function InlineModeDiagram() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
||||||
Chat Mode
|
Inline Mode
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
|
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
|
||||||
{/* Chat area */}
|
{/* Chat area */}
|
||||||
@@ -69,11 +69,11 @@ function ChatModeDiagram() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GenerateModeDiagram() {
|
function StandaloneModeDiagram() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
||||||
Generate Mode
|
Standalone Mode
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
|
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
|
||||||
{/* Left panel - prompt */}
|
{/* Left panel - prompt */}
|
||||||
@@ -104,10 +104,10 @@ export function GenerationModesDiagram() {
|
|||||||
<div className="not-prose my-8">
|
<div className="not-prose my-8">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
<div className="h-[280px]">
|
<div className="h-[280px]">
|
||||||
<ChatModeDiagram />
|
<InlineModeDiagram />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[280px]">
|
<div className="h-[280px]">
|
||||||
<GenerateModeDiagram />
|
<StandaloneModeDiagram />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const docsNavigation: NavSection[] = [
|
|||||||
{ title: "Introduction", href: "/docs" },
|
{ title: "Introduction", href: "/docs" },
|
||||||
{ title: "Installation", href: "/docs/installation" },
|
{ title: "Installation", href: "/docs/installation" },
|
||||||
{ title: "Quick Start", href: "/docs/quick-start" },
|
{ title: "Quick Start", href: "/docs/quick-start" },
|
||||||
|
{ title: "Skills", href: "/docs/skills" },
|
||||||
{ title: "Migration Guide", href: "/docs/migration" },
|
{ title: "Migration Guide", href: "/docs/migration" },
|
||||||
{ title: "Changelog", href: "/docs/changelog" },
|
{ title: "Changelog", href: "/docs/changelog" },
|
||||||
],
|
],
|
||||||
@@ -80,16 +81,26 @@ export const docsNavigation: NavSection[] = [
|
|||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Svelte",
|
||||||
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/svelte",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Vue",
|
title: "Vue",
|
||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Renders with Vite (Vue / React)",
|
title: "Renders with Vite (Vue / React / Svelte)",
|
||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "MCP App",
|
||||||
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -121,7 +132,13 @@ export const docsNavigation: NavSection[] = [
|
|||||||
{ title: "@json-render/image", href: "/docs/api/image" },
|
{ title: "@json-render/image", href: "/docs/api/image" },
|
||||||
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
||||||
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
||||||
|
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
|
||||||
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
|
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
|
||||||
|
{ title: "@json-render/mcp", href: "/docs/api/mcp" },
|
||||||
|
{ title: "@json-render/redux", href: "/docs/api/redux" },
|
||||||
|
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
|
||||||
|
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
|
||||||
|
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/ag-ui": "AG-UI Integration",
|
"docs/ag-ui": "AG-UI Integration",
|
||||||
"docs/migration": "Migration Guide",
|
"docs/migration": "Migration Guide",
|
||||||
"docs/changelog": "Changelog",
|
"docs/changelog": "Changelog",
|
||||||
|
"docs/skills": "Skills",
|
||||||
|
|
||||||
// API references
|
// API references
|
||||||
"docs/api/core": "@json-render/core API",
|
"docs/api/core": "@json-render/core API",
|
||||||
@@ -45,10 +46,16 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/api/react-pdf": "@json-render/react-pdf API",
|
"docs/api/react-pdf": "@json-render/react-pdf API",
|
||||||
"docs/api/react-email": "@json-render/react-email API",
|
"docs/api/react-email": "@json-render/react-email API",
|
||||||
"docs/api/react-native": "@json-render/react-native API",
|
"docs/api/react-native": "@json-render/react-native API",
|
||||||
|
"docs/api/svelte": "@json-render/svelte API",
|
||||||
"docs/api/codegen": "@json-render/codegen API",
|
"docs/api/codegen": "@json-render/codegen API",
|
||||||
"docs/api/image": "@json-render/image API",
|
"docs/api/image": "@json-render/image API",
|
||||||
"docs/api/remotion": "@json-render/remotion API",
|
"docs/api/remotion": "@json-render/remotion API",
|
||||||
"docs/api/shadcn": "@json-render/shadcn API",
|
"docs/api/shadcn": "@json-render/shadcn API",
|
||||||
|
"docs/api/mcp": "@json-render/mcp API",
|
||||||
|
"docs/api/redux": "@json-render/redux API",
|
||||||
|
"docs/api/zustand": "@json-render/zustand API",
|
||||||
|
"docs/api/jotai": "@json-render/jotai API",
|
||||||
|
"docs/api/xstate": "@json-render/xstate API",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-chat
|
# example-chat
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
- @json-render/shadcn@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
- @json-render/shadcn@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ When the user asks for a quiz, test, or Q&A, build an interactive experience:
|
|||||||
4. You can also add a final score section that becomes visible when all questions are submitted.
|
4. You can also add a final score section that becomes visible when all questions are submitted.
|
||||||
|
|
||||||
${explorerCatalog.prompt({
|
${explorerCatalog.prompt({
|
||||||
mode: "chat",
|
mode: "inline",
|
||||||
customRules: [
|
customRules: [
|
||||||
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
|
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
|
||||||
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
|
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-chat",
|
"name": "example-chat",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-dashboard
|
# example-dashboard
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/codegen@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/codegen@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-dashboard",
|
"name": "example-dashboard",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-image
|
# example-image
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/image@0.12.1
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/image@0.12.0
|
||||||
|
|
||||||
## 0.1.1
|
## 0.1.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-image",
|
"name": "example-image",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# example-mcp
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/mcp@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
- @json-render/shadcn@0.12.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/mcp@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
- @json-render/shadcn@0.12.0
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# MCP App Example
|
||||||
|
|
||||||
|
A json-render MCP App that serves interactive shadcn/ui-based UIs directly inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Cursor
|
||||||
|
|
||||||
|
Add to `.cursor/mcp.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"json-render": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Claude Desktop
|
||||||
|
|
||||||
|
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"json-render": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "/absolute/path/to/examples/mcp/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Transport
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm start
|
||||||
|
# Server listens on http://localhost:3001/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stdio Transport
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm start:stdio
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. The Vite build bundles the React app (with shadcn components and `useJsonRenderApp` hook) into a single self-contained HTML file
|
||||||
|
2. The MCP server registers a `render-ui` tool with the catalog prompt as its description
|
||||||
|
3. When the LLM calls the tool, it generates a json-render spec constrained to the catalog
|
||||||
|
4. The host renders the bundled HTML in a sandboxed iframe
|
||||||
|
5. The iframe receives the spec via the MCP Apps protocol and renders it with json-render
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
|
||||||
|
<title>json-render MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "example-mcp",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "tsx server.ts",
|
||||||
|
"start:stdio": "tsx server.ts --stdio",
|
||||||
|
"dev": "tsx watch server.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@json-render/mcp": "workspace:*",
|
||||||
|
"@json-render/react": "workspace:*",
|
||||||
|
"@json-render/shadcn": "workspace:*",
|
||||||
|
"@modelcontextprotocol/ext-apps": "^1.2.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "19.2.3",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-singlefile": "^2.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { createMcpApp } from "@json-render/mcp";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { catalog } from "./src/catalog.js";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
function loadHtml(): string {
|
||||||
|
const htmlPath = path.join(__dirname, "dist", "index.html");
|
||||||
|
if (!fs.existsSync(htmlPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return fs.readFileSync(htmlPath, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startStdio() {
|
||||||
|
const html = loadHtml();
|
||||||
|
const server = await createMcpApp({
|
||||||
|
name: "json-render Example",
|
||||||
|
version: "1.0.0",
|
||||||
|
catalog,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
await server.connect(new StdioServerTransport());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startHttp() {
|
||||||
|
const html = loadHtml();
|
||||||
|
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||||
|
|
||||||
|
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
|
||||||
|
|
||||||
|
expressApp.all("/mcp", async (req, res) => {
|
||||||
|
const server = await createMcpApp({
|
||||||
|
name: "json-render Example",
|
||||||
|
version: "1.0.0",
|
||||||
|
catalog,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on("close", () => {
|
||||||
|
transport.close().catch(() => {});
|
||||||
|
server.close().catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.connect(transport);
|
||||||
|
await transport.handleRequest(req, res, req.body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MCP error:", error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: { code: -32603, message: "Internal server error" },
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expressApp.listen(port, () => {
|
||||||
|
console.log(`MCP server listening on http://localhost:${port}/mcp`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv.includes("--stdio")) {
|
||||||
|
startStdio().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
startHttp().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
|
|
||||||
|
export const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
...shadcnComponentDefinitions,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@source "../../../packages/shadcn/src";
|
||||||
|
@source "../../../packages/react/src";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is([data-theme="dark"] *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
|
||||||
|
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||||
|
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
|
||||||
|
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||||
|
--popover: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
|
||||||
|
--popover-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||||
|
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
|
||||||
|
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
|
||||||
|
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||||
|
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||||
|
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||||
|
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
|
||||||
|
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||||
|
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||||
|
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
|
||||||
|
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
|
||||||
|
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
|
||||||
|
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
color: var(--color-foreground);
|
||||||
|
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply text-foreground;
|
||||||
|
background-color: var(--color-background) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import "./globals.css";
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App as McpApp } from "@modelcontextprotocol/ext-apps";
|
||||||
|
import { JSONUIProvider, Renderer, defineRegistry } from "@json-render/react";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import { catalog } from "./catalog";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
...shadcnComponents,
|
||||||
|
Avatar: ({ props }: { props: Record<string, unknown> }) => {
|
||||||
|
const name = (props.name as string) || (props.alt as string) || "?";
|
||||||
|
const src = props.src as string | undefined;
|
||||||
|
const initials = name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.slice(0, 2)
|
||||||
|
.toUpperCase();
|
||||||
|
const size = props.size === "lg" ? 48 : props.size === "sm" ? 32 : 40;
|
||||||
|
const [imgFailed, setImgFailed] = useState(false);
|
||||||
|
const onError = useCallback(() => setImgFailed(true), []);
|
||||||
|
const showImg = src && !imgFailed;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: "50%",
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor: "var(--color-background-tertiary, #27272a)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showImg ? (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={name}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
onError={onError}
|
||||||
|
style={{ objectFit: "cover", width: size, height: size }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: size * 0.4,
|
||||||
|
color: "var(--color-text-secondary, #a1a1aa)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{initials}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<
|
||||||
|
{ children: ReactNode },
|
||||||
|
{ error: Error | null }
|
||||||
|
> {
|
||||||
|
state = { error: null as Error | null };
|
||||||
|
static getDerivedStateFromError(error: Error) {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 16,
|
||||||
|
color: "#dc2626",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Error: {this.state.error.message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceFullWidth(spec: Spec): Spec {
|
||||||
|
if (!spec.elements) return spec;
|
||||||
|
const elements = { ...spec.elements };
|
||||||
|
for (const [key, el] of Object.entries(elements)) {
|
||||||
|
if (el.type === "Card" && el.props) {
|
||||||
|
elements[key] = {
|
||||||
|
...el,
|
||||||
|
props: { ...el.props, maxWidth: "full", centered: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...spec, elements };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSpec(
|
||||||
|
result: { content?: Array<{ type: string; text?: string }> } | undefined,
|
||||||
|
): Spec | null {
|
||||||
|
const text = result?.content?.find((c) => c.type === "text")?.text;
|
||||||
|
if (!text) return null;
|
||||||
|
try {
|
||||||
|
return forceFullWidth(JSON.parse(text) as Spec);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHostContext(ctx: {
|
||||||
|
theme?: string;
|
||||||
|
styles?: { variables?: Record<string, string> };
|
||||||
|
}) {
|
||||||
|
if (ctx.theme) {
|
||||||
|
document.documentElement.setAttribute("data-theme", ctx.theme);
|
||||||
|
document.documentElement.style.colorScheme = ctx.theme;
|
||||||
|
}
|
||||||
|
if (ctx.styles?.variables) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
for (const [key, value] of Object.entries(ctx.styles.variables)) {
|
||||||
|
root.style.setProperty(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function McpAppView() {
|
||||||
|
const [spec, setSpec] = useState<Spec | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let specReceived = false;
|
||||||
|
|
||||||
|
function handleSpec(s: Spec) {
|
||||||
|
if (specReceived) return;
|
||||||
|
specReceived = true;
|
||||||
|
setSpec(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessage(event: MessageEvent) {
|
||||||
|
if (specReceived) return;
|
||||||
|
const data = event.data as Record<string, unknown> | undefined;
|
||||||
|
if (!data || typeof data !== "object") return;
|
||||||
|
const method = data.method as string | undefined;
|
||||||
|
const params = data.params as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
// Cursor sends the spec via tool-input
|
||||||
|
if (method === "ui/notifications/tool-input" && params?.arguments) {
|
||||||
|
const args = params.arguments as Record<string, unknown>;
|
||||||
|
const rawSpec = args.spec;
|
||||||
|
if (
|
||||||
|
rawSpec &&
|
||||||
|
typeof rawSpec === "object" &&
|
||||||
|
"root" in rawSpec &&
|
||||||
|
"elements" in rawSpec
|
||||||
|
) {
|
||||||
|
handleSpec(forceFullWidth(rawSpec as Spec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("message", onMessage);
|
||||||
|
|
||||||
|
const app = new McpApp({ name: "json-render", version: "1.0.0" });
|
||||||
|
|
||||||
|
app.ontoolresult = (result) => {
|
||||||
|
const parsed = parseSpec(
|
||||||
|
result as { content?: Array<{ type: string; text?: string }> },
|
||||||
|
);
|
||||||
|
if (parsed) handleSpec(parsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
app.onhostcontextchanged = (ctx) =>
|
||||||
|
applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);
|
||||||
|
|
||||||
|
app.onerror = (err: unknown) => {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
app
|
||||||
|
.connect()
|
||||||
|
.then(() => {
|
||||||
|
const ctx = app.getHostContext?.();
|
||||||
|
if (ctx)
|
||||||
|
applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);
|
||||||
|
|
||||||
|
// Fallback: if host didn't provide theme, detect via media query
|
||||||
|
if (!ctx || !(ctx as Record<string, unknown>).theme) {
|
||||||
|
const prefersDark = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)",
|
||||||
|
).matches;
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-theme",
|
||||||
|
prefersDark ? "dark" : "light",
|
||||||
|
);
|
||||||
|
document.documentElement.style.colorScheme = prefersDark
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) =>
|
||||||
|
setError(err instanceof Error ? err.message : String(err)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", onMessage);
|
||||||
|
app.close().catch(() => {});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 16,
|
||||||
|
color: "#dc2626",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spec) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 16,
|
||||||
|
color: "#6b7280",
|
||||||
|
fontFamily: "sans-serif",
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
|
||||||
|
<div className="w-full">
|
||||||
|
<Renderer spec={spec} registry={registry} />
|
||||||
|
</div>
|
||||||
|
</JSONUIProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<McpAppView />
|
||||||
|
</ErrorBoundary>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { JSONUIProvider, Renderer } from "@json-render/react";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
|
import { defineRegistry } from "@json-render/react";
|
||||||
|
import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||||
|
import { catalog } from "./catalog";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
...shadcnComponents,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const debugStyle = {
|
||||||
|
padding: 12,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#000",
|
||||||
|
background: "#fffbe6",
|
||||||
|
border: "1px solid #faad14",
|
||||||
|
borderRadius: 4,
|
||||||
|
margin: 8,
|
||||||
|
whiteSpace: "pre-wrap" as const,
|
||||||
|
wordBreak: "break-all" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function McpAppView() {
|
||||||
|
const [logs, setLogs] = useState<string[]>(["App mounted"]);
|
||||||
|
|
||||||
|
const addLog = (msg: string) => {
|
||||||
|
setLogs((prev) => [
|
||||||
|
...prev,
|
||||||
|
`${new Date().toISOString().slice(11, 23)} ${msg}`,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { spec, loading, connected, connecting, error } = useJsonRenderApp({
|
||||||
|
name: "json-render-mcp-example",
|
||||||
|
version: "1.0.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addLog(
|
||||||
|
`State: connecting=${connecting} connected=${connected} error=${error?.message ?? "none"} loading=${loading} spec=${spec ? "yes" : "null"}`,
|
||||||
|
);
|
||||||
|
}, [connecting, connected, error, loading, spec]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (spec) {
|
||||||
|
addLog(
|
||||||
|
`Spec received: root=${spec.root}, elements=${Object.keys(spec.elements ?? {}).join(",")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [spec]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={debugStyle}>
|
||||||
|
<div style={{ color: "#ef4444", fontWeight: "bold" }}>
|
||||||
|
Connection error: {error.message}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 8 }}>{logs.join("\n")}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spec) {
|
||||||
|
return (
|
||||||
|
<div style={debugStyle}>
|
||||||
|
<div>
|
||||||
|
{connecting
|
||||||
|
? "Connecting to host..."
|
||||||
|
: loading
|
||||||
|
? "Waiting for UI spec..."
|
||||||
|
: "No spec received."}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 8 }}>{logs.join("\n")}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
|
||||||
|
<Renderer spec={spec} registry={registry} loading={loading} />
|
||||||
|
</JSONUIProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src", "server.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss(), viteSingleFile()],
|
||||||
|
resolve: {
|
||||||
|
dedupe: ["react", "react-dom"],
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: "index.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-no-ai
|
# example-no-ai
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
- @json-render/shadcn@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
- @json-render/shadcn@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-no-ai",
|
"name": "example-no-ai",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# example-react-email
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react-email@0.12.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react-email@0.12.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-email",
|
"name": "example-react-email",
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"react-resizable-panels": "^4.4.1",
|
"react-resizable-panels": "^4.4.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "4.3.5"
|
"zod": "4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@internal/typescript-config": "workspace:*",
|
"@internal/typescript-config": "workspace:*",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-native
|
# example-react-native
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react-native@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react-native@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-native",
|
"name": "example-react-native",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-pdf
|
# example-react-pdf
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react-pdf@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react-pdf@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-pdf",
|
"name": "example-react-pdf",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-remotion
|
# example-remotion
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/remotion@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/remotion@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-remotion",
|
"name": "example-remotion",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# com.example.json-render-demo
|
# com.example.json-render-demo
|
||||||
|
|
||||||
|
## 0.0.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
|
||||||
|
## 0.0.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
|
||||||
## 0.0.5
|
## 0.0.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-demo",
|
"name": "com.example.json-render-demo",
|
||||||
"version": "0.0.5",
|
"version": "0.0.7",
|
||||||
"description": "Test",
|
"description": "Test",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# com.example.json-render-fullpage-demo
|
# com.example.json-render-fullpage-demo
|
||||||
|
|
||||||
|
## 0.0.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
|
||||||
|
## 0.0.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
|
||||||
## 0.0.5
|
## 0.0.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-fullpage-demo",
|
"name": "com.example.json-render-fullpage-demo",
|
||||||
"version": "0.0.5",
|
"version": "0.0.7",
|
||||||
"description": "Full-page Stripe App example (alpha)",
|
"description": "Full-page Stripe App example (alpha)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# svelte-chat
|
||||||
|
|
||||||
|
## 0.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/svelte@0.12.1
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/svelte@0.12.0
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# create a new project
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
To recreate this project with the same configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# recreate this project
|
||||||
|
npx sv create --template minimal --types ts --no-install svelte-chat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "zinc"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "svelte-chat",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.3",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check-types": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@internationalized/date": "^3.10.0",
|
||||||
|
"@lucide/svelte": "^0.561.0",
|
||||||
|
"@sveltejs/adapter-auto": "^7.0.0",
|
||||||
|
"@sveltejs/kit": "^2.50.2",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"bits-ui": "^2.14.4",
|
||||||
|
"svelte": "^5.49.2",
|
||||||
|
"svelte-check": "^4.3.6",
|
||||||
|
"tailwind-variants": "^3.2.2",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/gateway": "^3.0.46",
|
||||||
|
"@ai-sdk/svelte": "^4.0.96",
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@json-render/svelte": "workspace:*",
|
||||||
|
"ai": "^6.0.86",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-svelte": "^0.500.0",
|
||||||
|
"tailwind-merge": "^3.2.0",
|
||||||
|
"zod": "4.3.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
currentColor 25%,
|
||||||
|
hsl(0 0% 64%) 50%,
|
||||||
|
currentColor 75%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: shimmer 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { ToolLoopAgent, stepCountIs } from "ai";
|
||||||
|
import { createGateway } from "@ai-sdk/gateway";
|
||||||
|
import { explorerCatalog } from "./render/catalog";
|
||||||
|
import { getWeather } from "./tools/weather";
|
||||||
|
import { getGitHubRepo, getGitHubPullRequests } from "./tools/github";
|
||||||
|
import { getCryptoPrice, getCryptoPriceHistory } from "./tools/crypto";
|
||||||
|
import { getHackerNewsTop } from "./tools/hackernews";
|
||||||
|
import { webSearch } from "./tools/search";
|
||||||
|
import { env } from "$env/dynamic/private";
|
||||||
|
|
||||||
|
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
|
||||||
|
|
||||||
|
const AGENT_INSTRUCTIONS = `You are a knowledgeable assistant that helps users explore data and learn about any topic. You look up real-time information, build visual dashboards, and create rich educational content.
|
||||||
|
|
||||||
|
WORKFLOW:
|
||||||
|
1. Call the appropriate tools to gather relevant data. Use webSearch for general topics not covered by specialized tools.
|
||||||
|
2. Respond with a brief, conversational summary of what you found.
|
||||||
|
3. Then output the JSONL UI spec wrapped in a \`\`\`spec fence to render a rich visual experience.
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Always call tools FIRST to get real data. Never make up data.
|
||||||
|
- Embed the fetched data directly in /state paths so components can reference it.
|
||||||
|
- Use Card components to group related information.
|
||||||
|
- NEVER nest a Card inside another Card. If you need sub-sections inside a Card, use Stack, Separator, Heading, or Accordion instead.
|
||||||
|
- Use Grid for multi-column layouts.
|
||||||
|
- Use Metric for key numeric values (temperature, stars, price, etc.).
|
||||||
|
- Use Table for lists of items (stories, forecasts, languages, etc.).
|
||||||
|
- Use BarChart or LineChart for numeric trends and time-series data.
|
||||||
|
- Use PieChart for compositional/proportional data (market share, breakdowns, distributions).
|
||||||
|
- Use Tabs when showing multiple categories of data side by side.
|
||||||
|
- Use Badge for status indicators.
|
||||||
|
- Use Callout for key facts, tips, warnings, or important takeaways.
|
||||||
|
- Use Accordion to organize detailed sections the user can expand for deeper reading.
|
||||||
|
- Use Timeline for historical events, processes, step-by-step explanations, or milestones.
|
||||||
|
- When teaching about a topic, combine multiple component types to create a rich, engaging experience.
|
||||||
|
|
||||||
|
DATA BINDING:
|
||||||
|
- The state model is the single source of truth. Put fetched data in /state, then reference it with { "$state": "/json/pointer" } in any prop.
|
||||||
|
- $state works on ANY prop at ANY nesting level. The renderer resolves expressions before components receive props.
|
||||||
|
- Scalar binding: "title": { "$state": "/quiz/title" }
|
||||||
|
- Array binding: "items": { "$state": "/quiz/questions" } (for Accordion, Timeline, etc.)
|
||||||
|
- For Table, BarChart, LineChart, and PieChart, use { "$state": "/path" } on the data prop to bind read-only data from state.
|
||||||
|
- Always emit /state patches BEFORE the elements that reference them, so data is available when the UI renders.
|
||||||
|
- Always use the { "$state": "/foo" } object syntax for data binding.
|
||||||
|
|
||||||
|
INTERACTIVITY:
|
||||||
|
- You can use visible, repeat, on.press, and $cond/$then/$else freely.
|
||||||
|
- visible: Conditionally show/hide elements based on state. e.g. "visible": { "$state": "/q1/answer", "eq": "a" }
|
||||||
|
- repeat: Iterate over state arrays. e.g. "repeat": { "statePath": "/items" }
|
||||||
|
- on.press: Trigger actions on button clicks. e.g. "on": { "press": { "action": "setState", "params": { "statePath": "/submitted", "value": true } } }
|
||||||
|
- $cond/$then/$else: Conditional prop values. e.g. { "$cond": { "$state": "/correct" }, "$then": "Correct!", "$else": "Try again" }
|
||||||
|
|
||||||
|
BUILT-IN ACTIONS (use with on.press):
|
||||||
|
- setState: Set a value at a state path. params: { statePath: "/foo", value: "bar" }
|
||||||
|
- pushState: Append to an array. params: { statePath: "/items", value: { ... } }
|
||||||
|
- removeState: Remove by index. params: { statePath: "/items", index: 0 }
|
||||||
|
|
||||||
|
INPUT COMPONENTS:
|
||||||
|
- RadioGroup: Renders radio buttons. Writes selected value to statePath automatically.
|
||||||
|
- SelectInput: Dropdown select. Writes selected value to statePath automatically.
|
||||||
|
- TextInput: Text input field. Writes entered value to statePath automatically.
|
||||||
|
- Button: Clickable button. Use on.press to trigger actions.
|
||||||
|
|
||||||
|
${explorerCatalog.prompt({
|
||||||
|
mode: "inline",
|
||||||
|
customRules: [
|
||||||
|
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
|
||||||
|
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
|
||||||
|
"Use Metric components for key numbers instead of plain Text.",
|
||||||
|
"Put chart data arrays in /state and reference them with { $state: '/path' } on the data prop.",
|
||||||
|
"Keep the UI clean and information-dense — no excessive padding or empty space.",
|
||||||
|
"For educational prompts ('teach me about', 'explain', 'what is'), use a mix of Callout, Accordion, Timeline, and charts to make the content visually rich.",
|
||||||
|
],
|
||||||
|
})}`;
|
||||||
|
|
||||||
|
export const gateway = createGateway({ apiKey: env.AI_GATEWAY_API_KEY });
|
||||||
|
|
||||||
|
export const agent = new ToolLoopAgent({
|
||||||
|
model: gateway(env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
|
||||||
|
instructions: AGENT_INSTRUCTIONS,
|
||||||
|
tools: {
|
||||||
|
getWeather,
|
||||||
|
getGitHubRepo,
|
||||||
|
getGitHubPullRequests,
|
||||||
|
getCryptoPrice,
|
||||||
|
getCryptoPriceHistory,
|
||||||
|
getHackerNewsTop,
|
||||||
|
webSearch,
|
||||||
|
},
|
||||||
|
stopWhen: stepCountIs(5),
|
||||||
|
temperature: 0.7,
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="accordion-content"
|
||||||
|
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div class={cn("pt-0 pb-4", className)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AccordionPrimitive.ItemProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="accordion-item"
|
||||||
|
class={cn("border-b last:border-b-0", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
level = 3,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
|
||||||
|
level?: AccordionPrimitive.HeaderProps["level"];
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Header {level} class="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: AccordionPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:value={value as never}
|
||||||
|
data-slot="accordion"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Root from "./accordion.svelte";
|
||||||
|
import Content from "./accordion-content.svelte";
|
||||||
|
import Item from "./accordion-item.svelte";
|
||||||
|
import Trigger from "./accordion-trigger.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Item,
|
||||||
|
Trigger,
|
||||||
|
//
|
||||||
|
Root as Accordion,
|
||||||
|
Content as AccordionContent,
|
||||||
|
Item as AccordionItem,
|
||||||
|
Trigger as AccordionTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-description"
|
||||||
|
class={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-title"
|
||||||
|
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const alertVariants = tv({
|
||||||
|
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
variant?: AlertVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert"
|
||||||
|
class={cn(alertVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Root from "./alert.svelte";
|
||||||
|
import Description from "./alert-description.svelte";
|
||||||
|
import Title from "./alert-title.svelte";
|
||||||
|
export { alertVariants, type AlertVariant } from "./alert.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Description,
|
||||||
|
Title,
|
||||||
|
//
|
||||||
|
Root as Alert,
|
||||||
|
Description as AlertDescription,
|
||||||
|
Title as AlertTitle,
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||||
|
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
|
||||||
|
outline:
|
||||||
|
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = "button",
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? "link" : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from "./button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-action"
|
||||||
|
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-footer"
|
||||||
|
class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-header"
|
||||||
|
class={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-title"
|
||||||
|
class={cn("leading-none font-semibold", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card"
|
||||||
|
class={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Root from "./card.svelte";
|
||||||
|
import Content from "./card-content.svelte";
|
||||||
|
import Description from "./card-description.svelte";
|
||||||
|
import Footer from "./card-footer.svelte";
|
||||||
|
import Header from "./card-header.svelte";
|
||||||
|
import Title from "./card-title.svelte";
|
||||||
|
import Action from "./card-action.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
//
|
||||||
|
Root as Card,
|
||||||
|
Content as CardContent,
|
||||||
|
Description as CardDescription,
|
||||||
|
Footer as CardFooter,
|
||||||
|
Header as CardHeader,
|
||||||
|
Title as CardTitle,
|
||||||
|
Action as CardAction,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||||
|
|
||||||
|
type Props = WithElementRef<
|
||||||
|
Omit<HTMLInputAttributes, "type"> &
|
||||||
|
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||||
|
>;
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
type,
|
||||||
|
files = $bindable(),
|
||||||
|
class: className,
|
||||||
|
"data-slot": dataSlot = "input",
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if type === "file"}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
type="file"
|
||||||
|
bind:files
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{type}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./label.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Label as LabelPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: LabelPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="label"
|
||||||
|
class={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./progress.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Progress,
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Progress as ProgressPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
max = 100,
|
||||||
|
value,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<ProgressPrimitive.RootProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="progress"
|
||||||
|
class={cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className)}
|
||||||
|
{value}
|
||||||
|
{max}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
class="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style="transform: translateX(-{100 - (100 * (value ?? 0)) / (max ?? 1)}%)"
|
||||||
|
></div>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Root from "./radio-group.svelte";
|
||||||
|
import Item from "./radio-group-item.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Item,
|
||||||
|
//
|
||||||
|
Root as RadioGroup,
|
||||||
|
Item as RadioGroupItem,
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||||
|
import CircleIcon from "@lucide/svelte/icons/circle";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
class={cn(
|
||||||
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<div data-slot="radio-group-indicator" class="relative flex items-center justify-center">
|
||||||
|
{#if checked}
|
||||||
|
<CircleIcon
|
||||||
|
class="fill-primary absolute start-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value = $bindable(""),
|
||||||
|
...restProps
|
||||||
|
}: RadioGroupPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="radio-group"
|
||||||
|
class={cn("grid gap-3", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user