From bc0e7636787aa29db6f040d44d0c5020f0950142 Mon Sep 17 00:00:00 2001 From: LuZhuJun Date: Fri, 6 Mar 2026 13:22:41 +0800 Subject: [PATCH 1/9] feat: add @json-render/antd package with 50+ pre-built Ant Design components Enable AI-generated UIs using Ant Design component library. Developers can now build json-render applications with 50+ ready-to-use components including forms, tables, modals, and data visualization. --- README.md | 38 +- packages/antd/CHANGELOG.md | 36 + packages/antd/README.md | 224 +++++ packages/antd/package.json | 73 ++ packages/antd/src/catalog.ts | 825 ++++++++++++++++++ packages/antd/src/components.tsx | 1392 ++++++++++++++++++++++++++++++ packages/antd/src/index.ts | 9 + packages/antd/tsconfig.json | 9 + packages/antd/tsup.config.ts | 18 + pnpm-lock.yaml | 870 ++++++++++++++++++- 10 files changed, 3489 insertions(+), 5 deletions(-) create mode 100644 packages/antd/CHANGELOG.md create mode 100644 packages/antd/README.md create mode 100644 packages/antd/package.json create mode 100644 packages/antd/src/catalog.ts create mode 100644 packages/antd/src/components.tsx create mode 100644 packages/antd/src/index.ts create mode 100644 packages/antd/tsconfig.json create mode 100644 packages/antd/tsup.config.ts diff --git a/README.md b/README.md index 8db26387..329b0cde 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Generate dynamic, personalized UIs from prompts without sacrificing reliability. npm install @json-render/core @json-render/react # for React with pre-built shadcn/ui components npm install @json-render/shadcn +# for React with pre-built Ant Design components +npm install @json-render/antd # or for React Native npm install @json-render/core @json-render/react-native # or for video @@ -29,7 +31,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur - **Predictable** - JSON output matches your schema, every time - **Fast** - Stream and render progressively as the model responds - **Cross-Platform** - React, Vue (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 and 50+ Ant Design components ready to use ## Quick Start @@ -117,6 +119,7 @@ function Dashboard({ spec }) { | `@json-render/react` | React renderer, contexts, hooks | | `@json-render/vue` | Vue 3 renderer, composables, providers | | `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) | +| `@json-render/antd` | 50+ pre-built Ant Design components | | `@json-render/react-native` | React Native renderer with standard mobile components | | `@json-render/remotion` | Remotion video renderer, timeline schema | | `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs | @@ -210,6 +213,39 @@ const { registry } = defineRegistry(catalog, { ``` +### Ant Design (Web) + +```tsx +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/react/schema"; +import { defineRegistry, Renderer } from "@json-render/react"; +import { antdComponentDefinitions } from "@json-render/antd/catalog"; +import { antdComponents } from "@json-render/antd"; + +// Pick components from the 50+ standard definitions +const catalog = defineCatalog(schema, { + components: { + Card: antdComponentDefinitions.Card, + Stack: antdComponentDefinitions.Stack, + Heading: antdComponentDefinitions.Heading, + Button: antdComponentDefinitions.Button, + }, + actions: {}, +}); + +// Use matching implementations +const { registry } = defineRegistry(catalog, { + components: { + Card: antdComponents.Card, + Stack: antdComponents.Stack, + Heading: antdComponents.Heading, + Button: antdComponents.Button, + }, +}); + + +``` + ### React Native (Mobile) ```tsx diff --git a/packages/antd/CHANGELOG.md b/packages/antd/CHANGELOG.md new file mode 100644 index 00000000..8f555e12 --- /dev/null +++ b/packages/antd/CHANGELOG.md @@ -0,0 +1,36 @@ +# @json-render/antd + +## 0.11.0 + +### Minor Changes + +- Initial release of `@json-render/antd` package. + + Pre-built [Ant Design](https://ant.design/) component library for json-render. 50+ components ready to use with `defineCatalog` and `defineRegistry`. + + - `antdComponentDefinitions` — Zod-based catalog definitions for all components (server-safe, no React dependency via `@json-render/antd/catalog`) + - `antdComponents` — React implementations for all components + + ### Layout Components + - Card, Stack, Grid, Divider, Space + + ### Navigation Components + - Tabs, Collapse, Menu + + ### Overlay Components + - Modal, Drawer, Popover, Tooltip, Dropdown + + ### Data Display Components + - Table, Heading, Text, Paragraph, Image, Avatar, Badge, Tag, Alert, Progress, Skeleton, Spin, Empty, Statistic, Descriptions, Timeline, Carousel + + ### Form Components + - Input, TextArea, InputNumber, Select, Checkbox, CheckboxGroup, Radio, Switch, Slider, Rate, DatePicker, TimePicker, Upload, Transfer + + ### Action Components + - Button, ButtonGroup, Link, Pagination, Segmented, Steps, Result + +### Patch Changes + +- Updated dependencies + - @json-render/core@0.11.0 + - @json-render/react@0.11.0 diff --git a/packages/antd/README.md b/packages/antd/README.md new file mode 100644 index 00000000..b31a124f --- /dev/null +++ b/packages/antd/README.md @@ -0,0 +1,224 @@ +# @json-render/antd + +Pre-built [Ant Design](https://ant.design/) components for json-render. Drop-in catalog definitions and React implementations for 50+ components built on Ant Design. + +## Installation + +```bash +npm install @json-render/antd @json-render/core @json-render/react antd zod +``` + +## Quick Start + +### 1. Create a Catalog + +Import standard definitions from `@json-render/antd/catalog` and pass them to `defineCatalog`: + +```typescript +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/react/schema"; +import { antdComponentDefinitions } from "@json-render/antd/catalog"; + +const catalog = defineCatalog(schema, { + components: { + // Pick the components you need + Card: antdComponentDefinitions.Card, + Stack: antdComponentDefinitions.Stack, + Heading: antdComponentDefinitions.Heading, + Button: antdComponentDefinitions.Button, + Input: antdComponentDefinitions.Input, + }, + actions: {}, +}); +``` + +> **Note:** State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled automatically by `ActionProvider`. You don't need to declare them in your catalog. + +### 2. Create a Registry + +Import standard implementations from `@json-render/antd` and pass them to `defineRegistry`: + +```typescript +import { defineRegistry } from "@json-render/react"; +import { antdComponents } from "@json-render/antd"; + +const { registry } = defineRegistry(catalog, { + components: { + Card: antdComponents.Card, + Stack: antdComponents.Stack, + Heading: antdComponents.Heading, + Button: antdComponents.Button, + Input: antdComponents.Input, + }, +}); +``` + +### 3. Render + +```tsx +import { Renderer } from "@json-render/react"; + +function App({ spec }) { + return ; +} +``` + +## Extending with Custom Components + +Pick standard components as a base and add your own alongside them: + +```typescript +import { z } from "zod"; + +// Catalog +const catalog = defineCatalog(schema, { + components: { + // Standard + Card: antdComponentDefinitions.Card, + Stack: antdComponentDefinitions.Stack, + Button: antdComponentDefinitions.Button, + + // Custom + Metric: { + props: z.object({ + label: z.string(), + value: z.string(), + trend: z.enum(["up", "down", "neutral"]).nullable(), + }), + description: "KPI metric display", + }, + }, + actions: {}, +}); + +// Registry +const { registry } = defineRegistry(catalog, { + components: { + // Standard + Card: antdComponents.Card, + Stack: antdComponents.Stack, + Button: antdComponents.Button, + + // Custom + Metric: ({ props }) => ( +
+ {props.label} + {props.value} +
+ ), + }, +}); +``` + +## Standard Components + +### Layout + +| Component | Description | +|-----------|-------------| +| `Card` | Container card with optional title and description | +| `Stack` | Flex container (horizontal/vertical) with gap, alignment, justify | +| `Grid` | Grid layout | +| `Divider` | Visual separator line | +| `Space` | Spacing component | + +### Navigation + +| Component | Description | +|-----------|-------------| +| `Tabs` | Tabbed navigation | +| `Collapse` | Collapsible accordion sections | +| `Menu` | Navigation menu | + +### Overlay + +| Component | Description | +|-----------|-------------| +| `Modal` | Modal dialog | +| `Drawer` | Drawer panel | +| `Popover` | Click-triggered popover | +| `Tooltip` | Hover tooltip | +| `Dropdown` | Dropdown menu | + +### Content + +| Component | Description | +|-----------|-------------| +| `Table` | Data table with columns and rows | +| `Heading` | Heading text | +| `Text` | Text content | +| `Paragraph` | Paragraph text | +| `Image` | Image display | +| `Avatar` | User avatar with fallback | +| `Badge` | Status badge | +| `Tag` | Tag component | +| `Alert` | Alert banner | +| `Progress` | Progress bar | +| `Skeleton` | Loading placeholder | +| `Spin` | Loading spinner | +| `Empty` | Empty state | +| `Statistic` | Statistic display | +| `Descriptions` | Description list | +| `Timeline` | Timeline display | +| `Carousel` | Horizontally scrollable carousel | + +### Input + +| Component | Description | +|-----------|-------------| +| `Input` | Text input with label, validation, and `validateOn` timing | +| `TextArea` | Multi-line text input with validation | +| `InputNumber` | Number input | +| `Select` | Dropdown select with validation | +| `Checkbox` | Checkbox input with validation | +| `CheckboxGroup` | Group of checkboxes | +| `Radio` | Radio button group with validation | +| `Switch` | Toggle switch with validation | +| `Slider` | Range slider | +| `Rate` | Star rating | +| `DatePicker` | Date picker | +| `TimePicker` | Time picker | +| `Upload` | File upload | +| `Transfer` | Transfer shuttle | + +### Action + +| Component | Description | +|-----------|-------------| +| `Button` | Clickable button with variants | +| `Link` | Anchor link | +| `ButtonGroup` | Group of buttons | +| `Pagination` | Page navigation | +| `Segmented` | Segmented control | +| `Steps` | Steps component | +| `Result` | Result page | + +## Built-in Actions + +State actions (`setState`, `pushState`, `removeState`, `validateForm`) are built into the `@json-render/react` schema and handled automatically by `ActionProvider`. They are included in prompts without needing to be declared in your catalog. + +| Action | Description | +|--------|-------------| +| `setState` | Set a value at a state path | +| `pushState` | Push a value onto an array in state | +| `removeState` | Remove an item from an array in state | +| `validateForm` | Validate all fields and write result to state | + +### Validation Timing (`validateOn`) + +All form components support the `validateOn` prop to control when validation runs: + +| Value | Description | Default For | +|-------|-------------|-------------| +| `"change"` | Validate on every input change | Select, Checkbox, Radio, Switch | +| `"blur"` | Validate when field loses focus | Input, Textarea | +| `"submit"` | Validate only on form submission | — | + +## Exports + +| Entry Point | Exports | +|-------------|---------| +| `@json-render/antd` | `antdComponents` | +| `@json-render/antd/catalog` | `antdComponentDefinitions` | + +The `/catalog` entry point contains only Zod schemas (no React dependency), so it can be used in server-side code for prompt generation. diff --git a/packages/antd/package.json b/packages/antd/package.json new file mode 100644 index 00000000..2b6fef28 --- /dev/null +++ b/packages/antd/package.json @@ -0,0 +1,73 @@ +{ + "name": "@json-render/antd", + "version": "0.11.0", + "license": "Apache-2.0", + "description": "Ant Design component library for @json-render/core. JSON becomes beautiful Ant Design React components.", + "keywords": [ + "json", + "ui", + "react", + "antd", + "ant-design", + "ai", + "generative-ui", + "llm", + "renderer", + "streaming", + "components" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/json-render.git", + "directory": "packages/antd" + }, + "homepage": "https://github.com/vercel-labs/json-render#readme", + "bugs": { + "url": "https://github.com/vercel-labs/json-render/issues" + }, + "publishConfig": { + "access": "public" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./catalog": { + "types": "./dist/catalog.d.ts", + "import": "./dist/catalog.mjs", + "require": "./dist/catalog.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "check-types": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@json-render/core": "workspace:*", + "@json-render/react": "workspace:*" + }, + "devDependencies": { + "@internal/typescript-config": "workspace:*", + "@types/react": "19.2.3", + "antd": "^6.3.1", + "tsup": "^8.0.2", + "typescript": "^5.4.5", + "zod": "^4.3.6" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "antd": "^6.0.0", + "zod": "^4.0.0" + } +} diff --git a/packages/antd/src/catalog.ts b/packages/antd/src/catalog.ts new file mode 100644 index 00000000..20e5ff3c --- /dev/null +++ b/packages/antd/src/catalog.ts @@ -0,0 +1,825 @@ +import { z } from "zod"; + +// ============================================================================= +// Shared validation schemas used across form components +// ============================================================================= + +const validationCheckSchema = z + .array( + z.object({ + type: z.string(), + message: z.string(), + args: z.record(z.string(), z.unknown()).optional(), + }), + ) + .nullable(); + +const validateOnSchema = z.enum(["change", "blur", "submit"]).nullable(); + +// ============================================================================= +// Ant Design Component Definitions +// ============================================================================= + +/** + * Ant Design component definitions for json-render catalogs. + * + * These can be used directly or extended with custom components. + * All components are built using Ant Design components. + */ +export const antdComponentDefinitions = { + // ========================================================================== + // Layout Components + // ========================================================================== + + Card: { + props: z.object({ + title: z.string().nullable(), + description: z.string().nullable(), + bordered: z.boolean().nullable(), + hoverable: z.boolean().nullable(), + loading: z.boolean().nullable(), + size: z.enum(["default", "small"]).nullable(), + }), + slots: ["default"], + description: + "Container card for content sections. Use for forms/content boxes.", + example: { title: "Overview", description: "Your account summary" }, + }, + + Stack: { + props: z.object({ + direction: z.enum(["horizontal", "vertical"]).nullable(), + gap: z.enum(["none", "sm", "md", "lg"]).nullable(), + align: z.enum(["start", "center", "end", "stretch"]).nullable(), + justify: z + .enum(["start", "center", "end", "between", "around"]) + .nullable(), + wrap: z.boolean().nullable(), + }), + slots: ["default"], + description: "Flex container for layouts", + example: { direction: "vertical", gap: "md" }, + }, + + Grid: { + props: z.object({ + columns: z.number().nullable(), + gap: z.enum(["sm", "md", "lg"]).nullable(), + }), + slots: ["default"], + description: "Grid layout (1-6 columns)", + example: { columns: 3, gap: "md" }, + }, + + Divider: { + props: z.object({ + orientation: z.enum(["horizontal", "vertical"]).nullable(), + dashed: z.boolean().nullable(), + text: z.string().nullable(), + }), + description: "A divider line that separates content.", + }, + + Space: { + props: z.object({ + direction: z.enum(["horizontal", "vertical"]).nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + wrap: z.boolean().nullable(), + align: z.enum(["start", "center", "end", "baseline"]).nullable(), + }), + slots: ["default"], + description: "Set components spacing with Ant Design Space component.", + }, + + // ========================================================================== + // Navigation Components + // ========================================================================== + + Tabs: { + props: z.object({ + tabs: z.array( + z.object({ + label: z.string(), + value: z.string(), + }), + ), + defaultValue: z.string().nullable(), + value: z.string().nullable(), + position: z.enum(["top", "bottom", "left", "right"]).nullable(), + type: z.enum(["line", "card"]).nullable(), + }), + slots: ["default"], + events: ["change"], + description: + "Tab navigation. Use { $bindState } on value for active tab binding.", + }, + + Collapse: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + content: z.string(), + }), + ), + accordion: z.boolean().nullable(), + }), + description: + "Collapsible sections. Items as [{title, content}]. Set accordion to true for single panel open.", + }, + + Menu: { + props: z.object({ + items: z.array( + z.object({ + label: z.string(), + key: z.string(), + icon: z.string().nullable(), + }), + ), + mode: z.enum(["horizontal", "vertical", "inline"]).nullable(), + selectedKey: z.string().nullable(), + }), + events: ["select"], + description: "Navigation menu with items.", + }, + + // ========================================================================== + // Overlay Components + // ========================================================================== + + Modal: { + props: z.object({ + title: z.string(), + description: z.string().nullable(), + openPath: z.string(), + width: z.number().nullable(), + footer: z.boolean().nullable(), + }), + slots: ["default"], + events: ["ok", "cancel"], + description: + "Modal dialog. Set openPath to a boolean state path. Use setState to toggle.", + }, + + Drawer: { + props: z.object({ + title: z.string(), + description: z.string().nullable(), + openPath: z.string(), + placement: z.enum(["top", "bottom", "left", "right"]).nullable(), + width: z.union([z.number(), z.string()]).nullable(), + }), + slots: ["default"], + events: ["close"], + description: "Side drawer panel. Set openPath to a boolean state path.", + }, + + Popover: { + props: z.object({ + trigger: z.string(), + content: z.string(), + placement: z.enum(["top", "bottom", "left", "right"]).nullable(), + }), + description: "Popover that appears on click of trigger.", + }, + + Tooltip: { + props: z.object({ + content: z.string(), + text: z.string(), + placement: z.enum(["top", "bottom", "left", "right"]).nullable(), + }), + description: "Hover tooltip. Shows content on hover over text.", + }, + + Dropdown: { + props: z.object({ + label: z.string(), + items: z.array( + z.object({ + label: z.string(), + key: z.string(), + icon: z.string().nullable(), + danger: z.boolean().nullable(), + }), + ), + }), + events: ["select"], + description: "Dropdown menu with trigger button and selectable items.", + }, + + // ========================================================================== + // Data Display Components + // ========================================================================== + + Table: { + props: z.object({ + columns: z.array(z.string()), + rows: z.array(z.array(z.string())), + caption: z.string().nullable(), + bordered: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + loading: z.boolean().nullable(), + }), + description: + "Data table. columns: header labels. rows: 2D array of cell strings.", + example: { + columns: ["Name", "Role"], + rows: [ + ["Alice", "Admin"], + ["Bob", "User"], + ], + }, + }, + + Heading: { + props: z.object({ + text: z.string(), + level: z.enum(["h1", "h2", "h3", "h4", "h5"]).nullable(), + }), + description: "Heading text (h1-h5)", + example: { text: "Welcome", level: "h1" }, + }, + + Text: { + props: z.object({ + text: z.string(), + type: z.enum(["secondary", "success", "warning", "danger"]).nullable(), + code: z.boolean().nullable(), + copyable: z.boolean().nullable(), + delete: z.boolean().nullable(), + mark: z.boolean().nullable(), + underline: z.boolean().nullable(), + strong: z.boolean().nullable(), + italic: z.boolean().nullable(), + }), + description: "Text with various styles", + example: { text: "Hello, world!", strong: true }, + }, + + Paragraph: { + props: z.object({ + text: z.string(), + ellipsis: z.boolean().nullable(), + rows: z.number().nullable(), + }), + description: "Paragraph text with optional ellipsis", + }, + + Image: { + props: z.object({ + src: z.string().nullable(), + alt: z.string(), + width: z.union([z.number(), z.string()]).nullable(), + height: z.union([z.number(), z.string()]).nullable(), + preview: z.boolean().nullable(), + }), + description: "Image component with preview support.", + }, + + Avatar: { + props: z.object({ + src: z.string().nullable(), + name: z.string(), + size: z + .union([z.number(), z.enum(["small", "default", "large"])]) + .nullable(), + shape: z.enum(["circle", "square"]).nullable(), + }), + description: "User avatar with fallback initials", + example: { name: "Jane Doe", size: "default" }, + }, + + Badge: { + props: z.object({ + text: z.string().nullable(), + count: z.number().nullable(), + color: z.string().nullable(), + status: z + .enum(["success", "processing", "default", "error", "warning"]) + .nullable(), + }), + slots: ["default"], + description: "Status badge or count indicator", + example: { count: 5 }, + }, + + Tag: { + props: z.object({ + text: z.string(), + color: z.string().nullable(), + closable: z.boolean().nullable(), + }), + events: ["close"], + description: "Tag for categorizing or marking.", + example: { text: "Active", color: "green" }, + }, + + Alert: { + props: z.object({ + title: z.string(), + message: z.string().nullable(), + type: z.enum(["info", "success", "warning", "error"]).nullable(), + closable: z.boolean().nullable(), + showIcon: z.boolean().nullable(), + }), + events: ["close"], + description: "Alert banner", + example: { + title: "Note", + message: "Your changes have been saved.", + type: "success", + }, + }, + + Progress: { + props: z.object({ + value: z.number(), + max: z.number().nullable(), + label: z.string().nullable(), + status: z.enum(["success", "exception", "normal", "active"]).nullable(), + type: z.enum(["line", "circle", "dashboard"]).nullable(), + }), + description: "Progress bar (value 0-100)", + example: { value: 65, max: 100, label: "Upload progress" }, + }, + + Skeleton: { + props: z.object({ + loading: z.boolean().nullable(), + active: z.boolean().nullable(), + rows: z.number().nullable(), + }), + slots: ["default"], + description: "Loading placeholder skeleton", + }, + + Spin: { + props: z.object({ + size: z.enum(["small", "default", "large"]).nullable(), + label: z.string().nullable(), + spinning: z.boolean().nullable(), + }), + slots: ["default"], + description: "Loading spinner indicator", + }, + + Empty: { + props: z.object({ + description: z.string().nullable(), + }), + description: "Empty state placeholder", + }, + + Statistic: { + props: z.object({ + title: z.string(), + value: z.union([z.number(), z.string()]), + prefix: z.string().nullable(), + suffix: z.string().nullable(), + }), + description: "Display statistic value with title", + }, + + Descriptions: { + props: z.object({ + title: z.string().nullable(), + items: z.array( + z.object({ + label: z.string(), + value: z.string(), + }), + ), + bordered: z.boolean().nullable(), + column: z.number().nullable(), + }), + description: "Display read-only data in key-value pairs", + }, + + Timeline: { + props: z.object({ + items: z.array( + z.object({ + content: z.string(), + color: z.string().nullable(), + }), + ), + }), + description: "Vertical timeline display", + }, + + Carousel: { + props: z.object({ + items: z.array( + z.object({ + title: z.string().nullable(), + description: z.string().nullable(), + }), + ), + autoplay: z.boolean().nullable(), + dots: z.boolean().nullable(), + }), + description: "Horizontally scrollable carousel.", + }, + + // ========================================================================== + // Form Input Components + // ========================================================================== + + Input: { + props: z.object({ + label: z.string(), + name: z.string(), + type: z.enum(["text", "email", "password", "number"]).nullable(), + placeholder: z.string().nullable(), + value: z.string().nullable(), + prefix: z.string().nullable(), + suffix: z.string().nullable(), + allowClear: z.boolean().nullable(), + showCount: z.boolean().nullable(), + maxLength: z.number().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + status: z.enum(["error", "warning"]).nullable(), + }), + events: ["submit", "focus", "blur", "change"], + description: + "Text input field. Use { $bindState } on value for two-way binding. Use checks for validation.", + example: { + label: "Email", + name: "email", + type: "email", + placeholder: "you@example.com", + }, + }, + + TextArea: { + props: z.object({ + label: z.string(), + name: z.string(), + placeholder: z.string().nullable(), + rows: z.number().nullable(), + value: z.string().nullable(), + allowClear: z.boolean().nullable(), + showCount: z.boolean().nullable(), + maxLength: z.number().nullable(), + autoSize: z + .union([ + z.boolean(), + z.object({ minRows: z.number(), maxRows: z.number() }), + ]) + .nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + description: + "Multi-line text input. Use { $bindState } on value for binding.", + }, + + InputNumber: { + props: z.object({ + label: z.string(), + name: z.string(), + placeholder: z.string().nullable(), + value: z.number().nullable(), + min: z.number().nullable(), + max: z.number().nullable(), + step: z.number().nullable(), + precision: z.number().nullable(), + prefix: z.string().nullable(), + suffix: z.string().nullable(), + }), + events: ["change"], + description: "Number input with controls.", + }, + + Select: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.union([ + z.string(), + z.object({ label: z.string(), value: z.string() }), + ]), + ), + placeholder: z.string().nullable(), + value: z.string().nullable(), + mode: z.enum(["multiple", "tags"]).nullable(), + allowClear: z.boolean().nullable(), + showSearch: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: + "Dropdown select input. Use { $bindState } on value for binding.", + }, + + Checkbox: { + props: z.object({ + label: z.string(), + name: z.string(), + checked: z.boolean().nullable(), + indeterminate: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: "Checkbox input. Use { $bindState } on checked for binding.", + }, + + CheckboxGroup: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.union([ + z.string(), + z.object({ label: z.string(), value: z.string() }), + ]), + ), + value: z.array(z.string()).nullable(), + }), + events: ["change"], + description: "Group of checkboxes.", + }, + + Radio: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.union([ + z.string(), + z.object({ label: z.string(), value: z.string() }), + ]), + ), + value: z.string().nullable(), + optionType: z.enum(["default", "button"]).nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: "Radio button group. Use { $bindState } on value for binding.", + }, + + Switch: { + props: z.object({ + label: z.string(), + name: z.string(), + checked: z.boolean().nullable(), + checkedChildren: z.string().nullable(), + unCheckedChildren: z.string().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: "Toggle switch. Use { $bindState } on checked for binding.", + }, + + Slider: { + props: z.object({ + label: z.string().nullable(), + min: z.number().nullable(), + max: z.number().nullable(), + step: z.number().nullable(), + value: z.union([z.number(), z.array(z.number())]).nullable(), + range: z.boolean().nullable(), + marks: z + .record( + z.string(), + z.union([ + z.string(), + z.object({ style: z.any(), label: z.string() }), + ]), + ) + .nullable(), + }), + events: ["change"], + description: "Range slider input. Use { $bindState } on value for binding.", + }, + + Rate: { + props: z.object({ + label: z.string().nullable(), + count: z.number().nullable(), + value: z.number().nullable(), + allowHalf: z.boolean().nullable(), + allowClear: z.boolean().nullable(), + }), + events: ["change"], + description: "Star rating component.", + }, + + DatePicker: { + props: z.object({ + label: z.string(), + name: z.string(), + placeholder: z.string().nullable(), + value: z.string().nullable(), + format: z.string().nullable(), + picker: z.enum(["date", "week", "month", "quarter", "year"]).nullable(), + showTime: z.boolean().nullable(), + }), + events: ["change"], + description: "Date picker input.", + }, + + TimePicker: { + props: z.object({ + label: z.string(), + name: z.string(), + placeholder: z.string().nullable(), + value: z.string().nullable(), + format: z.string().nullable(), + }), + events: ["change"], + description: "Time picker input.", + }, + + Upload: { + props: z.object({ + label: z.string(), + name: z.string(), + accept: z.string().nullable(), + multiple: z.boolean().nullable(), + maxCount: z.number().nullable(), + listType: z.enum(["text", "picture", "picture-card"]).nullable(), + buttonText: z.string().nullable(), + }), + events: ["change"], + description: "File upload component.", + }, + + Transfer: { + props: z.object({ + label: z.string(), + dataSource: z.array( + z.object({ + key: z.string(), + title: z.string(), + description: z.string().nullable(), + }), + ), + targetKeys: z.array(z.string()).nullable(), + titles: z.array(z.string()).nullable(), + }), + events: ["change"], + description: "Transfer items between two columns.", + }, + + // ========================================================================== + // Action Components + // ========================================================================== + + Button: { + props: z.object({ + label: z.string(), + type: z.enum(["primary", "default", "dashed", "text", "link"]).nullable(), + danger: z.boolean().nullable(), + disabled: z.boolean().nullable(), + loading: z.boolean().nullable(), + icon: z.string().nullable(), + block: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + }), + events: ["press"], + description: "Clickable button. Bind on.press for handler.", + example: { label: "Submit", type: "primary" }, + }, + + ButtonGroup: { + props: z.object({ + buttons: z.array( + z.object({ + label: z.string(), + value: z.string(), + type: z + .enum(["primary", "default", "dashed", "text", "link"]) + .nullable(), + }), + ), + selected: z.string().nullable(), + }), + events: ["change"], + description: + "Segmented button group. Use { $bindState } on selected for selected value.", + }, + + Link: { + props: z.object({ + label: z.string(), + href: z.string(), + target: z.enum(["_blank", "_self", "_parent", "_top"]).nullable(), + disabled: z.boolean().nullable(), + }), + events: ["press"], + description: "Anchor link. Bind on.press for click handler.", + }, + + Pagination: { + props: z.object({ + total: z.number(), + pageSize: z.number().nullable(), + current: z.number().nullable(), + showSizeChanger: z.boolean().nullable(), + showQuickJumper: z.boolean().nullable(), + simple: z.boolean().nullable(), + }), + events: ["change"], + description: + "Page navigation. Use { $bindState } on current for current page number.", + }, + + Segmented: { + props: z.object({ + options: z.array( + z.union([ + z.string(), + z.object({ + label: z.string(), + value: z.string(), + icon: z.string().nullable(), + }), + ]), + ), + value: z.string().nullable(), + block: z.boolean().nullable(), + }), + events: ["change"], + description: "Segmented control for toggling between options.", + }, + + Steps: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + description: z.string().nullable(), + icon: z.string().nullable(), + }), + ), + current: z.number().nullable(), + direction: z.enum(["horizontal", "vertical"]).nullable(), + status: z.enum(["wait", "process", "finish", "error"]).nullable(), + }), + events: ["change"], + description: "Step navigation bar.", + }, + + Result: { + props: z.object({ + status: z + .enum(["success", "error", "info", "warning", "404", "403", "500"]) + .nullable(), + title: z.string(), + subTitle: z.string().nullable(), + }), + slots: ["default"], + description: "Result page for success/error states.", + }, +}; + +// ============================================================================= +// Types +// ============================================================================= + +/** + * Type for a component definition + */ +export type ComponentDefinition = { + props: z.ZodType; + slots?: string[]; + events?: string[]; + description: string; + example?: Record; +}; + +/** + * Infer the props type for an antd component by name. + * Derives the TypeScript type directly from the Zod schema, + * so component implementations stay in sync with catalog definitions. + * + * @example + * ```ts + * type CardProps = AntdProps<"Card">; + * // { title: string | null; description: string | null; ... } + * ``` + */ +export type AntdProps = + z.output<(typeof antdComponentDefinitions)[K]["props"]>; + +/** + * Bindings configuration for state binding paths. + * Used for two-way data binding with state management. + */ +export type BindingsConfig = { + [key: string]: string | undefined; +}; + +/** + * Event emit function type + */ +export type EmitFunction = (eventName: string) => void; diff --git a/packages/antd/src/components.tsx b/packages/antd/src/components.tsx new file mode 100644 index 00000000..cfc8f107 --- /dev/null +++ b/packages/antd/src/components.tsx @@ -0,0 +1,1392 @@ +"use client"; + +import { useState } from "react"; +import dayjs from "dayjs"; +import { + useBoundProp, + useStateBinding, + useFieldValidation, + type BaseComponentProps, +} from "@json-render/react"; + +import { + Card, + Space, + Divider, + Tabs, + Collapse, + Menu, + Modal, + Drawer, + Popover, + Tooltip, + Dropdown, + Table, + Typography, + Image, + Avatar, + Badge, + Tag, + Alert, + Progress, + Skeleton, + Spin, + Empty, + Statistic, + Descriptions, + Timeline, + Carousel, + Input, + InputNumber, + Select, + Checkbox, + Radio, + Switch, + Slider, + Rate, + DatePicker, + TimePicker, + Upload, + Transfer, + Button, + Pagination, + Segmented, + Steps, + Result, + Flex, + Form, +} from "antd"; +import type { UploadFile } from "antd/es/upload/interface"; +import type { AntdProps } from "./catalog"; + +const { + Title, + Text: AntText, + Paragraph: AntParagraph, + Link: AntLink, +} = Typography; + +// ============================================================================= +// Standard Component Implementations +// ============================================================================= + +/** + * Ant Design component implementations for json-render. + * + * Pass to `defineRegistry()` from `@json-render/react` to create a + * component registry for rendering JSON specs with Ant Design components. + * + * @example + * ```ts + * import { defineRegistry } from "@json-render/react"; + * import { antdComponents } from "@json-render/antd"; + * + * const { registry } = defineRegistry(catalog, { + * components: { + * Card: antdComponents.Card, + * Button: antdComponents.Button, + * }, + * }); + * ``` + */ +export const antdComponents = { + // ── Layout ──────────────────────────────────────────────────────────── + + Card: ({ props, children }: BaseComponentProps>) => { + return ( + + {props.description && ( + {props.description} + )} + {children} + + ); + }, + + Stack: ({ props, children }: BaseComponentProps>) => { + const isHorizontal = props.direction === "horizontal"; + const gapMap: Record = { + none: 0, + sm: 8, + md: 16, + lg: 24, + }; + const alignMap: Record< + string, + "flex-start" | "center" | "flex-end" | "stretch" + > = { + start: "flex-start", + center: "center", + end: "flex-end", + stretch: "stretch", + }; + const justifyMap: Record< + string, + "flex-start" | "center" | "flex-end" | "space-between" | "space-around" + > = { + start: "flex-start", + center: "center", + end: "flex-end", + between: "space-between", + around: "space-around", + }; + + return ( + + {children} + + ); + }, + + Grid: ({ props, children }: BaseComponentProps>) => { + const n = Math.max(1, Math.min(12, props.columns ?? 3)); + const gapMap: Record = { + sm: 8, + md: 16, + lg: 24, + }; + + return ( +
+ {children} +
+ ); + }, + + Divider: ({ props }: BaseComponentProps>) => { + return ( + + {props.text} + + ); + }, + + Space: ({ props, children }: BaseComponentProps>) => { + const sizeMap: Record = { + small: 8, + middle: 16, + large: 24, + }; + + return ( + + {children} + + ); + }, + + // ── Navigation ───────────────────────────────────────────────────────── + + Tabs: ({ + props, + children, + bindings, + emit, + }: BaseComponentProps>) => { + const tabs = props.tabs ?? []; + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState( + props.defaultValue ?? tabs[0]?.value ?? "", + ); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? tabs[0]?.value ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + const items = tabs.map((tab) => ({ + key: tab.value, + label: tab.label, + children: tab.value === value ? children : null, + })); + + return ( + { + setValue(key); + emit("change"); + }} + tabPosition={props.position ?? "top"} + type={props.type ?? "line"} + items={items} + /> + ); + }, + + Collapse: ({ props }: BaseComponentProps>) => { + const items = props.items ?? []; + + const collapseItems = items.map((item, idx) => ({ + key: String(idx), + label: item.title, + children: {item.content}, + })); + + return ( + + ); + }, + + Menu: ({ props, emit }: BaseComponentProps>) => { + const items = props.items ?? []; + + const menuItems = items.map((item) => ({ + key: item.key, + label: item.label, + icon: item.icon ? : undefined, + })); + + return ( + emit("select")} + /> + ); + }, + + // ── Overlay ──────────────────────────────────────────────────────────── + + Modal: ({ + props, + children, + emit, + }: BaseComponentProps>) => { + const [open, setOpen] = useStateBinding(props.openPath ?? ""); + + return ( + { + emit("ok"); + setOpen(false); + }} + onCancel={() => { + emit("cancel"); + setOpen(false); + }} + width={props.width ?? 520} + footer={props.footer === false ? null : undefined} + > + {props.description && {props.description}} + {children} + + ); + }, + + Drawer: ({ + props, + children, + emit, + }: BaseComponentProps>) => { + const [open, setOpen] = useStateBinding(props.openPath ?? ""); + + return ( + { + emit("close"); + setOpen(false); + }} + placement={props.placement ?? "right"} + width={props.width ?? 378} + > + {props.description && {props.description}} + {children} + + ); + }, + + Popover: ({ props }: BaseComponentProps>) => { + return ( + + {props.trigger} + + ); + }, + + Tooltip: ({ props }: BaseComponentProps>) => { + return ( + + {props.text} + + ); + }, + + Dropdown: ({ props, emit }: BaseComponentProps>) => { + const items = props.items ?? []; + + const menuItems = items.map((item) => ({ + key: item.key, + label: item.label, + icon: item.icon ? : undefined, + danger: item.danger ?? false, + })); + + return ( + emit("select"), + }} + > + + + ); + }, + + // ── Data Display ─────────────────────────────────────────────────────── + + Table: ({ props }: BaseComponentProps>) => { + const columns = props.columns ?? []; + const rows = props.rows ?? []; + + const dataSource = rows.map((row, idx) => ({ + key: String(idx), + ...row.reduce( + (acc, cell, colIdx) => { + acc[`col${colIdx}`] = cell; + return acc; + }, + {} as Record, + ), + })); + + const tableColumns = columns.map((col, idx) => ({ + title: col, + dataIndex: `col${idx}`, + key: `col${idx}`, + })); + + return ( + {props.caption} + : undefined + } + pagination={false} + /> + ); + }, + + Heading: ({ props }: BaseComponentProps>) => { + const levelMap: Record = { + h1: 1, + h2: 2, + h3: 3, + h4: 4, + h5: 5, + }; + + return ( + {props.text} + ); + }, + + Text: ({ props }: BaseComponentProps>) => { + return ( + + {props.text} + + ); + }, + + Paragraph: ({ props }: BaseComponentProps>) => { + return ( + + {props.text} + + ); + }, + + Image: ({ props }: BaseComponentProps>) => { + if (props.src) { + return ( + + ); + } + return ( +
+ {props.alt || "img"} +
+ ); + }, + + Avatar: ({ props }: BaseComponentProps>) => { + const name = props.name || "?"; + + return ( + + {!props.src && name.charAt(0).toUpperCase()} + + ); + }, + + Badge: ({ props, children }: BaseComponentProps>) => { + if (props.count !== undefined) { + return ( + + {children} + + ); + } + return ( + + ); + }, + + Tag: ({ props, emit }: BaseComponentProps>) => { + return ( + { + e.preventDefault(); + emit("close"); + }} + > + {props.text} + + ); + }, + + Alert: ({ props, emit }: BaseComponentProps>) => { + return ( + emit("close")} + /> + ); + }, + + Progress: ({ props }: BaseComponentProps>) => { + const max = props.max ?? 100; + const value = Math.min(max, Math.max(0, props.value ?? 0)); + const percent = Math.round((value / max) * 100); + + return ( + props.label ?? `${percent}%`} + /> + ); + }, + + Skeleton: ({ + props, + children, + }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Spin: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Empty: ({ props }: BaseComponentProps>) => { + return ; + }, + + Statistic: ({ props }: BaseComponentProps>) => { + return ( + + ); + }, + + Descriptions: ({ props }: BaseComponentProps>) => { + const items = props.items ?? []; + + return ( + ({ + key: item.label, + label: item.label, + children: item.value, + }))} + /> + ); + }, + + Timeline: ({ props }: BaseComponentProps>) => { + const items = props.items ?? []; + + return ( + ({ + color: item.color ?? undefined, + children: item.content, + }))} + /> + ); + }, + + Carousel: ({ props }: BaseComponentProps>) => { + const items = props.items ?? []; + + return ( + + {items.map((item, idx) => ( +
+ {item.title && {item.title}} + {item.description && ( + {item.description} + )} +
+ ))} +
+ ); + }, + + // ── Form Inputs ──────────────────────────────────────────────────────── + + Input: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + const validateOn = props.validateOn ?? "blur"; + + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + + return ( + 0 ? "error" : (props.status ?? undefined) + } + help={errors[0]} + > + : undefined} + suffix={props.suffix ? : undefined} + allowClear={props.allowClear ?? false} + showCount={props.showCount ?? false} + maxLength={props.maxLength ?? undefined} + status={props.status ?? (errors.length > 0 ? "error" : undefined)} + onChange={(e) => { + setValue(e.target.value); + if (hasValidation && validateOn === "change") validate(); + emit("change"); + }} + onFocus={() => emit("focus")} + onBlur={() => { + if (hasValidation && validateOn === "blur") validate(); + emit("blur"); + }} + onPressEnter={() => emit("submit")} + /> + + ); + }, + + TextArea: ({ + props, + bindings, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + const validateOn = props.validateOn ?? "blur"; + + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + + return ( + 0 ? "error" : undefined} + help={errors[0]} + > + 0 ? "error" : undefined} + onChange={(e) => { + setValue(e.target.value); + if (hasValidation && validateOn === "change") validate(); + }} + onBlur={() => { + if (hasValidation && validateOn === "blur") validate(); + }} + /> + + ); + }, + + InputNumber: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as number | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(null); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? null) : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + return ( + + : undefined} + addonAfter={ + props.suffix ? : undefined + } + onChange={(val) => { + setValue(val as number); + emit("change"); + }} + /> + + ); + }, + + Select: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + const validateOn = props.validateOn ?? "change"; + + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + + const options = (props.options ?? []).map((opt) => + typeof opt === "string" ? { label: opt, value: opt } : opt, + ); + + return ( + 0 ? "error" : undefined} + help={errors[0]} + > + { + if (isBound) { + setBoundValue(e.target.value); + } + emit("change"); + }} + /> + + ); + }, + + TimePicker: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + // Convert string value to dayjs object for antd TimePicker + const dayjsValue = value ? dayjs(value, props.format ?? "HH:mm:ss") : null; + + return ( + + { + setValue(timeString as string); + emit("change"); + }} + /> + + ); + }, + + Upload: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [fileList, setFileList] = useState([]); + + return ( + + false} + onChange={(info) => { + setFileList(info.fileList); + emit("change"); + }} + > + + + + ); + }, + + Transfer: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundTargetKeys, setBoundTargetKeys] = useBoundProp( + (props.targetKeys as string[] | null) ?? undefined, + bindings?.targetKeys, + ); + const [localTargetKeys, setLocalTargetKeys] = useState([]); + const isBound = !!bindings?.targetKeys; + const targetKeys = isBound ? (boundTargetKeys ?? []) : localTargetKeys; + const setTargetKeys = isBound ? setBoundTargetKeys : setLocalTargetKeys; + + const dataSource = (props.dataSource ?? []).map((item) => ({ + ...item, + description: item.description ?? undefined, + })); + + return ( + + { + setTargetKeys(newTargetKeys as string[]); + emit("change"); + }} + render={(item) => item.title ?? ""} + /> + + ); + }, + + // ── Actions ─────────────────────────────────────────────────────────── + + Button: ({ props, emit, on }: BaseComponentProps>) => { + const type = props.type ?? "default"; + + return ( + + ); + }, + + ButtonGroup: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const buttons = props.buttons ?? []; + const [boundSelected, setBoundSelected] = useBoundProp( + props.selected as string | undefined, + bindings?.selected, + ); + const [localValue, setLocalValue] = useState(buttons[0]?.value ?? ""); + const isBound = !!bindings?.selected; + const value = isBound ? (boundSelected ?? "") : localValue; + const setValue = isBound ? setBoundSelected : setLocalValue; + + return ( + ({ + label: btn.label, + value: btn.value, + }))} + value={value} + onChange={(val) => { + setValue(val as string); + emit("change"); + }} + /> + ); + }, + + Link: ({ props, emit, on }: BaseComponentProps>) => { + const handlePress = () => { + emit("press"); + }; + + return ( + { + const press = on("press"); + if (press.shouldPreventDefault || !props.href || props.href === "#") { + e.preventDefault(); + } + press.emit(); + }} + > + {props.label} + + ); + }, + + Pagination: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundCurrent, setBoundCurrent] = useBoundProp( + props.current as number | undefined, + bindings?.current, + ); + const [localCurrent, setLocalCurrent] = useState(1); + const isBound = !!bindings?.current; + const current = isBound ? (boundCurrent ?? 1) : localCurrent; + const setCurrent = isBound ? setBoundCurrent : setLocalCurrent; + + return ( + { + setCurrent(page); + emit("change"); + }} + /> + ); + }, + + Segmented: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + const options = (props.options ?? []).map((opt) => + typeof opt === "string" + ? opt + : { + label: opt.label, + value: opt.value, + icon: opt.icon ? : undefined, + }, + ); + + return ( + { + setValue(val as string); + emit("change"); + }} + /> + ); + }, + + Steps: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundCurrent, setBoundCurrent] = useBoundProp( + props.current as number | undefined, + bindings?.current, + ); + const [localCurrent, setLocalCurrent] = useState(0); + const isBound = !!bindings?.current; + const current = isBound ? (boundCurrent ?? 0) : localCurrent; + const setCurrent = isBound ? setBoundCurrent : setLocalCurrent; + + const items = (props.items ?? []).map((item) => ({ + title: item.title, + description: item.description, + icon: item.icon ? : undefined, + })); + + return ( + { + setCurrent(current); + emit("change"); + }} + /> + ); + }, + + Result: ({ props, children }: BaseComponentProps>) => { + return ( + + ); + }, +}; diff --git a/packages/antd/src/index.ts b/packages/antd/src/index.ts new file mode 100644 index 00000000..a15cb7f3 --- /dev/null +++ b/packages/antd/src/index.ts @@ -0,0 +1,9 @@ +export { + antdComponentDefinitions, + type AntdProps, + type ComponentDefinition, + type BindingsConfig, + type EmitFunction, +} from "./catalog"; + +export { antdComponents } from "./components"; diff --git a/packages/antd/tsconfig.json b/packages/antd/tsconfig.json new file mode 100644 index 00000000..6c6204c0 --- /dev/null +++ b/packages/antd/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@internal/typescript-config/react-library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/antd/tsup.config.ts b/packages/antd/tsup.config.ts new file mode 100644 index 00000000..7f625c08 --- /dev/null +++ b/packages/antd/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts", "src/catalog.ts"], + format: ["cjs", "esm"], + dts: true, + sourcemap: true, + clean: true, + external: [ + "react", + "react-dom", + "@json-render/core", + "@json-render/react", + "antd", + "dayjs", + "zod", + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26ce4040..6202d93f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1025,6 +1025,40 @@ importers: specifier: ^3.2.5 version: 3.2.5(typescript@5.9.3) + packages/antd: + dependencies: + '@json-render/core': + specifier: workspace:* + version: link:../core + '@json-render/react': + specifier: workspace:* + version: link:../react + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + devDependencies: + '@internal/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/react': + specifier: 19.2.3 + version: 19.2.3 + antd: + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tsup: + specifier: ^8.0.2 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.4.5 + version: 5.9.3 + zod: + specifier: ^4.3.6 + version: 4.3.6 + packages/codegen: dependencies: '@json-render/core': @@ -1645,6 +1679,41 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@ant-design/colors@8.0.1': + resolution: {integrity: sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==} + + '@ant-design/cssinjs-utils@2.1.2': + resolution: {integrity: sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + '@ant-design/cssinjs@2.1.2': + resolution: {integrity: sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/fast-color@3.0.1': + resolution: {integrity: sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==} + engines: {node: '>=8.x'} + + '@ant-design/icons-svg@4.4.2': + resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==} + + '@ant-design/icons@6.1.0': + resolution: {integrity: sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/react-slick@2.0.0': + resolution: {integrity: sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==} + peerDependencies: + react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@antfu/ni@25.0.0': resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} hasBin: true @@ -2329,6 +2398,12 @@ packages: '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + '@emotion/unitless@0.7.5': + resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -4414,6 +4489,289 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rc-component/async-validator@5.1.0': + resolution: {integrity: sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==} + engines: {node: '>=14.x'} + + '@rc-component/cascader@1.14.0': + resolution: {integrity: sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/checkbox@2.0.0': + resolution: {integrity: sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/collapse@1.2.0': + resolution: {integrity: sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/color-picker@3.1.1': + resolution: {integrity: sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/context@2.0.1': + resolution: {integrity: sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/dialog@1.8.4': + resolution: {integrity: sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/drawer@1.4.2': + resolution: {integrity: sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/dropdown@1.0.2': + resolution: {integrity: sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==} + peerDependencies: + react: '>=16.11.0' + react-dom: '>=16.11.0' + + '@rc-component/form@1.6.2': + resolution: {integrity: sha512-OgIn2RAoaSBqaIgzJf/X6iflIa9LpTozci1lagLBdURDFhGA370v0+T0tXxOi8YShMjTha531sFhwtnrv+EJaQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/image@1.6.0': + resolution: {integrity: sha512-tSfn2ZE/oP082g4QIOxeehkmgnXB7R+5AFj/lIFr4k7pEuxHBdyGIq9axoCY9qea8NN0DY6p4IB/F07tLqaT5A==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/input-number@1.6.2': + resolution: {integrity: sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/input@1.1.2': + resolution: {integrity: sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@rc-component/mentions@1.6.0': + resolution: {integrity: sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/menu@1.2.0': + resolution: {integrity: sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/mini-decimal@1.1.0': + resolution: {integrity: sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==} + engines: {node: '>=8.x'} + + '@rc-component/motion@1.3.1': + resolution: {integrity: sha512-Wo1mkd0tCcHtvYvpPOmlYJz546z16qlsiwaygmW7NPJpOZOF9GBjhGzdzZSsC2lEJ1IUkWLF4gMHlRA1aSA+Yw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/mutate-observer@2.0.1': + resolution: {integrity: sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/notification@1.2.0': + resolution: {integrity: sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/overflow@1.0.0': + resolution: {integrity: sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/pagination@1.2.0': + resolution: {integrity: sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/picker@1.9.0': + resolution: {integrity: sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==} + engines: {node: '>=12.x'} + peerDependencies: + date-fns: '>= 2.x' + dayjs: '>= 1.x' + luxon: '>= 3.x' + moment: '>= 2.x' + react: '>=16.9.0' + react-dom: '>=16.9.0' + peerDependenciesMeta: + date-fns: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + + '@rc-component/portal@2.2.0': + resolution: {integrity: sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==} + engines: {node: '>=12.x'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/progress@1.0.2': + resolution: {integrity: sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/qrcode@1.1.1': + resolution: {integrity: sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/rate@1.0.1': + resolution: {integrity: sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/resize-observer@1.1.1': + resolution: {integrity: sha512-NfXXMmiR+SmUuKE1NwJESzEUYUFWIDUn2uXpxCTOLwiRUUakd62DRNFjRJArgzyFW8S5rsL4aX5XlyIXyC/vRA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/segmented@1.3.0': + resolution: {integrity: sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@rc-component/select@1.6.14': + resolution: {integrity: sha512-T1IWeLlSas7Z/igZtPtJ/bweCxMMkXIGKQBtnigK+I/n1AVNjCs+ZdL3Fj42mq3uqm4sd1uzeQLZkdCqR26ADw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '*' + react-dom: '*' + + '@rc-component/slider@1.0.1': + resolution: {integrity: sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/steps@1.2.2': + resolution: {integrity: sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/switch@1.0.3': + resolution: {integrity: sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/table@1.9.1': + resolution: {integrity: sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/tabs@1.7.0': + resolution: {integrity: sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/textarea@1.1.2': + resolution: {integrity: sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/tooltip@1.4.0': + resolution: {integrity: sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/tour@2.3.0': + resolution: {integrity: sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/tree-select@1.8.0': + resolution: {integrity: sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==} + peerDependencies: + react: '*' + react-dom: '*' + + '@rc-component/tree@1.2.3': + resolution: {integrity: sha512-mG8hF2ogQcKaEpfyxzPvMWqqkptofd7Sf+YiXOpPzuXLTLwNKfLDJtysc1/oybopbnzxNqWh2Vgwi+GYwNIb7w==} + engines: {node: '>=10.x'} + peerDependencies: + react: '*' + react-dom: '*' + + '@rc-component/trigger@3.9.0': + resolution: {integrity: sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/upload@1.1.0': + resolution: {integrity: sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/util@1.9.0': + resolution: {integrity: sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/virtual-list@1.0.2': + resolution: {integrity: sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + '@react-email/body@0.2.1': resolution: {integrity: sha512-ljDiQiJDu/Fq//vSIIP0z5Nuvt4+DX1RqGasstChDGJB/14ogd4VdNS9aacoede/ZjGy3o3Qb+cxyS+XgM6SwQ==} engines: {node: '>=20.0.0'} @@ -6147,6 +6505,12 @@ packages: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} + antd@6.3.1: + resolution: {integrity: sha512-8pRjvxitZFyrYAtgwml93Km7fCXjw9IeqlmzpIsusRsmO3eWFVrOMum6+0TsGCtR/WrXVnPwfsgrFg3ChzGCeA==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -6679,6 +7043,9 @@ packages: resolution: {integrity: sha512-jpKJjBTretQACTGLNuvnozP1JdP2ZLrjdGdBgk/tz1VfXlUcBhhSZW6vEsuThmeot/yjvSrPQKEgfF3X2Lpi8Q==} hasBin: true + compute-scroll-into-view@3.1.1: + resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -6871,6 +7238,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -8397,6 +8767,9 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-mobile@5.0.0: + resolution: {integrity: sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==} + is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -8864,6 +9237,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json2mq@0.2.0: + resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -10649,6 +11025,9 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -10923,6 +11302,9 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-convert@0.2.1: + resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -11054,6 +11436,9 @@ packages: babel-plugin-macros: optional: true + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -11192,6 +11577,10 @@ packages: throat@6.0.2: resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} + throttle-debounce@5.0.2: + resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} + engines: {node: '>=12.22'} + throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -12250,6 +12639,52 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@ant-design/colors@8.0.1': + dependencies: + '@ant-design/fast-color': 3.0.1 + + '@ant-design/cssinjs-utils@2.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@babel/runtime': 7.28.6 + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@ant-design/cssinjs@2.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@emotion/hash': 0.8.0 + '@emotion/unitless': 0.7.5 + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + csstype: 3.2.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + stylis: 4.3.6 + + '@ant-design/fast-color@3.0.1': {} + + '@ant-design/icons-svg@4.4.2': {} + + '@ant-design/icons@6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@ant-design/colors': 8.0.1 + '@ant-design/icons-svg': 4.4.2 + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@ant-design/react-slick@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + clsx: 2.1.1 + json2mq: 0.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + throttle-debounce: 5.0.2 + '@antfu/ni@25.0.0': dependencies: ansis: 4.2.0 @@ -13161,6 +13596,10 @@ snapshots: tslib: 2.8.1 optional: true + '@emotion/hash@0.8.0': {} + + '@emotion/unitless@0.7.5': {} + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -16306,6 +16745,352 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rc-component/async-validator@5.1.0': + dependencies: + '@babel/runtime': 7.28.6 + + '@rc-component/cascader@1.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/select': 1.6.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/tree': 1.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/checkbox@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/collapse@1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/color-picker@3.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@ant-design/fast-color': 3.0.1 + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/context@2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/dialog@1.8.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/portal': 2.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/drawer@1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/portal': 2.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/dropdown@1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/trigger': 3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/form@1.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/async-validator': 5.1.0 + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/image@1.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/portal': 2.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/input-number@1.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/mini-decimal': 1.1.0 + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/input@1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/mentions@1.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/input': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/menu': 1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/textarea': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/trigger': 3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/menu@1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/overflow': 1.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/trigger': 3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/mini-decimal@1.1.0': + dependencies: + '@babel/runtime': 7.28.6 + + '@rc-component/motion@1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/mutate-observer@2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/notification@1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/overflow@1.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@rc-component/resize-observer': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/pagination@1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/picker@1.9.0(dayjs@1.11.19)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/overflow': 1.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/resize-observer': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/trigger': 3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + dayjs: 1.11.19 + + '@rc-component/portal@2.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/progress@1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/qrcode@1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/rate@1.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/resize-observer@1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/segmented@1.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/select@1.6.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/overflow': 1.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/trigger': 3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/virtual-list': 1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/slider@1.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/steps@1.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/switch@1.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/table@1.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/context': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/resize-observer': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/virtual-list': 1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/tabs@1.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/dropdown': 1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/menu': 1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/resize-observer': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/textarea@1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/input': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/resize-observer': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/tooltip@1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/trigger': 3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/tour@2.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/portal': 2.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/trigger': 3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/tree-select@1.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/select': 1.6.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/tree': 1.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/tree@1.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/virtual-list': 1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/trigger@3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/portal': 2.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/resize-observer': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/upload@1.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@rc-component/util@1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + is-mobile: 5.0.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 18.3.1 + + '@rc-component/virtual-list@1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@rc-component/resize-observer': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@react-email/body@0.2.1(react@19.2.4)': dependencies: react: 19.2.4 @@ -16980,14 +17765,14 @@ snapshots: '@remotion/media-parser': 4.0.418 '@remotion/studio': 4.0.418(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@remotion/studio-shared': 4.0.418(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - css-loader: 5.2.7(webpack@5.96.1(esbuild@0.25.0)) + css-loader: 5.2.7(webpack@5.96.1) esbuild: 0.25.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) react-refresh: 0.9.0 remotion: 4.0.418(react-dom@19.2.3(react@19.2.3))(react@19.2.3) source-map: 0.7.3 - style-loader: 4.0.0(webpack@5.96.1(esbuild@0.25.0)) + style-loader: 4.0.0(webpack@5.96.1) webpack: 5.96.1(esbuild@0.25.0) transitivePeerDependencies: - '@swc/core' @@ -18354,6 +19139,63 @@ snapshots: ansis@4.2.0: {} + antd@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@ant-design/colors': 8.0.1 + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@ant-design/cssinjs-utils': 2.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@ant-design/fast-color': 3.0.1 + '@ant-design/icons': 6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@ant-design/react-slick': 2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@babel/runtime': 7.28.6 + '@rc-component/cascader': 1.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/checkbox': 2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/collapse': 1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/color-picker': 3.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/dialog': 1.8.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/drawer': 1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/dropdown': 1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/form': 1.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/image': 1.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/input': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/input-number': 1.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/mentions': 1.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/menu': 1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/motion': 1.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/mutate-observer': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/notification': 1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/pagination': 1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/picker': 1.9.0(dayjs@1.11.19)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/progress': 1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/qrcode': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/rate': 1.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/resize-observer': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/segmented': 1.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/select': 1.6.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/slider': 1.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/steps': 1.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/switch': 1.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/table': 1.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/tabs': 1.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/textarea': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/tooltip': 1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/tour': 2.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/tree': 1.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/tree-select': 1.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/trigger': 3.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/upload': 1.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/util': 1.9.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + clsx: 2.1.1 + dayjs: 1.11.19 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + scroll-into-view-if-needed: 3.1.0 + throttle-debounce: 5.0.2 + transitivePeerDependencies: + - date-fns + - luxon + - moment + any-promise@1.3.0: {} anymatch@3.1.3: @@ -18963,6 +19805,8 @@ snapshots: amdefine: 1.0.1 commander: 2.8.1 + compute-scroll-into-view@3.1.1: {} + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -19046,7 +19890,7 @@ snapshots: css-gradient-parser@0.0.17: {} - css-loader@5.2.7(webpack@5.96.1(esbuild@0.25.0)): + css-loader@5.2.7(webpack@5.96.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) loader-utils: 2.0.4 @@ -19159,6 +20003,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + dayjs@1.11.19: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -21098,6 +21944,8 @@ snapshots: is-map@2.0.3: {} + is-mobile@5.0.0: {} + is-negative-zero@2.0.3: {} is-node-process@1.2.0: {} @@ -21851,6 +22699,10 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json2mq@0.2.0: + dependencies: + string-convert: 0.2.1 + json5@2.2.3: {} jsonfile@4.0.0: @@ -24495,6 +25347,10 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + scroll-into-view-if-needed@3.1.0: + dependencies: + compute-scroll-into-view: 3.1.1 + section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -24933,6 +25789,8 @@ snapshots: string-argv@0.3.2: {} + string-convert@0.2.1: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -25061,7 +25919,7 @@ snapshots: structured-headers@0.4.1: {} - style-loader@4.0.0(webpack@5.96.1(esbuild@0.25.0)): + style-loader@4.0.0(webpack@5.96.1): dependencies: webpack: 5.96.1(esbuild@0.25.0) @@ -25090,6 +25948,8 @@ snapshots: client-only: 0.0.1 react: 19.2.4 + stylis@4.3.6: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -25249,6 +26109,8 @@ snapshots: throat@6.0.2: {} + throttle-debounce@5.0.2: {} + throttleit@2.1.0: {} tiny-inflate@1.0.3: {} From aae8737342b089032a5f75c1c676426232dd0973 Mon Sep 17 00:00:00 2001 From: LuZhuJun Date: Sat, 7 Mar 2026 16:45:03 +0800 Subject: [PATCH 2/9] feat(antd): add 22 new components and update to Antd v6 APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New components: - Layout: Layout, LayoutHeader, LayoutContent, LayoutFooter, LayoutSider - Layout: Flex, Row, Col, Masonry - Navigation: Affix, Anchor, Breadcrumb, BackTop - Data Display: Calendar, List, Tree, QRCode - Data Entry: AutoComplete, Cascader, ColorPicker, Mentions, TreeSelect Breaking changes to existing components: - Card: replace `bordered`/`description` with `variant`, add `extra`/`cover`/`actions` slots - Stack: align props to Antd v6 Flex API (gap/justify/align values updated) - Divider: add `titlePlacement`, `variant`; `orientation` now means line direction - Space: rename `direction` → `orientation` Internal: - Add SlottedComponentProps

type for slot-bearing components - Grid: use Children.toArray() for uniform child handling - Add dayjs as devDependency - Update README: 50+ → 70+ components, expand component tables --- packages/antd/README.md | 31 +- packages/antd/package.json | 1 + packages/antd/src/catalog.ts | 653 ++++++++++++++++++-- packages/antd/src/components.tsx | 981 +++++++++++++++++++++++++++---- pnpm-lock.yaml | 11 +- 5 files changed, 1503 insertions(+), 174 deletions(-) diff --git a/packages/antd/README.md b/packages/antd/README.md index b31a124f..3e71e612 100644 --- a/packages/antd/README.md +++ b/packages/antd/README.md @@ -1,6 +1,6 @@ # @json-render/antd -Pre-built [Ant Design](https://ant.design/) components for json-render. Drop-in catalog definitions and React implementations for 50+ components built on Ant Design. +Pre-built [Ant Design](https://ant.design/) components for json-render. Drop-in catalog definitions and React implementations for 70+ components built on Ant Design. ## Installation @@ -117,8 +117,16 @@ const { registry } = defineRegistry(catalog, { | Component | Description | |-----------|-------------| | `Card` | Container card with optional title and description | -| `Stack` | Flex container (horizontal/vertical) with gap, alignment, justify | -| `Grid` | Grid layout | +| `Flex` | Flex layout container with gap, alignment, justify | +| `Stack` | Stack layout container (alias for Flex) | +| `Row` | Grid row (24-column system) | +| `Col` | Grid column with span, offset | +| `Masonry` | Masonry layout with responsive columns | +| `Layout` | Antd layout container | +| `LayoutHeader` | Layout header | +| `LayoutContent` | Layout main content area | +| `LayoutFooter` | Layout footer | +| `LayoutSider` | Layout sidebar | | `Divider` | Visual separator line | | `Space` | Spacing component | @@ -129,6 +137,10 @@ const { registry } = defineRegistry(catalog, { | `Tabs` | Tabbed navigation | | `Collapse` | Collapsible accordion sections | | `Menu` | Navigation menu | +| `Affix` | Pin content to fixed position when scrolling | +| `Anchor` | Anchor navigation for page sections | +| `Breadcrumb` | Breadcrumb navigation path | +| `BackTop` | Back to top button | ### Overlay @@ -140,7 +152,7 @@ const { registry } = defineRegistry(catalog, { | `Tooltip` | Hover tooltip | | `Dropdown` | Dropdown menu | -### Content +### Data Display | Component | Description | |-----------|-------------| @@ -161,8 +173,12 @@ const { registry } = defineRegistry(catalog, { | `Descriptions` | Description list | | `Timeline` | Timeline display | | `Carousel` | Horizontally scrollable carousel | +| `Calendar` | Calendar for date display/selection | +| `List` | List component with pagination and grid | +| `Tree` | Tree structure display and selection | +| `QRCode` | QRCode generator | -### Input +### Data Entry | Component | Description | |-----------|-------------| @@ -180,6 +196,11 @@ const { registry } = defineRegistry(catalog, { | `TimePicker` | Time picker | | `Upload` | File upload | | `Transfer` | Transfer shuttle | +| `AutoComplete` | Input with suggestions | +| `Cascader` | Cascader selection for hierarchical data | +| `ColorPicker` | Color picker | +| `Mentions` | Mentions input for @-tagging | +| `TreeSelect` | Tree select dropdown | ### Action diff --git a/packages/antd/package.json b/packages/antd/package.json index 2b6fef28..3093ed6a 100644 --- a/packages/antd/package.json +++ b/packages/antd/package.json @@ -61,6 +61,7 @@ "@types/react": "19.2.3", "antd": "^6.3.1", "tsup": "^8.0.2", + "dayjs": "^1.11.11", "typescript": "^5.4.5", "zod": "^4.3.6" }, diff --git a/packages/antd/src/catalog.ts b/packages/antd/src/catalog.ts index 20e5ff3c..82013c41 100644 --- a/packages/antd/src/catalog.ts +++ b/packages/antd/src/catalog.ts @@ -17,48 +17,126 @@ const validationCheckSchema = z const validateOnSchema = z.enum(["change", "blur", "submit"]).nullable(); // ============================================================================= -// Ant Design Component Definitions +// Ant Design v6 Component Definitions // ============================================================================= /** - * Ant Design component definitions for json-render catalogs. + * Ant Design v6 component definitions for json-render catalogs. * * These can be used directly or extended with custom components. - * All components are built using Ant Design components. + * All components are built using Ant Design v6 components. + * + * Note: Component APIs follow Antd v6 specifications: + * - Use `variant` instead of deprecated `bordered` where applicable + * - Use `orientation` instead of deprecated `direction` where applicable + * - Use `titlePlacement` for Divider text alignment + * - Use `expandIconPlacement` instead of deprecated `expandIconPosition` */ export const antdComponentDefinitions = { // ========================================================================== // Layout Components // ========================================================================== + Layout: { + props: z.object({ + hasSider: z.boolean().nullable(), + }), + slots: ["default"], + description: + "Antd layout container. Compose with LayoutHeader / LayoutSider / LayoutContent / LayoutFooter inside.", + }, + + LayoutHeader: { + props: z.object({}), + slots: ["default"], + description: "Antd layout header. Use inside Layout only.", + }, + + LayoutContent: { + props: z.object({}), + slots: ["default"], + description: "Antd layout main content area. Use inside Layout only.", + }, + + LayoutFooter: { + props: z.object({}), + slots: ["default"], + description: "Antd layout footer. Use inside Layout only.", + }, + + LayoutSider: { + props: z.object({ + width: z.union([z.number(), z.string()]).nullable(), + collapsible: z.boolean().nullable(), + collapsed: z.boolean().nullable(), + defaultCollapsed: z.boolean().nullable(), + collapsedWidth: z.union([z.number(), z.string()]).nullable(), + reverseArrow: z.boolean().nullable(), + breakpoint: z.enum(["xs", "sm", "md", "lg", "xl", "xxl"]).nullable(), + theme: z.enum(["light", "dark"]).nullable(), + }), + slots: ["default"], + events: ["collapse"], + description: "Antd layout sidebar. Use inside Layout only.", + }, + Card: { props: z.object({ title: z.string().nullable(), - description: z.string().nullable(), - bordered: z.boolean().nullable(), + extra: z.string().nullable(), + variant: z.enum(["outlined", "borderless"]).nullable(), hoverable: z.boolean().nullable(), loading: z.boolean().nullable(), size: z.enum(["default", "small"]).nullable(), + cover: z.string().nullable(), + actions: z.array(z.string()).nullable(), + }), + slots: ["default", "extra", "cover", "actions"], + description: + "Container card for content sections. Use slots.default for card body, slots.extra for header extra content, slots.cover for cover image, slots.actions for action buttons.", + example: { title: "Overview", variant: "outlined" }, + }, + + Flex: { + props: z.object({ + vertical: z.boolean().nullable(), + wrap: z.boolean().nullable(), + justify: z.string().nullable(), + align: z.string().nullable(), + gap: z.union([z.string(), z.number()]).nullable(), + flex: z.string().nullable(), }), slots: ["default"], description: - "Container card for content sections. Use for forms/content boxes.", - example: { title: "Overview", description: "Your account summary" }, + "Flex layout container. justify/align accept CSS values (e.g. 'center', 'space-between'). gap accepts 'small'/'middle'/'large' or a number.", + example: { vertical: true, gap: "middle" }, }, Stack: { props: z.object({ - direction: z.enum(["horizontal", "vertical"]).nullable(), - gap: z.enum(["none", "sm", "md", "lg"]).nullable(), - align: z.enum(["start", "center", "end", "stretch"]).nullable(), + direction: z.enum(["vertical", "horizontal"]).nullable(), + wrap: z.boolean().nullable(), justify: z - .enum(["start", "center", "end", "between", "around"]) + .enum([ + "start", + "end", + "center", + "space-around", + "space-between", + "space-evenly", + ]) + .nullable(), + align: z + .enum(["start", "center", "end", "baseline", "stretch"]) + .nullable(), + gap: z + .union([z.enum(["small", "middle", "large"]), z.number()]) .nullable(), - wrap: z.boolean().nullable(), }), slots: ["default"], - description: "Flex container for layouts", - example: { direction: "vertical", gap: "md" }, + description: + "Stack layout container based on Flex. Defaults to vertical direction.", + example: { direction: "vertical", gap: "middle" }, }, Grid: { @@ -67,14 +145,75 @@ export const antdComponentDefinitions = { gap: z.enum(["sm", "md", "lg"]).nullable(), }), slots: ["default"], - description: "Grid layout (1-6 columns)", + description: "Grid layout (1-6 columns). Antd v6.", example: { columns: 3, gap: "md" }, }, + Row: { + props: z.object({ + gutter: z + .union([z.number(), z.tuple([z.number(), z.number()])]) + .nullable(), + align: z.enum(["top", "middle", "bottom", "stretch"]).nullable(), + justify: z + .enum([ + "start", + "end", + "center", + "space-around", + "space-between", + "space-evenly", + ]) + .nullable(), + wrap: z.boolean().nullable(), + }), + slots: ["default"], + description: + "Antd grid row. Use Col children inside. gutter: horizontal spacing (or [horizontal, vertical]).", + example: { gutter: 16 }, + }, + + Col: { + props: z.object({ + span: z.number().nullable(), + offset: z.number().nullable(), + order: z.number().nullable(), + push: z.number().nullable(), + pull: z.number().nullable(), + flex: z.union([z.number(), z.string()]).nullable(), + }), + slots: ["default"], + description: + "Antd grid column (span 1-24). Use inside Row. offset shifts the col right.", + example: { span: 12 }, + }, + + Masonry: { + props: z.object({ + columns: z + .union([z.number(), z.record(z.string(), z.number())]) + .nullable(), + gutter: z + .union([z.number(), z.tuple([z.number(), z.number()])]) + .nullable(), + }), + slots: ["default"], + description: + "Masonry layout. Children are automatically distributed across columns. columns can be number or responsive object like { xs: 1, sm: 2, md: 3 }.", + example: { columns: 3, gutter: [16, 16] }, + }, + Divider: { props: z.object({ orientation: z.enum(["horizontal", "vertical"]).nullable(), + vertical: z.boolean().nullable(), + titlePlacement: z + .enum(["left", "center", "right", "start", "end"]) + .nullable(), dashed: z.boolean().nullable(), + variant: z.enum(["solid", "dashed", "dotted"]).nullable(), + plain: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), text: z.string().nullable(), }), description: "A divider line that separates content.", @@ -82,7 +221,7 @@ export const antdComponentDefinitions = { Space: { props: z.object({ - direction: z.enum(["horizontal", "vertical"]).nullable(), + orientation: z.enum(["horizontal", "vertical"]).nullable(), size: z.enum(["small", "middle", "large"]).nullable(), wrap: z.boolean().nullable(), align: z.enum(["start", "center", "end", "baseline"]).nullable(), @@ -95,6 +234,59 @@ export const antdComponentDefinitions = { // Navigation Components // ========================================================================== + Affix: { + props: z.object({ + offsetBottom: z.number().nullable(), + offsetTop: z.number().nullable(), + target: z.string().nullable(), + }), + slots: ["default"], + description: + "Affix component. Pins children to a fixed position when scrolling.", + }, + + Anchor: { + props: z.object({ + items: z.array( + z.object({ + key: z.string(), + title: z.string(), + href: z.string().nullable(), + }), + ), + affix: z.boolean().nullable(), + bounds: z.number().nullable(), + offsetTop: z.number().nullable(), + targetOffset: z.number().nullable(), + }), + events: ["change", "click"], + description: "Anchor navigation for page sections.", + }, + + Breadcrumb: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + href: z.string().nullable(), + }), + ), + separator: z.string().nullable(), + }), + description: "Breadcrumb navigation path.", + }, + + BackTop: { + props: z.object({ + visibilityHeight: z.number().nullable(), + target: z.string().nullable(), + duration: z.number().nullable(), + }), + slots: ["default"], + description: + "Back to top button. Deprecated in antd v6, use FloatButton.BackTop instead.", + }, + Tabs: { props: z.object({ tabs: z.array( @@ -107,11 +299,15 @@ export const antdComponentDefinitions = { value: z.string().nullable(), position: z.enum(["top", "bottom", "left", "right"]).nullable(), type: z.enum(["line", "card"]).nullable(), + centered: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + tabBarGutter: z.number().nullable(), + destroyInactiveTabPane: z.boolean().nullable(), }), - slots: ["default"], + slots: ["tabs"], events: ["change"], description: - "Tab navigation. Use { $bindState } on value for active tab binding.", + "Tab navigation. Use slots.tabs for tab content items. Use { $bindState } on value for active tab binding.", }, Collapse: { @@ -119,13 +315,20 @@ export const antdComponentDefinitions = { items: z.array( z.object({ title: z.string(), - content: z.string(), }), ), accordion: z.boolean().nullable(), + bordered: z.boolean().nullable(), + ghost: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + expandIconPlacement: z.enum(["start", "end"]).nullable(), + collapsible: z.enum(["header", "icon", "disabled"]).nullable(), + defaultActiveKey: z.union([z.string(), z.array(z.string())]).nullable(), + activeKey: z.union([z.string(), z.array(z.string())]).nullable(), }), + slots: ["items"], description: - "Collapsible sections. Items as [{title, content}]. Set accordion to true for single panel open.", + "Collapsible sections. Each item has a title. Use slots.items for panel content.", }, Menu: { @@ -139,6 +342,10 @@ export const antdComponentDefinitions = { ), mode: z.enum(["horizontal", "vertical", "inline"]).nullable(), selectedKey: z.string().nullable(), + theme: z.enum(["light", "dark"]).nullable(), + defaultSelectedKeys: z.array(z.string()).nullable(), + inlineCollapsed: z.boolean().nullable(), + multiple: z.boolean().nullable(), }), events: ["select"], description: "Navigation menu with items.", @@ -155,6 +362,15 @@ export const antdComponentDefinitions = { openPath: z.string(), width: z.number().nullable(), footer: z.boolean().nullable(), + centered: z.boolean().nullable(), + closable: z.boolean().nullable(), + maskClosable: z.boolean().nullable(), + okText: z.string().nullable(), + cancelText: z.string().nullable(), + confirmLoading: z.boolean().nullable(), + destroyOnClose: z.boolean().nullable(), + keyboard: z.boolean().nullable(), + loading: z.boolean().nullable(), }), slots: ["default"], events: ["ok", "cancel"], @@ -169,6 +385,13 @@ export const antdComponentDefinitions = { openPath: z.string(), placement: z.enum(["top", "bottom", "left", "right"]).nullable(), width: z.union([z.number(), z.string()]).nullable(), + height: z.union([z.number(), z.string()]).nullable(), + closable: z.boolean().nullable(), + maskClosable: z.boolean().nullable(), + destroyOnClose: z.boolean().nullable(), + keyboard: z.boolean().nullable(), + loading: z.boolean().nullable(), + size: z.enum(["default", "large"]).nullable(), }), slots: ["default"], events: ["close"], @@ -178,8 +401,30 @@ export const antdComponentDefinitions = { Popover: { props: z.object({ trigger: z.string(), + title: z.string().nullable(), content: z.string(), - placement: z.enum(["top", "bottom", "left", "right"]).nullable(), + placement: z + .enum([ + "top", + "topLeft", + "topRight", + "bottom", + "bottomLeft", + "bottomRight", + "left", + "leftTop", + "leftBottom", + "right", + "rightTop", + "rightBottom", + ]) + .nullable(), + triggerType: z + .enum(["hover", "focus", "click", "contextMenu"]) + .nullable(), + arrow: z.boolean().nullable(), + open: z.boolean().nullable(), + defaultOpen: z.boolean().nullable(), }), description: "Popover that appears on click of trigger.", }, @@ -188,25 +433,64 @@ export const antdComponentDefinitions = { props: z.object({ content: z.string(), text: z.string(), - placement: z.enum(["top", "bottom", "left", "right"]).nullable(), + placement: z + .enum([ + "top", + "topLeft", + "topRight", + "bottom", + "bottomLeft", + "bottomRight", + "left", + "leftTop", + "leftBottom", + "right", + "rightTop", + "rightBottom", + ]) + .nullable(), + triggerType: z + .enum(["hover", "focus", "click", "contextMenu"]) + .nullable(), + arrow: z.boolean().nullable(), + color: z.string().nullable(), + open: z.boolean().nullable(), + defaultOpen: z.boolean().nullable(), }), description: "Hover tooltip. Shows content on hover over text.", }, Dropdown: { props: z.object({ - label: z.string(), items: z.array( z.object({ label: z.string(), key: z.string(), icon: z.string().nullable(), danger: z.boolean().nullable(), + disabled: z.boolean().nullable(), + divider: z.boolean().nullable(), }), ), + trigger: z.enum(["hover", "click", "contextMenu"]).nullable(), + placement: z + .enum([ + "topLeft", + "topCenter", + "topRight", + "bottomLeft", + "bottomCenter", + "bottomRight", + ]) + .nullable(), + arrow: z.boolean().nullable(), + disabled: z.boolean().nullable(), + open: z.boolean().nullable(), + defaultOpen: z.boolean().nullable(), }), - events: ["select"], - description: "Dropdown menu with trigger button and selectable items.", + slots: ["default"], + events: ["select", "openChange", "visibleChange"], + description: "Dropdown menu. Use children as trigger element.", }, // ========================================================================== @@ -221,6 +505,29 @@ export const antdComponentDefinitions = { bordered: z.boolean().nullable(), size: z.enum(["small", "middle", "large"]).nullable(), loading: z.boolean().nullable(), + pagination: z + .union([ + z.boolean(), + z.object({ + pageSize: z.number().nullable(), + current: z.number().nullable(), + total: z.number().nullable(), + showSizeChanger: z.boolean().nullable(), + showQuickJumper: z.boolean().nullable(), + simple: z.boolean().nullable(), + hideOnSinglePage: z.boolean().nullable(), + }), + ]) + .nullable(), + scroll: z + .object({ + x: z.union([z.number(), z.string()]).nullable(), + y: z.union([z.number(), z.string()]).nullable(), + }) + .nullable(), + showHeader: z.boolean().nullable(), + rowKey: z.string().nullable(), + sticky: z.boolean().nullable(), }), description: "Data table. columns: header labels. rows: 2D array of cell strings.", @@ -269,11 +576,12 @@ export const antdComponentDefinitions = { Image: { props: z.object({ - src: z.string().nullable(), + src: z.string(), alt: z.string(), width: z.union([z.number(), z.string()]).nullable(), height: z.union([z.number(), z.string()]).nullable(), preview: z.boolean().nullable(), + fallback: z.string().nullable(), }), description: "Image component with preview support.", }, @@ -286,6 +594,9 @@ export const antdComponentDefinitions = { .union([z.number(), z.enum(["small", "default", "large"])]) .nullable(), shape: z.enum(["circle", "square"]).nullable(), + icon: z.string().nullable(), + alt: z.string().nullable(), + gap: z.number().nullable(), }), description: "User avatar with fallback initials", example: { name: "Jane Doe", size: "default" }, @@ -293,15 +604,26 @@ export const antdComponentDefinitions = { Badge: { props: z.object({ - text: z.string().nullable(), - count: z.number().nullable(), + count: z.union([z.number(), z.string()]).nullable(), + dot: z.boolean().nullable(), color: z.string().nullable(), status: z .enum(["success", "processing", "default", "error", "warning"]) .nullable(), + text: z.string().nullable(), + size: z.enum(["default", "small"]).nullable(), + overflowCount: z.number().nullable(), + showZero: z.boolean().nullable(), + title: z.string().nullable(), + offset: z + .tuple([ + z.union([z.number(), z.string()]), + z.union([z.number(), z.string()]), + ]) + .nullable(), }), slots: ["default"], - description: "Status badge or count indicator", + description: "Badge component for status or count display", example: { count: 5 }, }, @@ -310,6 +632,8 @@ export const antdComponentDefinitions = { text: z.string(), color: z.string().nullable(), closable: z.boolean().nullable(), + bordered: z.boolean().nullable(), + icon: z.string().nullable(), }), events: ["close"], description: "Tag for categorizing or marking.", @@ -319,16 +643,17 @@ export const antdComponentDefinitions = { Alert: { props: z.object({ title: z.string(), - message: z.string().nullable(), + description: z.string().nullable(), type: z.enum(["info", "success", "warning", "error"]).nullable(), closable: z.boolean().nullable(), showIcon: z.boolean().nullable(), + banner: z.boolean().nullable(), }), events: ["close"], description: "Alert banner", example: { title: "Note", - message: "Your changes have been saved.", + description: "Your changes have been saved.", type: "success", }, }, @@ -340,6 +665,10 @@ export const antdComponentDefinitions = { label: z.string().nullable(), status: z.enum(["success", "exception", "normal", "active"]).nullable(), type: z.enum(["line", "circle", "dashboard"]).nullable(), + showInfo: z.boolean().nullable(), + strokeColor: z.string().nullable(), + size: z.union([z.enum(["small", "default"]), z.number()]).nullable(), + steps: z.number().nullable(), }), description: "Progress bar (value 0-100)", example: { value: 65, max: 100, label: "Upload progress" }, @@ -350,6 +679,9 @@ export const antdComponentDefinitions = { loading: z.boolean().nullable(), active: z.boolean().nullable(), rows: z.number().nullable(), + avatar: z.boolean().nullable(), + title: z.boolean().nullable(), + round: z.boolean().nullable(), }), slots: ["default"], description: "Loading placeholder skeleton", @@ -360,6 +692,8 @@ export const antdComponentDefinitions = { size: z.enum(["small", "default", "large"]).nullable(), label: z.string().nullable(), spinning: z.boolean().nullable(), + delay: z.number().nullable(), + fullscreen: z.boolean().nullable(), }), slots: ["default"], description: "Loading spinner indicator", @@ -378,6 +712,10 @@ export const antdComponentDefinitions = { value: z.union([z.number(), z.string()]), prefix: z.string().nullable(), suffix: z.string().nullable(), + precision: z.number().nullable(), + loading: z.boolean().nullable(), + groupSeparator: z.string().nullable(), + decimalSeparator: z.string().nullable(), }), description: "Display statistic value with title", }, @@ -389,10 +727,14 @@ export const antdComponentDefinitions = { z.object({ label: z.string(), value: z.string(), + span: z.number().nullable(), }), ), bordered: z.boolean().nullable(), column: z.number().nullable(), + colon: z.boolean().nullable(), + layout: z.enum(["horizontal", "vertical"]).nullable(), + size: z.enum(["default", "middle", "small"]).nullable(), }), description: "Display read-only data in key-value pairs", }, @@ -401,26 +743,102 @@ export const antdComponentDefinitions = { props: z.object({ items: z.array( z.object({ - content: z.string(), color: z.string().nullable(), }), ), + mode: z.enum(["left", "alternate", "right"]).nullable(), + reverse: z.boolean().nullable(), }), - description: "Vertical timeline display", + slots: ["items"], + description: + "Vertical timeline display. Use slots.items for each node's content.", }, Carousel: { props: z.object({ - items: z.array( + autoplay: z.boolean().nullable(), + dots: z.boolean().nullable(), + effect: z.enum(["scrollx", "fade"]).nullable(), + autoplaySpeed: z.number().nullable(), + speed: z.number().nullable(), + infinite: z.boolean().nullable(), + arrows: z.boolean().nullable(), + dotPosition: z.enum(["top", "bottom", "left", "right"]).nullable(), + }), + slots: ["default"], + description: + "Horizontally scrollable carousel. Use slots for slide content.", + }, + + Calendar: { + props: z.object({ + value: z.string().nullable(), + mode: z.enum(["month", "year"]).nullable(), + fullscreen: z.boolean().nullable(), + }), + events: ["change", "select"], + description: "Calendar component for date display and selection.", + }, + + List: { + props: z.object({ + dataSource: z.array(z.any()).nullable(), + bordered: z.boolean().nullable(), + loading: z.boolean().nullable(), + size: z.enum(["small", "default", "large"]).nullable(), + split: z.boolean().nullable(), + grid: z + .object({ + gutter: z.number().nullable(), + column: z.number().nullable(), + }) + .nullable(), + pagination: z + .union([ + z.boolean(), + z.object({ + pageSize: z.number().nullable(), + total: z.number().nullable(), + }), + ]) + .nullable(), + }), + slots: ["default"], + events: ["change"], + description: "List component. Use slots.default for list items.", + }, + + Tree: { + props: z.object({ + treeData: z.array( z.object({ - title: z.string().nullable(), - description: z.string().nullable(), + key: z.string(), + title: z.string(), + children: z.array(z.any()).nullable(), }), ), - autoplay: z.boolean().nullable(), - dots: z.boolean().nullable(), + checkable: z.boolean().nullable(), + checkedKeys: z.array(z.string()).nullable(), + expandedKeys: z.array(z.string()).nullable(), + selectedKeys: z.array(z.string()).nullable(), + defaultExpandAll: z.boolean().nullable(), + showLine: z.boolean().nullable(), + multiple: z.boolean().nullable(), }), - description: "Horizontally scrollable carousel.", + events: ["check", "expand", "select"], + description: "Tree structure display and selection.", + }, + + QRCode: { + props: z.object({ + value: z.string(), + size: z.number().nullable(), + color: z.string().nullable(), + bgColor: z.string().nullable(), + bordered: z.boolean().nullable(), + status: z.enum(["active", "expired", "loading"]).nullable(), + }), + description: "QRCode generator component.", }, // ========================================================================== @@ -439,6 +857,12 @@ export const antdComponentDefinitions = { allowClear: z.boolean().nullable(), showCount: z.boolean().nullable(), maxLength: z.number().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + variant: z.enum(["outlined", "borderless", "filled"]).nullable(), + readOnly: z.boolean().nullable(), + addonBefore: z.string().nullable(), + addonAfter: z.string().nullable(), + disabled: z.boolean().nullable(), checks: validationCheckSchema, validateOn: validateOnSchema, status: z.enum(["error", "warning"]).nullable(), @@ -464,12 +888,16 @@ export const antdComponentDefinitions = { allowClear: z.boolean().nullable(), showCount: z.boolean().nullable(), maxLength: z.number().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + variant: z.enum(["outlined", "borderless", "filled"]).nullable(), + readOnly: z.boolean().nullable(), autoSize: z .union([ z.boolean(), z.object({ minRows: z.number(), maxRows: z.number() }), ]) .nullable(), + disabled: z.boolean().nullable(), checks: validationCheckSchema, validateOn: validateOnSchema, }), @@ -489,6 +917,11 @@ export const antdComponentDefinitions = { precision: z.number().nullable(), prefix: z.string().nullable(), suffix: z.string().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + variant: z.enum(["outlined", "borderless", "filled"]).nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, }), events: ["change"], description: "Number input with controls.", @@ -509,6 +942,9 @@ export const antdComponentDefinitions = { mode: z.enum(["multiple", "tags"]).nullable(), allowClear: z.boolean().nullable(), showSearch: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + variant: z.enum(["outlined", "borderless", "filled"]).nullable(), + disabled: z.boolean().nullable(), checks: validationCheckSchema, validateOn: validateOnSchema, }), @@ -523,6 +959,7 @@ export const antdComponentDefinitions = { name: z.string(), checked: z.boolean().nullable(), indeterminate: z.boolean().nullable(), + disabled: z.boolean().nullable(), checks: validationCheckSchema, validateOn: validateOnSchema, }), @@ -541,6 +978,9 @@ export const antdComponentDefinitions = { ]), ), value: z.array(z.string()).nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, }), events: ["change"], description: "Group of checkboxes.", @@ -558,6 +998,7 @@ export const antdComponentDefinitions = { ), value: z.string().nullable(), optionType: z.enum(["default", "button"]).nullable(), + disabled: z.boolean().nullable(), checks: validationCheckSchema, validateOn: validateOnSchema, }), @@ -572,6 +1013,7 @@ export const antdComponentDefinitions = { checked: z.boolean().nullable(), checkedChildren: z.string().nullable(), unCheckedChildren: z.string().nullable(), + disabled: z.boolean().nullable(), checks: validationCheckSchema, validateOn: validateOnSchema, }), @@ -582,11 +1024,13 @@ export const antdComponentDefinitions = { Slider: { props: z.object({ label: z.string().nullable(), + name: z.string().nullable(), min: z.number().nullable(), max: z.number().nullable(), step: z.number().nullable(), value: z.union([z.number(), z.array(z.number())]).nullable(), range: z.boolean().nullable(), + disabled: z.boolean().nullable(), marks: z .record( z.string(), @@ -604,10 +1048,12 @@ export const antdComponentDefinitions = { Rate: { props: z.object({ label: z.string().nullable(), + name: z.string().nullable(), count: z.number().nullable(), value: z.number().nullable(), allowHalf: z.boolean().nullable(), allowClear: z.boolean().nullable(), + disabled: z.boolean().nullable(), }), events: ["change"], description: "Star rating component.", @@ -622,6 +1068,7 @@ export const antdComponentDefinitions = { format: z.string().nullable(), picker: z.enum(["date", "week", "month", "quarter", "year"]).nullable(), showTime: z.boolean().nullable(), + disabled: z.boolean().nullable(), }), events: ["change"], description: "Date picker input.", @@ -634,6 +1081,7 @@ export const antdComponentDefinitions = { placeholder: z.string().nullable(), value: z.string().nullable(), format: z.string().nullable(), + disabled: z.boolean().nullable(), }), events: ["change"], description: "Time picker input.", @@ -648,6 +1096,7 @@ export const antdComponentDefinitions = { maxCount: z.number().nullable(), listType: z.enum(["text", "picture", "picture-card"]).nullable(), buttonText: z.string().nullable(), + disabled: z.boolean().nullable(), }), events: ["change"], description: "File upload component.", @@ -665,11 +1114,117 @@ export const antdComponentDefinitions = { ), targetKeys: z.array(z.string()).nullable(), titles: z.array(z.string()).nullable(), + disabled: z.boolean().nullable(), }), events: ["change"], description: "Transfer items between two columns.", }, + AutoComplete: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.union([ + z.string(), + z.object({ label: z.string(), value: z.string() }), + ]), + ), + placeholder: z.string().nullable(), + value: z.string().nullable(), + allowClear: z.boolean().nullable(), + disabled: z.boolean().nullable(), + status: z.enum(["error", "warning"]).nullable(), + }), + events: ["change", "select"], + description: "AutoComplete input with suggestions.", + }, + + Cascader: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.object({ + label: z.string(), + value: z.string(), + children: z.array(z.any()).nullable(), + }), + ), + placeholder: z.string().nullable(), + value: z.array(z.string()).nullable(), + allowClear: z.boolean().nullable(), + showSearch: z.boolean().nullable(), + disabled: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + }), + events: ["change"], + description: "Cascader selection for hierarchical data.", + }, + + ColorPicker: { + props: z.object({ + label: z.string(), + name: z.string(), + value: z.string().nullable(), + showText: z.boolean().nullable(), + disabled: z.boolean().nullable(), + allowClear: z.boolean().nullable(), + format: z.enum(["hex", "rgb", "hsl"]).nullable(), + }), + events: ["change"], + description: "Color picker component.", + }, + + Mentions: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.object({ + value: z.string(), + label: z.string(), + }), + ), + placeholder: z.string().nullable(), + value: z.string().nullable(), + autoSize: z + .union([ + z.boolean(), + z.object({ minRows: z.number(), maxRows: z.number() }), + ]) + .nullable(), + disabled: z.boolean().nullable(), + }), + events: ["change"], + description: "Mentions input for @-tagging.", + }, + + TreeSelect: { + props: z.object({ + label: z.string(), + name: z.string(), + treeData: z.array( + z.object({ + key: z.string(), + title: z.string(), + value: z.string(), + children: z.array(z.any()).nullable(), + }), + ), + placeholder: z.string().nullable(), + value: z.string().nullable(), + allowClear: z.boolean().nullable(), + showSearch: z.boolean().nullable(), + multiple: z.boolean().nullable(), + disabled: z.boolean().nullable(), + treeCheckable: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + }), + events: ["change"], + description: "Tree select dropdown component.", + }, + // ========================================================================== // Action Components // ========================================================================== @@ -684,6 +1239,11 @@ export const antdComponentDefinitions = { icon: z.string().nullable(), block: z.boolean().nullable(), size: z.enum(["small", "middle", "large"]).nullable(), + ghost: z.boolean().nullable(), + shape: z.enum(["default", "circle", "round"]).nullable(), + href: z.string().nullable(), + target: z.enum(["_blank", "_self", "_parent", "_top"]).nullable(), + htmlType: z.enum(["button", "submit", "reset"]).nullable(), }), events: ["press"], description: "Clickable button. Bind on.press for handler.", @@ -727,6 +1287,11 @@ export const antdComponentDefinitions = { showSizeChanger: z.boolean().nullable(), showQuickJumper: z.boolean().nullable(), simple: z.boolean().nullable(), + disabled: z.boolean().nullable(), + size: z.enum(["default", "small"]).nullable(), + hideOnSinglePage: z.boolean().nullable(), + pageSizeOptions: z.array(z.number()).nullable(), + align: z.enum(["start", "center", "end"]).nullable(), }), events: ["change"], description: @@ -747,6 +1312,8 @@ export const antdComponentDefinitions = { ), value: z.string().nullable(), block: z.boolean().nullable(), + disabled: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), }), events: ["change"], description: "Segmented control for toggling between options.", @@ -758,12 +1325,20 @@ export const antdComponentDefinitions = { z.object({ title: z.string(), description: z.string().nullable(), + subTitle: z.string().nullable(), icon: z.string().nullable(), + disabled: z.boolean().nullable(), + status: z.enum(["wait", "process", "finish", "error"]).nullable(), }), ), current: z.number().nullable(), direction: z.enum(["horizontal", "vertical"]).nullable(), status: z.enum(["wait", "process", "finish", "error"]).nullable(), + size: z.enum(["default", "small"]).nullable(), + type: z.enum(["default", "navigation", "inline"]).nullable(), + initial: z.number().nullable(), + labelPlacement: z.enum(["horizontal", "vertical"]).nullable(), + percent: z.number().nullable(), }), events: ["change"], description: "Step navigation bar.", diff --git a/packages/antd/src/components.tsx b/packages/antd/src/components.tsx index cfc8f107..8c33acbc 100644 --- a/packages/antd/src/components.tsx +++ b/packages/antd/src/components.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { Children, useState } from "react"; import dayjs from "dayjs"; import { useBoundProp, @@ -8,11 +8,21 @@ import { useFieldValidation, type BaseComponentProps, } from "@json-render/react"; +import type { ReactNode } from "react"; + +/** BaseComponentProps extended with slots for components that use slotted content. */ +type SlottedComponentProps

= BaseComponentProps

& { + slots?: Record; +}; import { Card, Space, Divider, + Row, + Col, + Masonry, + Layout, Tabs, Collapse, Menu, @@ -36,6 +46,10 @@ import { Descriptions, Timeline, Carousel, + Calendar, + List, + Tree, + QRCode, Input, InputNumber, Select, @@ -48,6 +62,11 @@ import { TimePicker, Upload, Transfer, + AutoComplete, + Cascader, + ColorPicker, + Mentions, + TreeSelect, Button, Pagination, Segmented, @@ -55,6 +74,10 @@ import { Result, Flex, Form, + Affix, + Anchor, + Breadcrumb, + BackTop, } from "antd"; import type { UploadFile } from "antd/es/upload/interface"; import type { AntdProps } from "./catalog"; @@ -92,58 +115,111 @@ const { export const antdComponents = { // ── Layout ──────────────────────────────────────────────────────────── - Card: ({ props, children }: BaseComponentProps>) => { + Layout: ({ props, children }: BaseComponentProps>) => { + return {children}; + }, + + LayoutHeader: ({ + children, + }: BaseComponentProps>) => { + return {children}; + }, + + LayoutContent: ({ + children, + }: BaseComponentProps>) => { + return {children}; + }, + + LayoutFooter: ({ + children, + }: BaseComponentProps>) => { + return {children}; + }, + + LayoutSider: ({ + props, + children, + emit, + }: BaseComponentProps>) => { + return ( + emit("collapse")} + > + {children} + + ); + }, + + Card: ({ + props, + children, + slots, + }: SlottedComponentProps>) => { + const extraSlot = slots?.extra?.[0]; + const coverSlot = slots?.cover?.[0]; + const actionsSlots = slots?.actions ?? []; + return ( {props.extra} : undefined) + } + variant={props.variant ?? "outlined"} hoverable={props.hoverable ?? false} loading={props.loading ?? false} size={props.size ?? "default"} + cover={ + coverSlot ?? + (props.cover ? cover : undefined) + } + actions={ + actionsSlots.length > 0 + ? actionsSlots + : props.actions + ? props.actions.map((action, idx) => ( + {action} + )) + : undefined + } > - {props.description && ( - {props.description} - )} {children} ); }, - Stack: ({ props, children }: BaseComponentProps>) => { - const isHorizontal = props.direction === "horizontal"; - const gapMap: Record = { - none: 0, - sm: 8, - md: 16, - lg: 24, - }; - const alignMap: Record< - string, - "flex-start" | "center" | "flex-end" | "stretch" - > = { - start: "flex-start", - center: "center", - end: "flex-end", - stretch: "stretch", - }; - const justifyMap: Record< - string, - "flex-start" | "center" | "flex-end" | "space-between" | "space-around" - > = { - start: "flex-start", - center: "center", - end: "flex-end", - between: "space-between", - around: "space-around", - }; + Flex: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + Stack: ({ props, children }: BaseComponentProps>) => { return ( {children} @@ -151,31 +227,82 @@ export const antdComponents = { }, Grid: ({ props, children }: BaseComponentProps>) => { - const n = Math.max(1, Math.min(12, props.columns ?? 3)); - const gapMap: Record = { - sm: 8, - md: 16, - lg: 24, - }; + const columns = props.columns ?? 3; + const span = Math.floor(24 / Math.min(Math.max(columns, 1), 6)); + const gapMap = { sm: 8, md: 16, lg: 24 } as const; + const gutter = props.gap ? gapMap[props.gap] : 16; + const childArray = Children.toArray(children); + return ( + + {childArray.map((child, i) => ( +

+ {child} + + ))} + + ); + }, + Row: ({ props, children }: BaseComponentProps>) => { return ( -
+ {children} + + ); + }, + + Col: ({ props, children }: BaseComponentProps>) => { + return ( +
{children} - + + ); + }, + + Masonry: ({ props, children }: BaseComponentProps>) => { + const childArray = Array.isArray(children) + ? children + : children + ? [children] + : []; + const items = childArray.map((child, index) => ({ + key: index, + children: child, + data: {}, + })); + + return ( + ); }, Divider: ({ props }: BaseComponentProps>) => { return ( {props.text} @@ -191,7 +318,7 @@ export const antdComponents = { return ( >) => { + }: SlottedComponentProps>) => { const tabs = props.tabs ?? []; + const tabContents = slots?.tabs ?? []; const [boundValue, setBoundValue] = useBoundProp( props.value as string | undefined, bindings?.value, @@ -221,10 +349,10 @@ export const antdComponents = { const value = isBound ? (boundValue ?? tabs[0]?.value ?? "") : localValue; const setValue = isBound ? setBoundValue : setLocalValue; - const items = tabs.map((tab) => ({ + const items = tabs.map((tab, index) => ({ key: tab.value, label: tab.label, - children: tab.value === value ? children : null, + children: tabContents[index] ?? null, })); return ( @@ -236,22 +364,40 @@ export const antdComponents = { }} tabPosition={props.position ?? "top"} type={props.type ?? "line"} + centered={props.centered ?? false} + size={props.size ?? undefined} + tabBarGutter={props.tabBarGutter ?? undefined} + destroyInactiveTabPane={props.destroyInactiveTabPane ?? false} items={items} /> ); }, - Collapse: ({ props }: BaseComponentProps>) => { + Collapse: ({ + props, + slots, + }: SlottedComponentProps>) => { const items = props.items ?? []; + const itemContents = slots?.items ?? []; const collapseItems = items.map((item, idx) => ({ key: String(idx), label: item.title, - children: {item.content}, + children: itemContents[idx] ?? null, })); return ( - + ); }, @@ -268,12 +414,67 @@ export const antdComponents = { emit("select")} /> ); }, + Affix: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Anchor: ({ props, emit }: BaseComponentProps>) => { + const items = (props.items ?? []).map((item) => ({ + key: item.key, + title: item.title, + href: item.href ?? `#${item.key}`, + })); + + return ( + emit("change")} + onClick={(e, link) => emit("click")} + /> + ); + }, + + Breadcrumb: ({ props }: BaseComponentProps>) => { + const items = (props.items ?? []).map((item) => ({ + title: item.title, + href: item.href ?? undefined, + })); + + return ; + }, + + BackTop: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + // ── Overlay ──────────────────────────────────────────────────────────── Modal: ({ @@ -297,6 +498,15 @@ export const antdComponents = { }} width={props.width ?? 520} footer={props.footer === false ? null : undefined} + centered={props.centered ?? false} + closable={props.closable ?? true} + maskClosable={props.maskClosable ?? true} + okText={props.okText ?? undefined} + cancelText={props.cancelText ?? undefined} + confirmLoading={props.confirmLoading ?? false} + destroyOnClose={props.destroyOnClose ?? false} + keyboard={props.keyboard ?? true} + loading={props.loading ?? false} > {props.description && {props.description}} {children} @@ -321,6 +531,13 @@ export const antdComponents = { }} placement={props.placement ?? "right"} width={props.width ?? 378} + height={props.height ?? undefined} + closable={props.closable ?? true} + maskClosable={props.maskClosable ?? true} + destroyOnClose={props.destroyOnClose ?? false} + keyboard={props.keyboard ?? true} + loading={props.loading ?? false} + size={props.size ?? undefined} > {props.description && {props.description}} {children} @@ -330,7 +547,15 @@ export const antdComponents = { Popover: ({ props }: BaseComponentProps>) => { return ( - + {props.trigger} ); @@ -338,30 +563,68 @@ export const antdComponents = { Tooltip: ({ props }: BaseComponentProps>) => { return ( - + {props.text} ); }, - Dropdown: ({ props, emit }: BaseComponentProps>) => { + Dropdown: ({ + props, + children, + emit, + bindings, + }: SlottedComponentProps>) => { const items = props.items ?? []; - const menuItems = items.map((item) => ({ - key: item.key, - label: item.label, - icon: item.icon ? : undefined, - danger: item.danger ?? false, - })); + const menuItems = items + .filter((item) => !item.divider) + .map((item) => ({ + key: item.key, + label: item.label, + icon: item.icon ? : undefined, + danger: item.danger ?? false, + disabled: item.disabled ?? false, + })); + + const [boundOpen, setBoundOpen] = useBoundProp( + props.open as boolean | undefined, + bindings?.open, + ); + const isControlled = + bindings?.open !== undefined || props.open !== undefined; + const [localOpen, setLocalOpen] = useState(props.defaultOpen ?? false); + const open = isControlled ? (boundOpen ?? false) : localOpen; + const setOpen = isControlled ? setBoundOpen : setLocalOpen; return ( emit("select"), + onClick: ({ key }) => { + emit("select"); + }, + }} + trigger={props.trigger ? [props.trigger] : undefined} + placement={props.placement ?? undefined} + arrow={props.arrow ?? false} + disabled={props.disabled ?? false} + open={open} + onOpenChange={(visible) => { + setOpen(visible); + emit("openChange"); + emit("visibleChange"); }} > - + {children} ); }, @@ -389,6 +652,21 @@ export const antdComponents = { key: `col${idx}`, })); + const paginationConfig = + props.pagination === false + ? false + : props.pagination === true || props.pagination == null + ? false + : { + pageSize: props.pagination.pageSize ?? undefined, + current: props.pagination.current ?? undefined, + total: props.pagination.total ?? undefined, + showSizeChanger: props.pagination.showSizeChanger ?? false, + showQuickJumper: props.pagination.showQuickJumper ?? false, + simple: props.pagination.simple ?? false, + hideOnSinglePage: props.pagination.hideOnSinglePage ?? false, + }; + return (
{props.caption} : undefined } - pagination={false} + pagination={paginationConfig} + scroll={ + props.scroll + ? { x: props.scroll.x ?? undefined, y: props.scroll.y ?? undefined } + : undefined + } + showHeader={props.showHeader ?? true} + rowKey={props.rowKey ?? "key"} + sticky={props.sticky ?? false} /> ); }, @@ -452,27 +738,15 @@ export const antdComponents = { }, Image: ({ props }: BaseComponentProps>) => { - if (props.src) { - return ( - - ); - } return ( -
- {props.alt || "img"} -
+ ); }, @@ -484,6 +758,9 @@ export const antdComponents = { src={props.src ?? undefined} size={props.size ?? "default"} shape={props.shape ?? "circle"} + icon={props.icon ? : undefined} + alt={props.alt ?? undefined} + gap={props.gap ?? undefined} > {!props.src && name.charAt(0).toUpperCase()} @@ -491,18 +768,33 @@ export const antdComponents = { }, Badge: ({ props, children }: BaseComponentProps>) => { - if (props.count !== undefined) { + const hasChildren = !!children; + + if (!hasChildren) { return ( - - {children} - + ); } + return ( + size={props.size ?? undefined} + overflowCount={props.overflowCount ?? 99} + showZero={props.showZero ?? false} + title={props.title ?? undefined} + offset={props.offset ?? undefined} + > + {children} + ); }, @@ -511,6 +803,8 @@ export const antdComponents = { : undefined} onClose={(e) => { e.preventDefault(); emit("close"); @@ -524,11 +818,12 @@ export const antdComponents = { Alert: ({ props, emit }: BaseComponentProps>) => { return ( emit("close")} /> ); @@ -544,6 +839,10 @@ export const antdComponents = { percent={percent} status={props.status ?? undefined} type={props.type ?? "line"} + showInfo={props.showInfo ?? true} + strokeColor={props.strokeColor ?? undefined} + size={props.size ?? undefined} + steps={props.steps ?? undefined} format={() => props.label ?? `${percent}%`} /> ); @@ -557,6 +856,9 @@ export const antdComponents = { {children} @@ -570,6 +872,8 @@ export const antdComponents = { size={props.size ?? "default"} tip={props.label ?? undefined} spinning={props.spinning ?? true} + delay={props.delay ?? undefined} + fullscreen={props.fullscreen ?? false} > {children} @@ -587,6 +891,10 @@ export const antdComponents = { value={props.value} prefix={props.prefix ?? undefined} suffix={props.suffix ?? undefined} + precision={props.precision ?? undefined} + loading={props.loading ?? false} + groupSeparator={props.groupSeparator ?? undefined} + decimalSeparator={props.decimalSeparator ?? undefined} /> ); }, @@ -599,45 +907,190 @@ export const antdComponents = { title={props.title ?? undefined} bordered={props.bordered ?? false} column={props.column ?? 1} + colon={props.colon ?? true} + layout={props.layout ?? undefined} + size={props.size ?? undefined} items={items.map((item) => ({ key: item.label, label: item.label, children: item.value, + span: item.span ?? undefined, }))} /> ); }, - Timeline: ({ props }: BaseComponentProps>) => { + Timeline: ({ + props, + slots, + }: SlottedComponentProps>) => { const items = props.items ?? []; + const itemContents = slots?.items ?? []; return ( ({ + mode={props.mode ?? undefined} + reverse={props.reverse ?? false} + items={items.map((item, idx) => ({ color: item.color ?? undefined, - children: item.content, + children: itemContents[idx] ?? null, }))} /> ); }, - Carousel: ({ props }: BaseComponentProps>) => { - const items = props.items ?? []; - + Carousel: ({ + props, + children, + }: BaseComponentProps>) => { return ( - - {items.map((item, idx) => ( -
- {item.title && {item.title}} - {item.description && ( - {item.description} - )} -
- ))} + + {children} ); }, + Calendar: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(null); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? null) : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + const dayjsValue = value ? dayjs(value) : undefined; + + return ( + { + setValue(date.format("YYYY-MM-DD")); + emit("change"); + }} + onSelect={(date) => { + setValue(date.format("YYYY-MM-DD")); + emit("select"); + }} + /> + ); + }, + + List: ({ props, children }: BaseComponentProps>) => { + const pagination = + props.pagination === false + ? false + : props.pagination === true + ? { pageSize: 10 } + : props.pagination + ? { + pageSize: props.pagination.pageSize ?? 10, + total: props.pagination.total ?? undefined, + } + : false; + + const grid = props.grid + ? { + gutter: props.grid.gutter ?? undefined, + column: props.grid.column ?? undefined, + } + : undefined; + + return ( + { + const childArray = Array.isArray(children) + ? children + : children + ? [children] + : []; + return {childArray[index] ?? null}; + }} + /> + ); + }, + + Tree: ({ props, bindings, emit }: BaseComponentProps>) => { + const [boundCheckedKeys, setBoundCheckedKeys] = useBoundProp( + props.checkedKeys as string[] | undefined, + bindings?.checkedKeys, + ); + const [localCheckedKeys, setLocalCheckedKeys] = useState([]); + const isBoundChecked = !!bindings?.checkedKeys; + const checkedKeys = isBoundChecked + ? (boundCheckedKeys ?? []) + : localCheckedKeys; + const setCheckedKeys = isBoundChecked + ? setBoundCheckedKeys + : setLocalCheckedKeys; + + // Transform treeData to handle null children -> undefined + const transformTreeData = (data: unknown[]): unknown[] => { + return data.map((item: any) => ({ + key: item.key, + title: item.title, + ...(item.children + ? { children: transformTreeData(item.children) } + : {}), + })); + }; + + return ( + { + setCheckedKeys(checked as string[]); + emit("check"); + }} + onExpand={(expandedKeys) => emit("expand")} + onSelect={(selectedKeys) => emit("select")} + /> + ); + }, + + QRCode: ({ props }: BaseComponentProps>) => { + return ( + + ); + }, + // ── Form Inputs ──────────────────────────────────────────────────────── Input: ({ @@ -679,6 +1132,12 @@ export const antdComponents = { allowClear={props.allowClear ?? false} showCount={props.showCount ?? false} maxLength={props.maxLength ?? undefined} + size={props.size ?? undefined} + variant={props.variant ?? undefined} + readOnly={props.readOnly ?? false} + addonBefore={props.addonBefore ?? undefined} + addonAfter={props.addonAfter ?? undefined} + disabled={props.disabled ?? false} status={props.status ?? (errors.length > 0 ? "error" : undefined)} onChange={(e) => { setValue(e.target.value); @@ -730,7 +1189,11 @@ export const antdComponents = { allowClear={props.allowClear ?? false} showCount={props.showCount ?? false} maxLength={props.maxLength ?? undefined} + size={props.size ?? undefined} + variant={props.variant ?? undefined} + readOnly={props.readOnly ?? false} autoSize={props.autoSize ?? undefined} + disabled={props.disabled ?? false} status={errors.length > 0 ? "error" : undefined} onChange={(e) => { setValue(e.target.value); @@ -758,8 +1221,19 @@ export const antdComponents = { const value = isBound ? (boundValue ?? null) : localValue; const setValue = isBound ? setBoundValue : setLocalValue; + const validateOn = props.validateOn ?? "change"; + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + return ( - + 0 ? "error" : undefined} + help={errors[0]} + > : undefined} addonAfter={ props.suffix ? : undefined } + status={errors.length > 0 ? "error" : undefined} onChange={(val) => { setValue(val as number); + if (hasValidation && validateOn === "change") validate(); emit("change"); }} /> @@ -819,6 +1298,9 @@ export const antdComponents = { mode={props.mode ?? undefined} allowClear={props.allowClear ?? false} showSearch={props.showSearch ?? false} + size={props.size ?? undefined} + variant={props.variant ?? undefined} + disabled={props.disabled ?? false} status={errors.length > 0 ? "error" : undefined} onChange={(val) => { setValue(val as string); @@ -859,6 +1341,7 @@ export const antdComponents = { { setChecked(e.target.checked); @@ -886,17 +1369,30 @@ export const antdComponents = { const value = isBound ? (boundValue ?? []) : localValue; const setValue = isBound ? setBoundValue : setLocalValue; + const validateOn = props.validateOn ?? "change"; + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + const options = (props.options ?? []).map((opt) => typeof opt === "string" ? { label: opt, value: opt } : opt, ); return ( - + 0 ? "error" : undefined} + help={errors[0]} + > { setValue(checkedValues as string[]); + if (hasValidation && validateOn === "change") validate(); emit("change"); }} /> @@ -938,6 +1434,7 @@ export const antdComponents = { { setValue(e.target.value); @@ -984,6 +1481,7 @@ export const antdComponents = { > { @@ -1028,6 +1526,7 @@ export const antdComponents = { step={props.step ?? 1} value={value as [number, number]} range + disabled={props.disabled ?? false} marks={props.marks ?? undefined} onChange={(val: number | number[]) => { setValue(val as [number, number]); @@ -1040,6 +1539,7 @@ export const antdComponents = { max={props.max ?? 100} step={props.step ?? 1} value={value as number} + disabled={props.disabled ?? false} marks={props.marks ?? undefined} onChange={(val: number | number[]) => { setValue(val as number); @@ -1068,6 +1568,7 @@ export const antdComponents = { value={value} allowHalf={props.allowHalf ?? false} allowClear={props.allowClear ?? true} + disabled={props.disabled ?? false} onChange={(val) => { setValue(val); emit("change"); @@ -1086,22 +1587,26 @@ export const antdComponents = { props.value as string | undefined, bindings?.value, ); + const [localValue, setLocalValue] = useState(null); const isBound = !!bindings?.value; - const value = boundValue ?? props.value; + const value = isBound ? (boundValue ?? null) : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + // Convert string value to dayjs object for antd DatePicker + const dayjsValue = value ? dayjs(value) : null; - // Note: Antd DatePicker requires dayjs for full functionality - // For string-based usage, we pass the value directly return ( - { - if (isBound) { - setBoundValue(e.target.value); - } + format={props.format ?? "YYYY-MM-DD"} + picker={props.picker ?? "date"} + showTime={props.showTime ?? false} + disabled={props.disabled ?? false} + value={dayjsValue} + onChange={(date, dateString) => { + setValue(dateString as string); emit("change"); }} /> @@ -1132,6 +1637,7 @@ export const antdComponents = { name={props.name} placeholder={props.placeholder ?? undefined} format={props.format ?? "HH:mm:ss"} + disabled={props.disabled ?? false} value={dayjsValue} onChange={(time, timeString) => { setValue(timeString as string); @@ -1156,6 +1662,7 @@ export const antdComponents = { multiple={props.multiple ?? false} maxCount={props.maxCount ?? undefined} listType={props.listType ?? "text"} + disabled={props.disabled ?? false} fileList={fileList} beforeUpload={() => false} onChange={(info) => { @@ -1193,6 +1700,7 @@ export const antdComponents = { { setTargetKeys(newTargetKeys as string[]); @@ -1204,6 +1712,207 @@ export const antdComponents = { ); }, + AutoComplete: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + const options = (props.options ?? []).map((opt) => + typeof opt === "string" ? { label: opt, value: opt } : opt, + ); + + return ( + + { + setValue(val as string); + emit("change"); + }} + onSelect={(val) => { + setValue(val as string); + emit("select"); + }} + /> + + ); + }, + + Cascader: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string[] | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState([]); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? []) : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + // Transform options to handle null children -> undefined + const transformOptions = (options: unknown[]): unknown[] => { + return options.map((opt: any) => ({ + label: opt.label, + value: opt.value, + ...(opt.children ? { children: transformOptions(opt.children) } : {}), + })); + }; + + return ( + + { + setValue(val as string[]); + emit("change"); + }} + /> + + ); + }, + + ColorPicker: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(null); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? null) : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + // Map format to valid antd format types + const format = + props.format === "hex" + ? ("hex" as const) + : props.format === "rgb" + ? ("rgb" as const) + : ("hsb" as const); + + return ( + + { + const colorValue = + props.format === "hex" + ? color.toHexString() + : props.format === "rgb" + ? color.toRgbString() + : color.toHsbString(); + setValue(colorValue); + emit("change"); + }} + /> + + ); + }, + + Mentions: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + const options = (props.options ?? []).map((opt) => ({ + value: opt.value, + label: opt.label, + })); + + return ( + + { + setValue(val); + emit("change"); + }} + /> + + ); + }, + + TreeSelect: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + return ( + + ({ + ...item, + children: item.children ?? undefined, + }))} + placeholder={props.placeholder ?? undefined} + value={value || undefined} + allowClear={props.allowClear ?? false} + showSearch={props.showSearch ?? false} + multiple={props.multiple ?? false} + disabled={props.disabled ?? false} + treeCheckable={props.treeCheckable ?? false} + size={props.size ?? undefined} + onChange={(val) => { + setValue(val as string); + emit("change"); + }} + /> + + ); + }, + // ── Actions ─────────────────────────────────────────────────────────── Button: ({ props, emit, on }: BaseComponentProps>) => { @@ -1218,6 +1927,11 @@ export const antdComponents = { icon={props.icon ? : undefined} block={props.block ?? false} size={props.size ?? "middle"} + ghost={props.ghost ?? false} + shape={props.shape ?? undefined} + href={props.href ?? undefined} + target={props.target ?? undefined} + htmlType={props.htmlType ?? undefined} onClick={() => emit("press")} > {props.label} @@ -1300,6 +2014,11 @@ export const antdComponents = { showSizeChanger={props.showSizeChanger ?? false} showQuickJumper={props.showQuickJumper ?? false} simple={props.simple ?? false} + disabled={props.disabled ?? false} + size={props.size === "small" ? "small" : undefined} + hideOnSinglePage={props.hideOnSinglePage ?? false} + pageSizeOptions={props.pageSizeOptions ?? undefined} + align={props.align ?? undefined} onChange={(page, pageSize) => { setCurrent(page); emit("change"); @@ -1337,6 +2056,8 @@ export const antdComponents = { options={options} value={value} block={props.block ?? false} + disabled={props.disabled ?? false} + size={props.size ?? undefined} onChange={(val) => { setValue(val as string); emit("change"); @@ -1362,7 +2083,10 @@ export const antdComponents = { const items = (props.items ?? []).map((item) => ({ title: item.title, description: item.description, + subTitle: item.subTitle ?? undefined, icon: item.icon ? : undefined, + disabled: item.disabled ?? false, + status: item.status ?? undefined, })); return ( @@ -1370,6 +2094,11 @@ export const antdComponents = { current={current} direction={props.direction ?? "horizontal"} status={props.status ?? "process"} + size={props.size ?? undefined} + type={props.type ?? undefined} + initial={props.initial ?? undefined} + labelPlacement={props.labelPlacement ?? undefined} + percent={props.percent ?? undefined} items={items} onChange={(current) => { setCurrent(current); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6202d93f..0fbade29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1049,6 +1049,9 @@ importers: antd: specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + dayjs: + specifier: ^1.11.11 + version: 1.11.19 tsup: specifier: ^8.0.2 version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) @@ -17765,14 +17768,14 @@ snapshots: '@remotion/media-parser': 4.0.418 '@remotion/studio': 4.0.418(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@remotion/studio-shared': 4.0.418(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - css-loader: 5.2.7(webpack@5.96.1) + css-loader: 5.2.7(webpack@5.96.1(esbuild@0.25.0)) esbuild: 0.25.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) react-refresh: 0.9.0 remotion: 4.0.418(react-dom@19.2.3(react@19.2.3))(react@19.2.3) source-map: 0.7.3 - style-loader: 4.0.0(webpack@5.96.1) + style-loader: 4.0.0(webpack@5.96.1(esbuild@0.25.0)) webpack: 5.96.1(esbuild@0.25.0) transitivePeerDependencies: - '@swc/core' @@ -19890,7 +19893,7 @@ snapshots: css-gradient-parser@0.0.17: {} - css-loader@5.2.7(webpack@5.96.1): + css-loader@5.2.7(webpack@5.96.1(esbuild@0.25.0)): dependencies: icss-utils: 5.1.0(postcss@8.5.6) loader-utils: 2.0.4 @@ -25919,7 +25922,7 @@ snapshots: structured-headers@0.4.1: {} - style-loader@4.0.0(webpack@5.96.1): + style-loader@4.0.0(webpack@5.96.1(esbuild@0.25.0)): dependencies: webpack: 5.96.1(esbuild@0.25.0) From 537e053fe927c1ca957f69627ad60fcdce1ae54c Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:31:06 +0100 Subject: [PATCH 3/9] feat: Svelte-renderer (#168) * create svelte-renderer * helpers * reactive API * turn into provider components * fixes * examples * docs * correct component props for catalog, harmonize defineRegistry API with other APis * shuffle things around * createRenderer * on * boundary/warning * functions * fix * fix types # Conflicts: # pnpm-lock.yaml --- .gitignore | 1 + README.md | 3 +- apps/web/app/(main)/docs/api/svelte/page.mdx | 126 ++ .../web/app/(main)/docs/installation/page.mdx | 8 + apps/web/lib/docs-navigation.ts | 8 +- apps/web/lib/page-titles.ts | 1 + examples/react-email/package.json | 2 +- examples/svelte-chat/.gitignore | 23 + examples/svelte-chat/.npmrc | 1 + examples/svelte-chat/README.md | 42 + examples/svelte-chat/components.json | 16 + examples/svelte-chat/package.json | 39 + examples/svelte-chat/src/app.css | 125 ++ examples/svelte-chat/src/app.d.ts | 13 + examples/svelte-chat/src/app.html | 11 + examples/svelte-chat/src/lib/agent.ts | 92 + .../svelte-chat/src/lib/assets/favicon.svg | 1 + .../ui/accordion/accordion-content.svelte | 22 + .../ui/accordion/accordion-item.svelte | 17 + .../ui/accordion/accordion-trigger.svelte | 32 + .../components/ui/accordion/accordion.svelte | 16 + .../src/lib/components/ui/accordion/index.ts | 16 + .../ui/alert/alert-description.svelte | 23 + .../components/ui/alert/alert-title.svelte | 20 + .../src/lib/components/ui/alert/alert.svelte | 44 + .../src/lib/components/ui/alert/index.ts | 14 + .../src/lib/components/ui/badge/badge.svelte | 50 + .../src/lib/components/ui/badge/index.ts | 2 + .../lib/components/ui/button/button.svelte | 82 + .../src/lib/components/ui/button/index.ts | 17 + .../lib/components/ui/card/card-action.svelte | 20 + .../components/ui/card/card-content.svelte | 15 + .../ui/card/card-description.svelte | 20 + .../lib/components/ui/card/card-footer.svelte | 20 + .../lib/components/ui/card/card-header.svelte | 23 + .../lib/components/ui/card/card-title.svelte | 20 + .../src/lib/components/ui/card/card.svelte | 23 + .../src/lib/components/ui/card/index.ts | 25 + .../src/lib/components/ui/input/index.ts | 7 + .../src/lib/components/ui/input/input.svelte | 52 + .../src/lib/components/ui/label/index.ts | 7 + .../src/lib/components/ui/label/label.svelte | 20 + .../src/lib/components/ui/progress/index.ts | 7 + .../components/ui/progress/progress.svelte | 27 + .../lib/components/ui/radio-group/index.ts | 10 + .../ui/radio-group/radio-group-item.svelte | 31 + .../ui/radio-group/radio-group.svelte | 19 + .../src/lib/components/ui/select/index.ts | 37 + .../ui/select/select-content.svelte | 45 + .../ui/select/select-group-heading.svelte | 21 + .../components/ui/select/select-group.svelte | 7 + .../components/ui/select/select-item.svelte | 38 + .../components/ui/select/select-label.svelte | 20 + .../components/ui/select/select-portal.svelte | 7 + .../select/select-scroll-down-button.svelte | 20 + .../ui/select/select-scroll-up-button.svelte | 20 + .../ui/select/select-separator.svelte | 18 + .../ui/select/select-trigger.svelte | 29 + .../lib/components/ui/select/select.svelte | 11 + .../src/lib/components/ui/separator/index.ts | 7 + .../components/ui/separator/separator.svelte | 21 + .../src/lib/components/ui/table/index.ts | 28 + .../lib/components/ui/table/table-body.svelte | 20 + .../components/ui/table/table-caption.svelte | 20 + .../lib/components/ui/table/table-cell.svelte | 23 + .../components/ui/table/table-footer.svelte | 20 + .../lib/components/ui/table/table-head.svelte | 23 + .../components/ui/table/table-header.svelte | 20 + .../lib/components/ui/table/table-row.svelte | 23 + .../src/lib/components/ui/table/table.svelte | 22 + .../src/lib/components/ui/tabs/index.ts | 16 + .../components/ui/tabs/tabs-content.svelte | 17 + .../lib/components/ui/tabs/tabs-list.svelte | 20 + .../components/ui/tabs/tabs-trigger.svelte | 20 + .../src/lib/components/ui/tabs/tabs.svelte | 19 + examples/svelte-chat/src/lib/index.ts | 4 + .../src/lib/render/Renderer.svelte | 16 + .../svelte-chat/src/lib/render/catalog.ts | 363 ++++ .../lib/render/components/Accordion.svelte | 22 + .../src/lib/render/components/Alert.svelte | 19 + .../src/lib/render/components/Badge.svelte | 13 + .../src/lib/render/components/BarChart.svelte | 99 ++ .../src/lib/render/components/Button.svelte | 22 + .../src/lib/render/components/Callout.svelte | 57 + .../src/lib/render/components/Card.svelte | 32 + .../src/lib/render/components/Grid.svelte | 31 + .../src/lib/render/components/Heading.svelte | 22 + .../lib/render/components/LineChart.svelte | 109 ++ .../src/lib/render/components/Link.svelte | 19 + .../src/lib/render/components/Metric.svelte | 40 + .../src/lib/render/components/PieChart.svelte | 106 ++ .../src/lib/render/components/Progress.svelte | 13 + .../lib/render/components/RadioGroup.svelte | 45 + .../lib/render/components/SelectInput.svelte | 56 + .../lib/render/components/Separator.svelte | 10 + .../src/lib/render/components/Skeleton.svelte | 14 + .../src/lib/render/components/Stack.svelte | 28 + .../lib/render/components/TabContent.svelte | 19 + .../src/lib/render/components/Table.svelte | 91 + .../src/lib/render/components/Tabs.svelte | 29 + .../src/lib/render/components/Text.svelte | 14 + .../lib/render/components/TextInput.svelte | 43 + .../src/lib/render/components/Timeline.svelte | 44 + .../svelte-chat/src/lib/render/registry.ts | 61 + examples/svelte-chat/src/lib/tools/crypto.ts | 165 ++ examples/svelte-chat/src/lib/tools/github.ts | 237 +++ .../svelte-chat/src/lib/tools/hackernews.ts | 67 + examples/svelte-chat/src/lib/tools/search.ts | 36 + examples/svelte-chat/src/lib/tools/weather.ts | 126 ++ examples/svelte-chat/src/lib/utils.ts | 18 + .../svelte-chat/src/routes/+layout.svelte | 13 + examples/svelte-chat/src/routes/+page.svelte | 381 ++++ .../src/routes/api/generate/+server.ts | 35 + examples/svelte-chat/static/robots.txt | 3 + examples/svelte-chat/svelte.config.js | 10 + examples/svelte-chat/tsconfig.json | 20 + examples/svelte-chat/vite.config.ts | 10 + examples/svelte/index.html | 12 + examples/svelte/package.json | 24 + examples/svelte/src/App.svelte | 11 + examples/svelte/src/DemoRenderer.svelte | 46 + examples/svelte/src/app.css | 13 + examples/svelte/src/lib/catalog.ts | 90 + .../svelte/src/lib/components/Badge.svelte | 24 + .../svelte/src/lib/components/Button.svelte | 44 + .../svelte/src/lib/components/Card.svelte | 47 + .../svelte/src/lib/components/Input.svelte | 29 + .../svelte/src/lib/components/ListItem.svelte | 49 + .../svelte/src/lib/components/Stack.svelte | 35 + .../svelte/src/lib/components/Text.svelte | 34 + examples/svelte/src/lib/registry.ts | 22 + examples/svelte/src/lib/spec.ts | 130 ++ examples/svelte/src/main.ts | 9 + examples/svelte/src/vite-env.d.ts | 1 + examples/svelte/svelte.config.js | 1 + examples/svelte/tsconfig.json | 17 + examples/svelte/vite.config.ts | 6 + examples/vite-renderers/package.json | 6 +- examples/vite-renderers/src/main.ts | 8 +- .../vite-renderers/src/react/registry.tsx | 17 +- .../vite-renderers/src/shared/catalog-def.ts | 6 +- .../vite-renderers/src/shared/handlers.ts | 6 + examples/vite-renderers/src/shared/styles.css | 15 + examples/vite-renderers/src/spec.ts | 4 +- examples/vite-renderers/src/svelte/App.svelte | 22 + .../src/svelte/DemoRenderer.svelte | 28 + examples/vite-renderers/src/svelte/catalog.ts | 4 + .../src/svelte/components/Badge.svelte | 24 + .../src/svelte/components/Button.svelte | 44 + .../src/svelte/components/Card.svelte | 47 + .../src/svelte/components/Input.svelte | 29 + .../src/svelte/components/ListItem.svelte | 49 + .../svelte/components/RendererBadge.svelte | 16 + .../src/svelte/components/RendererTabs.svelte | 46 + .../src/svelte/components/Stack.svelte | 35 + .../src/svelte/components/Text.svelte | 34 + examples/vite-renderers/src/svelte/mount.ts | 19 + .../vite-renderers/src/svelte/registry.ts | 28 + examples/vite-renderers/src/vue/registry.ts | 19 +- examples/vite-renderers/svelte.config.js | 1 + examples/vite-renderers/tsconfig.json | 2 +- examples/vite-renderers/vite.config.ts | 3 +- examples/vue/package.json | 2 +- examples/vue/src/DemoRenderer.vue | 1 + package.json | 3 + packages/svelte/package.json | 70 + packages/svelte/src/CatalogRenderer.svelte | 55 + packages/svelte/src/ConfirmDialog.svelte | 98 ++ .../svelte/src/ConfirmDialogManager.svelte | 15 + packages/svelte/src/ElementRenderer.svelte | 147 ++ packages/svelte/src/JsonUIProvider.svelte | 68 + packages/svelte/src/Renderer.svelte | 34 + .../src/RendererWithProvider.test.svelte | 37 + packages/svelte/src/RepeatChildren.svelte | 50 + packages/svelte/src/TestButton.svelte | 11 + packages/svelte/src/TestContainer.svelte | 19 + packages/svelte/src/TestText.svelte | 9 + packages/svelte/src/catalog-types.ts | 104 ++ .../svelte/src/contexts/ActionProvider.svelte | 344 ++++ .../contexts/FunctionsContextProvider.svelte | 44 + .../src/contexts/RepeatScopeProvider.svelte | 49 + .../svelte/src/contexts/StateProvider.svelte | 182 ++ .../src/contexts/ValidationProvider.svelte | 198 +++ .../src/contexts/VisibilityProvider.svelte | 82 + packages/svelte/src/contexts/actions.test.ts | 278 +++ packages/svelte/src/contexts/state.test.ts | 207 +++ .../svelte/src/contexts/visibility.test.ts | 161 ++ packages/svelte/src/index.ts | 130 ++ packages/svelte/src/renderer.test.ts | 194 +++ packages/svelte/src/renderer.ts | 274 +++ packages/svelte/src/schema.ts | 109 ++ packages/svelte/src/streaming.svelte.ts | 533 ++++++ packages/svelte/src/utils.svelte.ts | 124 ++ packages/svelte/src/utils.test.ts | 379 ++++ packages/svelte/svelte.config.js | 6 + packages/svelte/tsconfig.json | 11 + pnpm-lock.yaml | 1526 ++++++++++++----- skills/json-render-svelte/SKILL.md | 284 +++ vitest.config.ts => vitest.config.mts | 9 +- 199 files changed, 10909 insertions(+), 461 deletions(-) create mode 100644 apps/web/app/(main)/docs/api/svelte/page.mdx create mode 100644 examples/svelte-chat/.gitignore create mode 100644 examples/svelte-chat/.npmrc create mode 100644 examples/svelte-chat/README.md create mode 100644 examples/svelte-chat/components.json create mode 100644 examples/svelte-chat/package.json create mode 100644 examples/svelte-chat/src/app.css create mode 100644 examples/svelte-chat/src/app.d.ts create mode 100644 examples/svelte-chat/src/app.html create mode 100644 examples/svelte-chat/src/lib/agent.ts create mode 100644 examples/svelte-chat/src/lib/assets/favicon.svg create mode 100644 examples/svelte-chat/src/lib/components/ui/accordion/accordion-content.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/accordion/accordion-item.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/accordion/accordion-trigger.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/accordion/accordion.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/accordion/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/alert/alert-description.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/alert/alert-title.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/alert/alert.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/alert/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/badge/badge.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/badge/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/button/button.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/button/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/card/card-action.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/card/card-content.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/card/card-description.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/card/card-footer.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/card/card-header.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/card/card-title.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/card/card.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/card/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/input/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/input/input.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/label/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/label/label.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/progress/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/progress/progress.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/radio-group/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/radio-group/radio-group-item.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/radio-group/radio-group.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-content.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-group-heading.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-group.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-item.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-label.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-portal.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-scroll-down-button.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-scroll-up-button.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-separator.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select-trigger.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/select/select.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/separator/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/separator/separator.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/table/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/table/table-body.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/table/table-caption.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/table/table-cell.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/table/table-footer.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/table/table-head.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/table/table-header.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/table/table-row.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/table/table.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/tabs/index.ts create mode 100644 examples/svelte-chat/src/lib/components/ui/tabs/tabs-content.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/tabs/tabs-list.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/tabs/tabs-trigger.svelte create mode 100644 examples/svelte-chat/src/lib/components/ui/tabs/tabs.svelte create mode 100644 examples/svelte-chat/src/lib/index.ts create mode 100644 examples/svelte-chat/src/lib/render/Renderer.svelte create mode 100644 examples/svelte-chat/src/lib/render/catalog.ts create mode 100644 examples/svelte-chat/src/lib/render/components/Accordion.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Alert.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Badge.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/BarChart.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Button.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Callout.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Card.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Grid.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Heading.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/LineChart.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Link.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Metric.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/PieChart.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Progress.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/RadioGroup.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/SelectInput.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Separator.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Skeleton.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Stack.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/TabContent.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Table.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Tabs.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Text.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/TextInput.svelte create mode 100644 examples/svelte-chat/src/lib/render/components/Timeline.svelte create mode 100644 examples/svelte-chat/src/lib/render/registry.ts create mode 100644 examples/svelte-chat/src/lib/tools/crypto.ts create mode 100644 examples/svelte-chat/src/lib/tools/github.ts create mode 100644 examples/svelte-chat/src/lib/tools/hackernews.ts create mode 100644 examples/svelte-chat/src/lib/tools/search.ts create mode 100644 examples/svelte-chat/src/lib/tools/weather.ts create mode 100644 examples/svelte-chat/src/lib/utils.ts create mode 100644 examples/svelte-chat/src/routes/+layout.svelte create mode 100644 examples/svelte-chat/src/routes/+page.svelte create mode 100644 examples/svelte-chat/src/routes/api/generate/+server.ts create mode 100644 examples/svelte-chat/static/robots.txt create mode 100644 examples/svelte-chat/svelte.config.js create mode 100644 examples/svelte-chat/tsconfig.json create mode 100644 examples/svelte-chat/vite.config.ts create mode 100644 examples/svelte/index.html create mode 100644 examples/svelte/package.json create mode 100644 examples/svelte/src/App.svelte create mode 100644 examples/svelte/src/DemoRenderer.svelte create mode 100644 examples/svelte/src/app.css create mode 100644 examples/svelte/src/lib/catalog.ts create mode 100644 examples/svelte/src/lib/components/Badge.svelte create mode 100644 examples/svelte/src/lib/components/Button.svelte create mode 100644 examples/svelte/src/lib/components/Card.svelte create mode 100644 examples/svelte/src/lib/components/Input.svelte create mode 100644 examples/svelte/src/lib/components/ListItem.svelte create mode 100644 examples/svelte/src/lib/components/Stack.svelte create mode 100644 examples/svelte/src/lib/components/Text.svelte create mode 100644 examples/svelte/src/lib/registry.ts create mode 100644 examples/svelte/src/lib/spec.ts create mode 100644 examples/svelte/src/main.ts create mode 100644 examples/svelte/src/vite-env.d.ts create mode 100644 examples/svelte/svelte.config.js create mode 100644 examples/svelte/tsconfig.json create mode 100644 examples/svelte/vite.config.ts create mode 100644 examples/vite-renderers/src/svelte/App.svelte create mode 100644 examples/vite-renderers/src/svelte/DemoRenderer.svelte create mode 100644 examples/vite-renderers/src/svelte/catalog.ts create mode 100644 examples/vite-renderers/src/svelte/components/Badge.svelte create mode 100644 examples/vite-renderers/src/svelte/components/Button.svelte create mode 100644 examples/vite-renderers/src/svelte/components/Card.svelte create mode 100644 examples/vite-renderers/src/svelte/components/Input.svelte create mode 100644 examples/vite-renderers/src/svelte/components/ListItem.svelte create mode 100644 examples/vite-renderers/src/svelte/components/RendererBadge.svelte create mode 100644 examples/vite-renderers/src/svelte/components/RendererTabs.svelte create mode 100644 examples/vite-renderers/src/svelte/components/Stack.svelte create mode 100644 examples/vite-renderers/src/svelte/components/Text.svelte create mode 100644 examples/vite-renderers/src/svelte/mount.ts create mode 100644 examples/vite-renderers/src/svelte/registry.ts create mode 100644 examples/vite-renderers/svelte.config.js create mode 100644 packages/svelte/package.json create mode 100644 packages/svelte/src/CatalogRenderer.svelte create mode 100644 packages/svelte/src/ConfirmDialog.svelte create mode 100644 packages/svelte/src/ConfirmDialogManager.svelte create mode 100644 packages/svelte/src/ElementRenderer.svelte create mode 100644 packages/svelte/src/JsonUIProvider.svelte create mode 100644 packages/svelte/src/Renderer.svelte create mode 100644 packages/svelte/src/RendererWithProvider.test.svelte create mode 100644 packages/svelte/src/RepeatChildren.svelte create mode 100644 packages/svelte/src/TestButton.svelte create mode 100644 packages/svelte/src/TestContainer.svelte create mode 100644 packages/svelte/src/TestText.svelte create mode 100644 packages/svelte/src/catalog-types.ts create mode 100644 packages/svelte/src/contexts/ActionProvider.svelte create mode 100644 packages/svelte/src/contexts/FunctionsContextProvider.svelte create mode 100644 packages/svelte/src/contexts/RepeatScopeProvider.svelte create mode 100644 packages/svelte/src/contexts/StateProvider.svelte create mode 100644 packages/svelte/src/contexts/ValidationProvider.svelte create mode 100644 packages/svelte/src/contexts/VisibilityProvider.svelte create mode 100644 packages/svelte/src/contexts/actions.test.ts create mode 100644 packages/svelte/src/contexts/state.test.ts create mode 100644 packages/svelte/src/contexts/visibility.test.ts create mode 100644 packages/svelte/src/index.ts create mode 100644 packages/svelte/src/renderer.test.ts create mode 100644 packages/svelte/src/renderer.ts create mode 100644 packages/svelte/src/schema.ts create mode 100644 packages/svelte/src/streaming.svelte.ts create mode 100644 packages/svelte/src/utils.svelte.ts create mode 100644 packages/svelte/src/utils.test.ts create mode 100644 packages/svelte/svelte.config.js create mode 100644 packages/svelte/tsconfig.json create mode 100644 skills/json-render-svelte/SKILL.md rename vitest.config.ts => vitest.config.mts (68%) diff --git a/.gitignore b/.gitignore index 4d2d4b6f..cdd528c4 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ out/ build dist *.tsbuildinfo +.svelte-kit/ # Debug diff --git a/README.md b/README.md index 329b0cde..b458e236 100644 --- a/README.md +++ b/README.md @@ -498,8 +498,9 @@ pnpm dev - http://react-email-demo.json-render.localhost:1355 - React Email Example - http://remotion-demo.json-render.localhost:1355 - Remotion Video Example - 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` -- 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` ## How It Works diff --git a/apps/web/app/(main)/docs/api/svelte/page.mdx b/apps/web/app/(main)/docs/api/svelte/page.mdx new file mode 100644 index 00000000..a082a7bb --- /dev/null +++ b/apps/web/app/(main)/docs/api/svelte/page.mdx @@ -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 + + + +Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`. + + + +## Components + +### Renderer + +```svelte + +``` + +Renders a spec with your component registry. If `spec` is `null`, it renders nothing. + +### JsonUIProvider + +Convenience wrapper around `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`. + +```svelte + + + +``` + +## 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`: + +```typescript +interface BaseComponentProps { + props: TProps; + children?: Snippet; + emit: (event: string) => void; + bindings?: Record; + 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. diff --git a/apps/web/app/(main)/docs/installation/page.mdx b/apps/web/app/(main)/docs/installation/page.mdx index ff909de7..6195b7a0 100644 --- a/apps/web/app/(main)/docs/installation/page.mdx +++ b/apps/web/app/(main)/docs/installation/page.mdx @@ -21,6 +21,14 @@ Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`. +## For Svelte + + + +Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`. + + + ## For React UI with shadcn/ui Pre-built components for fast prototyping and production use: diff --git a/apps/web/lib/docs-navigation.ts b/apps/web/lib/docs-navigation.ts index be708ffb..cb700b22 100644 --- a/apps/web/lib/docs-navigation.ts +++ b/apps/web/lib/docs-navigation.ts @@ -80,13 +80,18 @@ export const docsNavigation: NavSection[] = [ href: "https://github.com/vercel-labs/json-render/tree/main/examples/image", external: true, }, + { + title: "Svelte", + href: "https://github.com/vercel-labs/json-render/tree/main/examples/svelte", + external: true, + }, { title: "Vue", href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue", 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", external: true, }, @@ -121,6 +126,7 @@ export const docsNavigation: NavSection[] = [ { title: "@json-render/image", href: "/docs/api/image" }, { title: "@json-render/remotion", href: "/docs/api/remotion" }, { title: "@json-render/vue", href: "/docs/api/vue" }, + { title: "@json-render/svelte", href: "/docs/api/svelte" }, { title: "@json-render/codegen", href: "/docs/api/codegen" }, ], }, diff --git a/apps/web/lib/page-titles.ts b/apps/web/lib/page-titles.ts index 3a06628e..2dd35139 100644 --- a/apps/web/lib/page-titles.ts +++ b/apps/web/lib/page-titles.ts @@ -45,6 +45,7 @@ export const PAGE_TITLES: Record = { "docs/api/react-pdf": "@json-render/react-pdf API", "docs/api/react-email": "@json-render/react-email 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/image": "@json-render/image API", "docs/api/remotion": "@json-render/remotion API", diff --git a/examples/react-email/package.json b/examples/react-email/package.json index ff6f6288..882fee33 100644 --- a/examples/react-email/package.json +++ b/examples/react-email/package.json @@ -25,7 +25,7 @@ "react-resizable-panels": "^4.4.1", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.1.18", - "zod": "4.3.5" + "zod": "4.3.6" }, "devDependencies": { "@internal/typescript-config": "workspace:*", diff --git a/examples/svelte-chat/.gitignore b/examples/svelte-chat/.gitignore new file mode 100644 index 00000000..3b462cb0 --- /dev/null +++ b/examples/svelte-chat/.gitignore @@ -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-* diff --git a/examples/svelte-chat/.npmrc b/examples/svelte-chat/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/examples/svelte-chat/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/examples/svelte-chat/README.md b/examples/svelte-chat/README.md new file mode 100644 index 00000000..3d2e6bf2 --- /dev/null +++ b/examples/svelte-chat/README.md @@ -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. diff --git a/examples/svelte-chat/components.json b/examples/svelte-chat/components.json new file mode 100644 index 00000000..f258682d --- /dev/null +++ b/examples/svelte-chat/components.json @@ -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" +} diff --git a/examples/svelte-chat/package.json b/examples/svelte-chat/package.json new file mode 100644 index 00000000..10136891 --- /dev/null +++ b/examples/svelte-chat/package.json @@ -0,0 +1,39 @@ +{ + "name": "svelte-chat", + "private": true, + "version": "0.0.1", + "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" + } +} diff --git a/examples/svelte-chat/src/app.css b/examples/svelte-chat/src/app.css new file mode 100644 index 00000000..b95a3cda --- /dev/null +++ b/examples/svelte-chat/src/app.css @@ -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; +} diff --git a/examples/svelte-chat/src/app.d.ts b/examples/svelte-chat/src/app.d.ts new file mode 100644 index 00000000..520c4217 --- /dev/null +++ b/examples/svelte-chat/src/app.d.ts @@ -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 {}; diff --git a/examples/svelte-chat/src/app.html b/examples/svelte-chat/src/app.html new file mode 100644 index 00000000..f273cc58 --- /dev/null +++ b/examples/svelte-chat/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/svelte-chat/src/lib/agent.ts b/examples/svelte-chat/src/lib/agent.ts new file mode 100644 index 00000000..42331bea --- /dev/null +++ b/examples/svelte-chat/src/lib/agent.ts @@ -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: "chat", + 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, +}); diff --git a/examples/svelte-chat/src/lib/assets/favicon.svg b/examples/svelte-chat/src/lib/assets/favicon.svg new file mode 100644 index 00000000..cc5dc66a --- /dev/null +++ b/examples/svelte-chat/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/examples/svelte-chat/src/lib/components/ui/accordion/accordion-content.svelte b/examples/svelte-chat/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 00000000..559db3d5 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,22 @@ + + + +
+ {@render children?.()} +
+
diff --git a/examples/svelte-chat/src/lib/components/ui/accordion/accordion-item.svelte b/examples/svelte-chat/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 00000000..780545c6 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/accordion/accordion-trigger.svelte b/examples/svelte-chat/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 00000000..c46c2468 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,32 @@ + + + + svg]:rotate-180", + className + )} + {...restProps} + > + {@render children?.()} + + + diff --git a/examples/svelte-chat/src/lib/components/ui/accordion/accordion.svelte b/examples/svelte-chat/src/lib/components/ui/accordion/accordion.svelte new file mode 100644 index 00000000..117ee37f --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/accordion/accordion.svelte @@ -0,0 +1,16 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/accordion/index.ts b/examples/svelte-chat/src/lib/components/ui/accordion/index.ts new file mode 100644 index 00000000..ef0dab75 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/accordion/index.ts @@ -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, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/alert/alert-description.svelte b/examples/svelte-chat/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 00000000..8b56aed2 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/alert/alert-title.svelte b/examples/svelte-chat/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 00000000..77e45ad5 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/alert/alert.svelte b/examples/svelte-chat/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 00000000..2b2eff9a --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/examples/svelte-chat/src/lib/components/ui/alert/index.ts b/examples/svelte-chat/src/lib/components/ui/alert/index.ts new file mode 100644 index 00000000..e47ba7d3 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/alert/index.ts @@ -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, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/badge/badge.svelte b/examples/svelte-chat/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 00000000..e3164ba7 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/examples/svelte-chat/src/lib/components/ui/badge/index.ts b/examples/svelte-chat/src/lib/components/ui/badge/index.ts new file mode 100644 index 00000000..64e0aa9b --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/examples/svelte-chat/src/lib/components/ui/button/button.svelte b/examples/svelte-chat/src/lib/components/ui/button/button.svelte new file mode 100644 index 00000000..a8296aed --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/examples/svelte-chat/src/lib/components/ui/button/index.ts b/examples/svelte-chat/src/lib/components/ui/button/index.ts new file mode 100644 index 00000000..872d97cb --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/button/index.ts @@ -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, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/card/card-action.svelte b/examples/svelte-chat/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 00000000..cc36c566 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/card/card-content.svelte b/examples/svelte-chat/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 00000000..bc90b837 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/card/card-description.svelte b/examples/svelte-chat/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 00000000..9b20ac70 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/examples/svelte-chat/src/lib/components/ui/card/card-footer.svelte b/examples/svelte-chat/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 00000000..2d4d0f24 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/card/card-header.svelte b/examples/svelte-chat/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 00000000..25017884 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/card/card-title.svelte b/examples/svelte-chat/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 00000000..74472311 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/card/card.svelte b/examples/svelte-chat/src/lib/components/ui/card/card.svelte new file mode 100644 index 00000000..99448cc9 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/card/index.ts b/examples/svelte-chat/src/lib/components/ui/card/index.ts new file mode 100644 index 00000000..406a5ceb --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/card/index.ts @@ -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, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/input/index.ts b/examples/svelte-chat/src/lib/components/ui/input/index.ts new file mode 100644 index 00000000..ceb4b164 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from "./input.svelte"; + +export { + Root, + // + Root as Input, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/input/input.svelte b/examples/svelte-chat/src/lib/components/ui/input/input.svelte new file mode 100644 index 00000000..ff1a4c87 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/input/input.svelte @@ -0,0 +1,52 @@ + + +{#if type === "file"} + +{:else} + +{/if} diff --git a/examples/svelte-chat/src/lib/components/ui/label/index.ts b/examples/svelte-chat/src/lib/components/ui/label/index.ts new file mode 100644 index 00000000..b0b23ce0 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/label/label.svelte b/examples/svelte-chat/src/lib/components/ui/label/label.svelte new file mode 100644 index 00000000..d71afbca --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/label/label.svelte @@ -0,0 +1,20 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/progress/index.ts b/examples/svelte-chat/src/lib/components/ui/progress/index.ts new file mode 100644 index 00000000..1e415fc3 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/progress/index.ts @@ -0,0 +1,7 @@ +import Root from "./progress.svelte"; + +export { + Root, + // + Root as Progress, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/progress/progress.svelte b/examples/svelte-chat/src/lib/components/ui/progress/progress.svelte new file mode 100644 index 00000000..68330136 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/progress/progress.svelte @@ -0,0 +1,27 @@ + + + +
+
diff --git a/examples/svelte-chat/src/lib/components/ui/radio-group/index.ts b/examples/svelte-chat/src/lib/components/ui/radio-group/index.ts new file mode 100644 index 00000000..b6089461 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/radio-group/index.ts @@ -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, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/radio-group/radio-group-item.svelte b/examples/svelte-chat/src/lib/components/ui/radio-group/radio-group-item.svelte new file mode 100644 index 00000000..f0813db3 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/radio-group/radio-group-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} +
+ {#if checked} + + {/if} +
+ {/snippet} +
diff --git a/examples/svelte-chat/src/lib/components/ui/radio-group/radio-group.svelte b/examples/svelte-chat/src/lib/components/ui/radio-group/radio-group.svelte new file mode 100644 index 00000000..da2912b0 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/radio-group/radio-group.svelte @@ -0,0 +1,19 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/select/index.ts b/examples/svelte-chat/src/lib/components/ui/select/index.ts new file mode 100644 index 00000000..222d568a --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import Root from "./select.svelte"; +import Group from "./select-group.svelte"; +import Label from "./select-label.svelte"; +import Item from "./select-item.svelte"; +import Content from "./select-content.svelte"; +import Trigger from "./select-trigger.svelte"; +import Separator from "./select-separator.svelte"; +import ScrollDownButton from "./select-scroll-down-button.svelte"; +import ScrollUpButton from "./select-scroll-up-button.svelte"; +import GroupHeading from "./select-group-heading.svelte"; +import Portal from "./select-portal.svelte"; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + Portal, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, + Portal as SelectPortal, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-content.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 00000000..4b9ca438 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,45 @@ + + + + + + + {@render children?.()} + + + + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-group-heading.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 00000000..1fab5f00 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-group.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 00000000..a1f43bf3 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-item.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 00000000..b85eef69 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-label.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 00000000..46960259 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-portal.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-portal.svelte new file mode 100644 index 00000000..424bcddc --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-scroll-down-button.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 00000000..36292058 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-scroll-up-button.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 00000000..1aa2300c --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-separator.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 00000000..0eac3ebc --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select-trigger.svelte b/examples/svelte-chat/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 00000000..dbb81dfa --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/examples/svelte-chat/src/lib/components/ui/select/select.svelte b/examples/svelte-chat/src/lib/components/ui/select/select.svelte new file mode 100644 index 00000000..05eb6634 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/select/select.svelte @@ -0,0 +1,11 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/separator/index.ts b/examples/svelte-chat/src/lib/components/ui/separator/index.ts new file mode 100644 index 00000000..d66644e4 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/separator/separator.svelte b/examples/svelte-chat/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 00000000..f40999fa --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,21 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/table/index.ts b/examples/svelte-chat/src/lib/components/ui/table/index.ts new file mode 100644 index 00000000..3fe1e39d --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/table/table-body.svelte b/examples/svelte-chat/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 00000000..29e96875 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} + diff --git a/examples/svelte-chat/src/lib/components/ui/table/table-caption.svelte b/examples/svelte-chat/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 00000000..4696cff5 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/table/table-cell.svelte b/examples/svelte-chat/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 00000000..2c0c26a0 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,23 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/table/table-footer.svelte b/examples/svelte-chat/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 00000000..b9b14ebf --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0", className)} + {...restProps} +> + {@render children?.()} + diff --git a/examples/svelte-chat/src/lib/components/ui/table/table-head.svelte b/examples/svelte-chat/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 00000000..b67a6f9b --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/table/table-header.svelte b/examples/svelte-chat/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 00000000..f47d2597 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/examples/svelte-chat/src/lib/components/ui/table/table-row.svelte b/examples/svelte-chat/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 00000000..0df769e0 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + +svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", + className + )} + {...restProps} +> + {@render children?.()} + diff --git a/examples/svelte-chat/src/lib/components/ui/table/table.svelte b/examples/svelte-chat/src/lib/components/ui/table/table.svelte new file mode 100644 index 00000000..a3349563 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
+
+ {@render children?.()} +
+ {@render children?.()} +
+ {@render children?.()} +
+ {@render children?.()} +
+ diff --git a/examples/svelte-chat/src/lib/components/ui/tabs/index.ts b/examples/svelte-chat/src/lib/components/ui/tabs/index.ts new file mode 100644 index 00000000..4c728b6e --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,16 @@ +import Root from "./tabs.svelte"; +import Content from "./tabs-content.svelte"; +import List from "./tabs-list.svelte"; +import Trigger from "./tabs-trigger.svelte"; + +export { + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger, +}; diff --git a/examples/svelte-chat/src/lib/components/ui/tabs/tabs-content.svelte b/examples/svelte-chat/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 00000000..340d65cf --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/tabs/tabs-list.svelte b/examples/svelte-chat/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 00000000..08932b60 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,20 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/tabs/tabs-trigger.svelte b/examples/svelte-chat/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 00000000..e623b366 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,20 @@ + + + diff --git a/examples/svelte-chat/src/lib/components/ui/tabs/tabs.svelte b/examples/svelte-chat/src/lib/components/ui/tabs/tabs.svelte new file mode 100644 index 00000000..ef6cada5 --- /dev/null +++ b/examples/svelte-chat/src/lib/components/ui/tabs/tabs.svelte @@ -0,0 +1,19 @@ + + + diff --git a/examples/svelte-chat/src/lib/index.ts b/examples/svelte-chat/src/lib/index.ts new file mode 100644 index 00000000..71fe4cc3 --- /dev/null +++ b/examples/svelte-chat/src/lib/index.ts @@ -0,0 +1,4 @@ +// place files you want to import through the `$lib` alias in this folder. +export { explorerCatalog } from "./render/catalog"; +export { registry } from "./render/registry"; +export { agent } from "./agent"; diff --git a/examples/svelte-chat/src/lib/render/Renderer.svelte b/examples/svelte-chat/src/lib/render/Renderer.svelte new file mode 100644 index 00000000..fe03f820 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/Renderer.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/examples/svelte-chat/src/lib/render/catalog.ts b/examples/svelte-chat/src/lib/render/catalog.ts new file mode 100644 index 00000000..bb0c5379 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/catalog.ts @@ -0,0 +1,363 @@ +import { schema } from "@json-render/svelte/schema"; +import { z } from "zod"; + +/** + * json-render + AI SDK Example Catalog (Svelte) + * + * Components for rendering data dashboards generated by the ToolLoopAgent. + * Data flows in through tools (weather, GitHub, crypto, HN), not user actions. + */ +export const explorerCatalog = schema.createCatalog({ + components: { + // Layout + Stack: { + props: z.object({ + direction: z.enum(["horizontal", "vertical"]).nullable(), + gap: z.enum(["sm", "md", "lg"]).nullable(), + wrap: z.boolean().nullable(), + }), + slots: ["default"], + description: "Flex layout container", + example: { direction: "vertical", gap: "md", wrap: null }, + }, + + Card: { + props: z.object({ + title: z.string().nullable(), + description: z.string().nullable(), + }), + slots: ["default"], + description: "Card container with optional title and description", + example: { title: "Weather", description: "Current conditions" }, + }, + + Grid: { + props: z.object({ + columns: z.enum(["1", "2", "3", "4"]).nullable(), + gap: z.enum(["sm", "md", "lg"]).nullable(), + }), + slots: ["default"], + description: "Responsive grid layout container", + example: { columns: "3", gap: "md" }, + }, + + // Typography + Heading: { + props: z.object({ + text: z.string(), + level: z.enum(["h1", "h2", "h3", "h4"]).nullable(), + }), + description: "Section heading", + example: { text: "Data Explorer", level: "h1" }, + }, + + Text: { + props: z.object({ + content: z.string(), + muted: z.boolean().nullable(), + }), + description: "Text content", + example: { content: "Here is your data overview." }, + }, + + // Data display + Badge: { + props: z.object({ + text: z.string(), + variant: z + .enum(["default", "secondary", "destructive", "outline"]) + .nullable(), + }), + description: "Status badge", + example: { text: "Live", variant: "default" }, + }, + + Alert: { + props: z.object({ + variant: z.enum(["default", "destructive"]).nullable(), + title: z.string(), + description: z.string().nullable(), + }), + description: "Alert or info message", + }, + + Separator: { + props: z.object({}), + description: "Visual divider", + }, + + Metric: { + props: z.object({ + label: z.string(), + value: z.string(), + detail: z.string().nullable(), + trend: z.enum(["up", "down", "neutral"]).nullable(), + }), + description: + "Single metric display with label, value, and optional trend indicator", + example: { + label: "Temperature", + value: "72F", + detail: "Feels like 68F", + trend: "up", + }, + }, + + Table: { + props: z.object({ + data: z.array(z.record(z.string(), z.unknown())), + columns: z.array( + z.object({ + key: z.string(), + label: z.string(), + }), + ), + emptyMessage: z.string().nullable(), + }), + description: + 'Data table. Use { "$state": "/path" } to bind read-only data from state.', + example: { + data: { $state: "/stories" }, + columns: [ + { key: "title", label: "Title" }, + { key: "score", label: "Score" }, + ], + }, + }, + + Link: { + props: z.object({ + text: z.string(), + href: z.string(), + }), + description: "External link that opens in a new tab", + example: { text: "View on GitHub", href: "https://github.com" }, + }, + + // Charts + BarChart: { + props: z.object({ + title: z.string().nullable(), + data: z.array(z.record(z.string(), z.unknown())), + xKey: z.string(), + yKey: z.string(), + aggregate: z.enum(["sum", "count", "avg"]).nullable(), + color: z.string().nullable(), + height: z.number().nullable(), + }), + description: + 'Bar chart visualization. Use { "$state": "/path" } to bind read-only data. xKey is the category field, yKey is the numeric value field.', + }, + + LineChart: { + props: z.object({ + title: z.string().nullable(), + data: z.array(z.record(z.string(), z.unknown())), + xKey: z.string(), + yKey: z.string(), + aggregate: z.enum(["sum", "count", "avg"]).nullable(), + color: z.string().nullable(), + height: z.number().nullable(), + }), + description: + 'Line chart visualization. Use { "$state": "/path" } to bind read-only data. xKey is the x-axis field, yKey is the numeric value field.', + }, + + // Interactive + Tabs: { + props: z.object({ + defaultValue: z.string().nullable(), + tabs: z.array( + z.object({ + value: z.string(), + label: z.string(), + }), + ), + }), + slots: ["default"], + description: "Tabbed content container", + }, + + TabContent: { + props: z.object({ + value: z.string(), + }), + slots: ["default"], + description: "Content for a specific tab", + }, + + Progress: { + props: z.object({ + value: z.number(), + max: z.number().nullable(), + }), + description: "Progress bar", + }, + + Skeleton: { + props: z.object({ + width: z.string().nullable(), + height: z.string().nullable(), + }), + description: "Loading placeholder", + }, + + // Educational / Rich content + Callout: { + props: z.object({ + type: z.enum(["info", "tip", "warning", "important"]).nullable(), + title: z.string().nullable(), + content: z.string(), + }), + description: + "Highlighted callout box for tips, warnings, notes, or key information", + example: { + type: "tip", + title: "Did you know?", + content: "The sun is about 93 million miles from Earth.", + }, + }, + + Accordion: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + content: z.string(), + }), + ), + type: z.enum(["single", "multiple"]).nullable(), + }), + description: + "Collapsible accordion sections for organizing detailed content", + example: { + items: [{ title: "Overview", content: "A brief introduction." }], + type: "multiple", + }, + }, + + Timeline: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + description: z.string().nullable(), + date: z.string().nullable(), + status: z.enum(["completed", "current", "upcoming"]).nullable(), + }), + ), + }), + description: + "Vertical timeline showing ordered events, steps, or historical milestones", + example: { + items: [ + { + title: "Discovery", + description: "Initial breakthrough", + date: "1905", + status: "completed", + }, + ], + }, + }, + + PieChart: { + props: z.object({ + title: z.string().nullable(), + data: z.array(z.record(z.string(), z.unknown())), + nameKey: z.string(), + valueKey: z.string(), + height: z.number().nullable(), + }), + description: + 'Pie/donut chart for proportional data. Use { "$state": "/path" } to bind read-only data. nameKey is the label field, valueKey is the numeric value field.', + }, + + // Interactive / Input + RadioGroup: { + props: z.object({ + label: z.string().nullable(), + value: z.string().nullable(), + options: z.array( + z.object({ + value: z.string(), + label: z.string(), + }), + ), + }), + description: + 'Radio button group for single selection. Use { "$bindState": "/path" } for two-way binding.', + example: { + label: "Choose one", + value: { $bindState: "/answer" }, + options: [ + { value: "a", label: "Option A" }, + { value: "b", label: "Option B" }, + ], + }, + }, + + SelectInput: { + props: z.object({ + label: z.string().nullable(), + value: z.string().nullable(), + placeholder: z.string().nullable(), + options: z.array( + z.object({ + value: z.string(), + label: z.string(), + }), + ), + }), + description: + 'Dropdown select input. Use { "$bindState": "/path" } for two-way binding.', + example: { + label: "Country", + value: { $bindState: "/selectedCountry" }, + placeholder: "Select a country", + options: [ + { value: "us", label: "United States" }, + { value: "uk", label: "United Kingdom" }, + ], + }, + }, + + TextInput: { + props: z.object({ + label: z.string().nullable(), + value: z.string().nullable(), + placeholder: z.string().nullable(), + type: z.enum(["text", "email", "number", "password", "url"]).nullable(), + }), + description: + 'Text input field. Use { "$bindState": "/path" } for two-way binding.', + example: { + label: "Your name", + value: { $bindState: "/userName" }, + placeholder: "Enter your name", + type: "text", + }, + }, + + Button: { + props: z.object({ + label: z.string(), + variant: z + .enum(["default", "secondary", "destructive", "outline", "ghost"]) + .nullable(), + size: z.enum(["default", "sm", "lg"]).nullable(), + disabled: z.boolean().nullable(), + }), + description: + "Clickable button. Use with on.press to trigger actions like setState.", + example: { + label: "Submit", + variant: "default", + size: "default", + disabled: null, + }, + }, + }, + + actions: {}, +}); diff --git a/examples/svelte-chat/src/lib/render/components/Accordion.svelte b/examples/svelte-chat/src/lib/render/components/Accordion.svelte new file mode 100644 index 00000000..18773536 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Accordion.svelte @@ -0,0 +1,22 @@ + + + + {#each props.items ?? [] as item, i} + + {item.title} + +

{item.content}

+
+
+ {/each} +
diff --git a/examples/svelte-chat/src/lib/render/components/Alert.svelte b/examples/svelte-chat/src/lib/render/components/Alert.svelte new file mode 100644 index 00000000..a216747a --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Alert.svelte @@ -0,0 +1,19 @@ + + + + {props.title} + {#if props.description} + {props.description} + {/if} + diff --git a/examples/svelte-chat/src/lib/render/components/Badge.svelte b/examples/svelte-chat/src/lib/render/components/Badge.svelte new file mode 100644 index 00000000..9ee24c46 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Badge.svelte @@ -0,0 +1,13 @@ + + +{props.text} diff --git a/examples/svelte-chat/src/lib/render/components/BarChart.svelte b/examples/svelte-chat/src/lib/render/components/BarChart.svelte new file mode 100644 index 00000000..0b110a90 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/BarChart.svelte @@ -0,0 +1,99 @@ + + +
+ {#if props.title} +

{props.title}

+ {/if} + + {#if chartData.items.length === 0} +
No data available
+ {:else} +
+ {#each chartData.items as item} +
+ {item.label} +
+
+
+ {item.value.toLocaleString()} +
+ {/each} +
+ {/if} +
diff --git a/examples/svelte-chat/src/lib/render/components/Button.svelte b/examples/svelte-chat/src/lib/render/components/Button.svelte new file mode 100644 index 00000000..011bc2ee --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Button.svelte @@ -0,0 +1,22 @@ + + + diff --git a/examples/svelte-chat/src/lib/render/components/Callout.svelte b/examples/svelte-chat/src/lib/render/components/Callout.svelte new file mode 100644 index 00000000..f5da6ad9 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Callout.svelte @@ -0,0 +1,57 @@ + + +
+
+ {#if props.type === "tip"} + + {:else if props.type === "warning"} + + {:else if props.type === "important"} + + {:else} + + {/if} +
+ {#if props.title} +

{props.title}

+ {/if} +

{props.content}

+
+
+
diff --git a/examples/svelte-chat/src/lib/render/components/Card.svelte b/examples/svelte-chat/src/lib/render/components/Card.svelte new file mode 100644 index 00000000..b6e6751e --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Card.svelte @@ -0,0 +1,32 @@ + + + + {#if props.title || props.description} + + {#if props.title} + {props.title} + {/if} + {#if props.description} + {props.description} + {/if} + + {/if} + + {#if children} + {@render children()} + {/if} + + diff --git a/examples/svelte-chat/src/lib/render/components/Grid.svelte b/examples/svelte-chat/src/lib/render/components/Grid.svelte new file mode 100644 index 00000000..d1a95063 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Grid.svelte @@ -0,0 +1,31 @@ + + +
+ {#if children} + {@render children()} + {/if} +
diff --git a/examples/svelte-chat/src/lib/render/components/Heading.svelte b/examples/svelte-chat/src/lib/render/components/Heading.svelte new file mode 100644 index 00000000..01dbd420 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Heading.svelte @@ -0,0 +1,22 @@ + + +{#if level === "h1"} +

{props.text}

+{:else if level === "h2"} +

{props.text}

+{:else if level === "h3"} +

{props.text}

+{:else} +

{props.text}

+{/if} diff --git a/examples/svelte-chat/src/lib/render/components/LineChart.svelte b/examples/svelte-chat/src/lib/render/components/LineChart.svelte new file mode 100644 index 00000000..feb84675 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/LineChart.svelte @@ -0,0 +1,109 @@ + + +
+ {#if props.title} +

{props.title}

+ {/if} + + {#if chartData.length === 0} +
No data available
+ {:else} +
+ + + +
+ {#each chartData.filter((_, i) => i === 0 || i === chartData.length - 1 || i === Math.floor(chartData.length / 2)) as item} + {item.label} + {/each} +
+
+ {/if} +
diff --git a/examples/svelte-chat/src/lib/render/components/Link.svelte b/examples/svelte-chat/src/lib/render/components/Link.svelte new file mode 100644 index 00000000..e8de97cb --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Link.svelte @@ -0,0 +1,19 @@ + + + + {props.text} + diff --git a/examples/svelte-chat/src/lib/render/components/Metric.svelte b/examples/svelte-chat/src/lib/render/components/Metric.svelte new file mode 100644 index 00000000..1a27d1ac --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Metric.svelte @@ -0,0 +1,40 @@ + + +
+

{props.label}

+
+ {props.value} + {#if props.trend} + {#if props.trend === "up"} + + {:else if props.trend === "down"} + + {:else} + + {/if} + {/if} +
+ {#if props.detail} +

{props.detail}

+ {/if} +
diff --git a/examples/svelte-chat/src/lib/render/components/PieChart.svelte b/examples/svelte-chat/src/lib/render/components/PieChart.svelte new file mode 100644 index 00000000..ff2295a2 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/PieChart.svelte @@ -0,0 +1,106 @@ + + +
+ {#if props.title} +

{props.title}

+ {/if} + + {#if items.length === 0} +
No data available
+ {:else} +
+ + {#each segments() as seg} + {#if seg.endAngle - seg.startAngle >= 1} + + {/if} + {/each} + +
+ {#each segments() as seg} +
+ + {seg.name} + {seg.percentage}% +
+ {/each} +
+
+ {/if} +
diff --git a/examples/svelte-chat/src/lib/render/components/Progress.svelte b/examples/svelte-chat/src/lib/render/components/Progress.svelte new file mode 100644 index 00000000..b74fed29 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Progress.svelte @@ -0,0 +1,13 @@ + + + diff --git a/examples/svelte-chat/src/lib/render/components/RadioGroup.svelte b/examples/svelte-chat/src/lib/render/components/RadioGroup.svelte new file mode 100644 index 00000000..b9d4e8f5 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/RadioGroup.svelte @@ -0,0 +1,45 @@ + + +
+ {#if props.label} + + {/if} + + {#each props.options ?? [] as opt} +
+ + +
+ {/each} +
+
diff --git a/examples/svelte-chat/src/lib/render/components/SelectInput.svelte b/examples/svelte-chat/src/lib/render/components/SelectInput.svelte new file mode 100644 index 00000000..443e88df --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/SelectInput.svelte @@ -0,0 +1,56 @@ + + +
+ {#if props.label} + + {/if} + + + {#if selectedOption} + {selectedOption.label} + {:else} + {props.placeholder ?? "Select..."} + {/if} + + + {#each props.options ?? [] as opt} + {opt.label} + {/each} + + +
diff --git a/examples/svelte-chat/src/lib/render/components/Separator.svelte b/examples/svelte-chat/src/lib/render/components/Separator.svelte new file mode 100644 index 00000000..872364b6 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Separator.svelte @@ -0,0 +1,10 @@ + + + diff --git a/examples/svelte-chat/src/lib/render/components/Skeleton.svelte b/examples/svelte-chat/src/lib/render/components/Skeleton.svelte new file mode 100644 index 00000000..71d961e1 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Skeleton.svelte @@ -0,0 +1,14 @@ + + +
diff --git a/examples/svelte-chat/src/lib/render/components/Stack.svelte b/examples/svelte-chat/src/lib/render/components/Stack.svelte new file mode 100644 index 00000000..66c7d613 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Stack.svelte @@ -0,0 +1,28 @@ + + +
+ {#if children} + {@render children()} + {/if} +
diff --git a/examples/svelte-chat/src/lib/render/components/TabContent.svelte b/examples/svelte-chat/src/lib/render/components/TabContent.svelte new file mode 100644 index 00000000..6ec4bcd6 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/TabContent.svelte @@ -0,0 +1,19 @@ + + + + {#if children} + {@render children()} + {/if} + diff --git a/examples/svelte-chat/src/lib/render/components/Table.svelte b/examples/svelte-chat/src/lib/render/components/Table.svelte new file mode 100644 index 00000000..f8677b0a --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Table.svelte @@ -0,0 +1,91 @@ + + +{#if items.length === 0} +
+ {props.emptyMessage ?? "No data"} +
+{:else} + + + + {#each props.columns as col} + + + + {/each} + + + + {#each sorted as item, i} + + {#each props.columns as col} + {String(item[col.key] ?? "")} + {/each} + + {/each} + + +{/if} diff --git a/examples/svelte-chat/src/lib/render/components/Tabs.svelte b/examples/svelte-chat/src/lib/render/components/Tabs.svelte new file mode 100644 index 00000000..de1258d6 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Tabs.svelte @@ -0,0 +1,29 @@ + + + + + {#each props.tabs ?? [] as tab} + {tab.label} + {/each} + + {#if children} + {@render children()} + {/if} + diff --git a/examples/svelte-chat/src/lib/render/components/Text.svelte b/examples/svelte-chat/src/lib/render/components/Text.svelte new file mode 100644 index 00000000..96e4cb97 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Text.svelte @@ -0,0 +1,14 @@ + + +

+ {props.content} +

diff --git a/examples/svelte-chat/src/lib/render/components/TextInput.svelte b/examples/svelte-chat/src/lib/render/components/TextInput.svelte new file mode 100644 index 00000000..c701a722 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/TextInput.svelte @@ -0,0 +1,43 @@ + + +
+ {#if props.label} + + {/if} + +
diff --git a/examples/svelte-chat/src/lib/render/components/Timeline.svelte b/examples/svelte-chat/src/lib/render/components/Timeline.svelte new file mode 100644 index 00000000..0804bef6 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/components/Timeline.svelte @@ -0,0 +1,44 @@ + + +
+
+
+ {#each props.items ?? [] as item} +
+
+
+
+

{item.title}

+ {#if item.date} + + {item.date} + + {/if} +
+ {#if item.description} +

{item.description}

+ {/if} +
+
+ {/each} +
+
diff --git a/examples/svelte-chat/src/lib/render/registry.ts b/examples/svelte-chat/src/lib/render/registry.ts new file mode 100644 index 00000000..7802bb1f --- /dev/null +++ b/examples/svelte-chat/src/lib/render/registry.ts @@ -0,0 +1,61 @@ +import { defineRegistry, type ComponentRegistry } from "@json-render/svelte"; +import { explorerCatalog } from "./catalog"; + +// Import render components +import StackComponent from "./components/Stack.svelte"; +import CardComponent from "./components/Card.svelte"; +import GridComponent from "./components/Grid.svelte"; +import HeadingComponent from "./components/Heading.svelte"; +import TextComponent from "./components/Text.svelte"; +import BadgeComponent from "./components/Badge.svelte"; +import AlertComponent from "./components/Alert.svelte"; +import SeparatorComponent from "./components/Separator.svelte"; +import MetricComponent from "./components/Metric.svelte"; +import TableComponent from "./components/Table.svelte"; +import LinkComponent from "./components/Link.svelte"; +import BarChartComponent from "./components/BarChart.svelte"; +import LineChartComponent from "./components/LineChart.svelte"; +import TabsComponent from "./components/Tabs.svelte"; +import TabContentComponent from "./components/TabContent.svelte"; +import ProgressComponent from "./components/Progress.svelte"; +import SkeletonComponent from "./components/Skeleton.svelte"; +import CalloutComponent from "./components/Callout.svelte"; +import AccordionComponent from "./components/Accordion.svelte"; +import TimelineComponent from "./components/Timeline.svelte"; +import PieChartComponent from "./components/PieChart.svelte"; +import RadioGroupComponent from "./components/RadioGroup.svelte"; +import SelectInputComponent from "./components/SelectInput.svelte"; +import TextInputComponent from "./components/TextInput.svelte"; +import ButtonComponent from "./components/Button.svelte"; + +const components: ComponentRegistry = { + Stack: StackComponent, + Card: CardComponent, + Grid: GridComponent, + Heading: HeadingComponent, + Text: TextComponent, + Badge: BadgeComponent, + Alert: AlertComponent, + Separator: SeparatorComponent, + Metric: MetricComponent, + Table: TableComponent, + Link: LinkComponent, + BarChart: BarChartComponent, + LineChart: LineChartComponent, + Tabs: TabsComponent, + TabContent: TabContentComponent, + Progress: ProgressComponent, + Skeleton: SkeletonComponent, + Callout: CalloutComponent, + Accordion: AccordionComponent, + Timeline: TimelineComponent, + PieChart: PieChartComponent, + RadioGroup: RadioGroupComponent, + SelectInput: SelectInputComponent, + TextInput: TextInputComponent, + Button: ButtonComponent, +}; + +export const { registry } = defineRegistry(explorerCatalog, { + components, +}); diff --git a/examples/svelte-chat/src/lib/tools/crypto.ts b/examples/svelte-chat/src/lib/tools/crypto.ts new file mode 100644 index 00000000..60fd15c3 --- /dev/null +++ b/examples/svelte-chat/src/lib/tools/crypto.ts @@ -0,0 +1,165 @@ +import { tool } from "ai"; +import { z } from "zod"; + +// ============================================================================= +// Helpers +// ============================================================================= + +function handleFetchError(res: Response, coinId: string) { + if (res.status === 404) { + return { error: `Cryptocurrency not found: ${coinId}` }; + } + if (res.status === 429) { + return { error: "CoinGecko rate limit exceeded. Try again in a minute." }; + } + return { error: `Failed to fetch crypto data: ${res.statusText}` }; +} + +function sampleTimeSeries( + prices: [number, number][], + maxPoints: number, +): Array<{ date: string; price: number }> { + const step = Math.max(1, Math.floor(prices.length / maxPoints)); + return prices + .filter((_, i) => i % step === 0) + .map(([timestamp, price]) => ({ + date: new Date(timestamp).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + price: Math.round(price * 100) / 100, + })); +} + +// ============================================================================= +// getCryptoPrice — current market data + 7-day sparkline +// ============================================================================= + +/** + * Get cryptocurrency market data from CoinGecko. + * Free public API, no API key required. + * https://docs.coingecko.com/reference/introduction + */ +export const getCryptoPrice = tool({ + description: + "Get current price, market cap, 24h change, and 7-day sparkline for a cryptocurrency. For longer price history (30d, 90d, 365d), use getCryptoPriceHistory instead.", + inputSchema: z.object({ + coinId: z + .string() + .describe( + "CoinGecko coin ID (e.g., 'bitcoin', 'ethereum', 'solana', 'dogecoin', 'cardano')", + ), + }), + execute: async ({ coinId }) => { + const url = `https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coinId)}?localization=false&tickers=false&community_data=false&developer_data=false&sparkline=true`; + + const res = await fetch(url, { + headers: { Accept: "application/json" }, + }); + + if (!res.ok) return handleFetchError(res, coinId); + + const data = (await res.json()) as { + id: string; + symbol: string; + name: string; + market_data: { + current_price: { usd: number }; + market_cap: { usd: number }; + total_volume: { usd: number }; + price_change_percentage_24h: number; + price_change_percentage_7d: number; + price_change_percentage_30d: number; + high_24h: { usd: number }; + low_24h: { usd: number }; + ath: { usd: number }; + ath_date: { usd: string }; + circulating_supply: number; + total_supply: number | null; + sparkline_7d: { price: number[] }; + }; + market_cap_rank: number; + }; + + const md = data.market_data; + + // Convert sparkline (hourly array) to dated points + const now = Date.now(); + const sparkline = md.sparkline_7d.price; + const step = Math.max(1, Math.floor(sparkline.length / 14)); + const sparklineData = sparkline + .filter((_, i) => i % step === 0) + .map((price, i) => { + const hourIndex = i * step; + const ts = now - (sparkline.length - hourIndex) * 3600_000; + return { + date: new Date(ts).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + price: Math.round(price * 100) / 100, + }; + }); + + return { + id: data.id, + symbol: data.symbol.toUpperCase(), + name: data.name, + rank: data.market_cap_rank, + price: md.current_price.usd, + marketCap: md.market_cap.usd, + volume24h: md.total_volume.usd, + change24h: Math.round(md.price_change_percentage_24h * 100) / 100, + change7d: Math.round(md.price_change_percentage_7d * 100) / 100, + change30d: Math.round(md.price_change_percentage_30d * 100) / 100, + high24h: md.high_24h.usd, + low24h: md.low_24h.usd, + allTimeHigh: md.ath.usd, + allTimeHighDate: md.ath_date.usd, + circulatingSupply: md.circulating_supply, + totalSupply: md.total_supply, + sparkline7d: sparklineData, + }; + }, +}); + +// ============================================================================= +// getCryptoPriceHistory — flexible date range price history +// ============================================================================= + +export const getCryptoPriceHistory = tool({ + description: + "Get historical price data for a cryptocurrency over a specified number of days (e.g., 30, 90, 365). Returns date-labeled data points suitable for charting.", + inputSchema: z.object({ + coinId: z + .string() + .describe("CoinGecko coin ID (e.g., 'bitcoin', 'ethereum', 'solana')"), + days: z + .number() + .int() + .min(1) + .max(365) + .describe("Number of days of history to fetch (e.g., 30, 90, 365)"), + }), + execute: async ({ coinId, days }) => { + const url = `https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coinId)}/market_chart?vs_currency=usd&days=${days}`; + + const res = await fetch(url, { + headers: { Accept: "application/json" }, + }); + + if (!res.ok) return handleFetchError(res, coinId); + + const data = (await res.json()) as { + prices: [number, number][]; + }; + + const priceHistory = sampleTimeSeries(data.prices, 20); + + return { + coinId, + days, + priceHistory, + }; + }, +}); diff --git a/examples/svelte-chat/src/lib/tools/github.ts b/examples/svelte-chat/src/lib/tools/github.ts new file mode 100644 index 00000000..a0d0980e --- /dev/null +++ b/examples/svelte-chat/src/lib/tools/github.ts @@ -0,0 +1,237 @@ +import { tool } from "ai"; +import { z } from "zod"; + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +const ghHeaders = { Accept: "application/vnd.github.v3+json" }; + +function handleGitHubError(res: Response, context: string) { + if (res.status === 404) return { error: `Not found: ${context}` }; + if (res.status === 403) + return { error: "GitHub API rate limit exceeded. Try again later." }; + return { error: `Failed to fetch ${context}: ${res.statusText}` }; +} + +// --------------------------------------------------------------------------- +// getGitHubRepo +// --------------------------------------------------------------------------- + +/** + * Get public GitHub repository information. + * Uses the public GitHub REST API (no auth, 60 req/hr rate limit). + */ +export const getGitHubRepo = tool({ + description: + "Get information about a public GitHub repository including stars, forks, open issues, description, language, and recent activity.", + inputSchema: z.object({ + owner: z.string().describe("Repository owner (e.g., 'vercel')"), + repo: z.string().describe("Repository name (e.g., 'next.js')"), + }), + execute: async ({ owner, repo }) => { + const repoUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; + + const [repoRes, languagesRes] = await Promise.all([ + fetch(repoUrl, { headers: ghHeaders }), + fetch(`${repoUrl}/languages`, { headers: ghHeaders }), + ]); + + if (!repoRes.ok) { + return handleGitHubError(repoRes, `${owner}/${repo}`); + } + + const repoData = (await repoRes.json()) as { + full_name: string; + description: string | null; + html_url: string; + stargazers_count: number; + forks_count: number; + open_issues_count: number; + watchers_count: number; + language: string | null; + license: { spdx_id: string } | null; + created_at: string; + updated_at: string; + pushed_at: string; + topics: string[]; + size: number; + default_branch: string; + archived: boolean; + fork: boolean; + }; + + const languages: Record = languagesRes.ok + ? ((await languagesRes.json()) as Record) + : {}; + + const totalBytes = Object.values(languages).reduce((a, b) => a + b, 0); + const languageBreakdown = Object.entries(languages) + .map(([lang, bytes]) => ({ + language: lang, + percentage: Math.round((bytes / totalBytes) * 100), + bytes, + })) + .sort((a, b) => b.bytes - a.bytes) + .slice(0, 8); + + return { + name: repoData.full_name, + description: repoData.description, + url: repoData.html_url, + stars: repoData.stargazers_count, + forks: repoData.forks_count, + openIssues: repoData.open_issues_count, + watchers: repoData.watchers_count, + primaryLanguage: repoData.language, + license: repoData.license?.spdx_id ?? "None", + createdAt: repoData.created_at, + updatedAt: repoData.updated_at, + lastPush: repoData.pushed_at, + topics: repoData.topics, + defaultBranch: repoData.default_branch, + archived: repoData.archived, + isFork: repoData.fork, + languages: languageBreakdown, + }; + }, +}); + +// --------------------------------------------------------------------------- +// getGitHubPullRequests +// --------------------------------------------------------------------------- + +type GitHubPR = { + number: number; + title: string; + state: string; + html_url: string; + user: { login: string } | null; + created_at: string; + updated_at: string; + merged_at: string | null; + comments: number; + labels: Array<{ name: string }>; + draft: boolean; +}; + +type GitHubPRReview = { + id: number; +}; + +type GitHubPRReaction = { + total_count: number; +}; + +/** + * Get pull requests from a public GitHub repository. + * Supports filtering by state and sorting by various criteria. + * Fetches comment counts and reactions for ranking "most popular" PRs. + */ +export const getGitHubPullRequests = tool({ + description: + "Get pull requests from a public GitHub repository. Returns titles, authors, state, comment counts, and reactions. Use sort='popularity' to find the most discussed / reacted PRs.", + inputSchema: z.object({ + owner: z.string().describe("Repository owner (e.g., 'vercel')"), + repo: z.string().describe("Repository name (e.g., 'next.js')"), + state: z + .enum(["open", "closed", "all"]) + .nullable() + .describe("Filter by state. Defaults to 'open'."), + sort: z + .enum(["created", "updated", "popularity", "long-running"]) + .nullable() + .describe( + "Sort order. 'popularity' sorts by reactions+comments, 'long-running' sorts by age. Defaults to 'created'.", + ), + perPage: z + .number() + .int() + .min(1) + .max(30) + .nullable() + .describe("Number of PRs to return (1-30). Defaults to 10."), + }), + execute: async ({ owner, repo, state, sort, perPage }) => { + const count = perPage ?? 10; + const prState = state ?? "open"; + + // GitHub API sort param: 'popularity' and 'long-running' are API-native + const apiSort = + sort === "popularity" + ? "popularity" + : sort === "long-running" + ? "long-running" + : sort === "updated" + ? "updated" + : "created"; + + const url = new URL( + `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, + ); + url.searchParams.set("state", prState); + url.searchParams.set("sort", apiSort); + url.searchParams.set("direction", "desc"); + url.searchParams.set("per_page", String(count)); + + const res = await fetch(url.toString(), { headers: ghHeaders }); + + if (!res.ok) { + return handleGitHubError(res, `${owner}/${repo} pull requests`); + } + + const prs = (await res.json()) as GitHubPR[]; + + // Fetch review + reaction counts in parallel for richer data + const enriched = await Promise.all( + prs.map(async (pr) => { + const base = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${pr.number}`; + + const [reviewsRes, reactionsRes] = await Promise.all([ + fetch(`${base}/reviews?per_page=100`, { headers: ghHeaders }), + fetch( + `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${pr.number}/reactions`, + { + headers: { + ...ghHeaders, + Accept: "application/vnd.github.squirrel-girl-preview+json", + }, + }, + ), + ]); + + const reviews: GitHubPRReview[] = reviewsRes.ok + ? ((await reviewsRes.json()) as GitHubPRReview[]) + : []; + + let reactionCount = 0; + if (reactionsRes.ok) { + const reactions = (await reactionsRes.json()) as GitHubPRReaction[]; + reactionCount = reactions.length; + } + + return { + number: pr.number, + title: pr.title, + state: pr.merged_at ? "merged" : pr.state, + author: pr.user?.login ?? "unknown", + url: pr.html_url, + createdAt: pr.created_at, + updatedAt: pr.updated_at, + comments: pr.comments, + reviews: reviews.length, + reactions: reactionCount, + labels: pr.labels.map((l) => l.name), + draft: pr.draft, + }; + }), + ); + + return { + repository: `${owner}/${repo}`, + state: prState, + count: enriched.length, + pullRequests: enriched, + }; + }, +}); diff --git a/examples/svelte-chat/src/lib/tools/hackernews.ts b/examples/svelte-chat/src/lib/tools/hackernews.ts new file mode 100644 index 00000000..e6562251 --- /dev/null +++ b/examples/svelte-chat/src/lib/tools/hackernews.ts @@ -0,0 +1,67 @@ +import { tool } from "ai"; +import { z } from "zod"; + +/** + * Get top stories from Hacker News. + * Uses the official HN Firebase API. Free, no auth required. + * https://github.com/HackerNewsAPI/API + */ +export const getHackerNewsTop = tool({ + description: + "Get the current top stories from Hacker News, including title, score, author, URL, and comment count.", + inputSchema: z.object({ + count: z + .number() + .min(1) + .max(30) + .describe("Number of top stories to fetch (1-30)"), + }), + execute: async ({ count }) => { + const topUrl = + "https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty"; + const topRes = await fetch(topUrl); + + if (!topRes.ok) { + return { error: "Failed to fetch Hacker News top stories" }; + } + + const topIds = (await topRes.json()) as number[]; + const storyIds = topIds.slice(0, count); + + const stories = await Promise.all( + storyIds.map(async (id) => { + const storyRes = await fetch( + `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`, + ); + if (!storyRes.ok) return null; + + const story = (await storyRes.json()) as { + id: number; + title: string; + url?: string; + score: number; + by: string; + time: number; + descendants?: number; + type: string; + }; + + return { + id: story.id, + title: story.title, + url: story.url ?? `https://news.ycombinator.com/item?id=${story.id}`, + score: story.score, + author: story.by, + comments: story.descendants ?? 0, + postedAt: new Date(story.time * 1000).toISOString(), + hnUrl: `https://news.ycombinator.com/item?id=${story.id}`, + }; + }), + ); + + return { + stories: stories.filter(Boolean), + fetchedAt: new Date().toISOString(), + }; + }, +}); diff --git a/examples/svelte-chat/src/lib/tools/search.ts b/examples/svelte-chat/src/lib/tools/search.ts new file mode 100644 index 00000000..9ff7105b --- /dev/null +++ b/examples/svelte-chat/src/lib/tools/search.ts @@ -0,0 +1,36 @@ +import { tool, generateText } from "ai"; +import { gateway } from "@ai-sdk/gateway"; +import { z } from "zod"; + +/** + * Web search tool using Perplexity Sonar via AI Gateway. + * + * Perplexity Sonar models have built-in internet access and return + * synthesized answers with citations. This is wrapped as a regular tool + * (with an `execute` function) so that ToolLoopAgent can loop: it calls + * the model, gets results, and feeds them back for the next step. + */ +export const webSearch = tool({ + description: + "Search the web for current information on any topic. Use this when the user asks about something not covered by the specialized tools (weather, crypto, GitHub, Hacker News). Returns a synthesized answer based on real-time web data.", + inputSchema: z.object({ + query: z + .string() + .describe( + "The search query — be specific and include relevant context for better results", + ), + }), + execute: async ({ query }) => { + try { + const { text } = await generateText({ + model: gateway("perplexity/sonar"), + prompt: query, + }); + return { content: text }; + } catch (error) { + return { + error: `Search failed: ${error instanceof Error ? error.message : "Unknown error"}`, + }; + } + }, +}); diff --git a/examples/svelte-chat/src/lib/tools/weather.ts b/examples/svelte-chat/src/lib/tools/weather.ts new file mode 100644 index 00000000..b9d90f8d --- /dev/null +++ b/examples/svelte-chat/src/lib/tools/weather.ts @@ -0,0 +1,126 @@ +import { tool } from "ai"; +import { z } from "zod"; + +/** + * Get current weather and 7-day forecast for a city using Open-Meteo API. + * Free, no API key required. + * https://open-meteo.com/ + */ +export const getWeather = tool({ + description: + "Get current weather conditions and a 7-day forecast for a given city. Returns temperature, humidity, wind speed, weather conditions, and daily forecasts.", + inputSchema: z.object({ + city: z + .string() + .describe("City name (e.g., 'New York', 'London', 'Tokyo')"), + }), + execute: async ({ city }) => { + // Step 1: Geocode the city name to coordinates + const geocodeUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json`; + const geocodeRes = await fetch(geocodeUrl); + + if (!geocodeRes.ok) { + return { error: `Failed to geocode city: ${city}` }; + } + + const geocodeData = (await geocodeRes.json()) as { + results?: Array<{ + name: string; + country: string; + latitude: number; + longitude: number; + timezone: string; + }>; + }; + + if (!geocodeData.results || geocodeData.results.length === 0) { + return { error: `City not found: ${city}` }; + } + + const location = geocodeData.results[0]!; + + // Step 2: Get weather data + const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=${encodeURIComponent(location.timezone)}&forecast_days=7`; + + const weatherRes = await fetch(weatherUrl); + + if (!weatherRes.ok) { + return { error: "Failed to fetch weather data" }; + } + + const weather = (await weatherRes.json()) as { + current: { + temperature_2m: number; + relative_humidity_2m: number; + apparent_temperature: number; + weather_code: number; + wind_speed_10m: number; + }; + daily: { + time: string[]; + weather_code: number[]; + temperature_2m_max: number[]; + temperature_2m_min: number[]; + precipitation_sum: number[]; + }; + }; + + const weatherDescription = describeWeatherCode( + weather.current.weather_code, + ); + + const forecast = weather.daily.time.map((date, i) => ({ + date, + day: new Date(date + "T12:00:00").toLocaleDateString("en-US", { + weekday: "short", + }), + high: Math.round(weather.daily.temperature_2m_max[i]!), + low: Math.round(weather.daily.temperature_2m_min[i]!), + condition: describeWeatherCode(weather.daily.weather_code[i]!), + precipitation: weather.daily.precipitation_sum[i]!, + })); + + return { + city: location.name, + country: location.country, + current: { + temperature: Math.round(weather.current.temperature_2m), + feelsLike: Math.round(weather.current.apparent_temperature), + humidity: weather.current.relative_humidity_2m, + windSpeed: Math.round(weather.current.wind_speed_10m), + condition: weatherDescription, + }, + forecast, + }; + }, +}); + +function describeWeatherCode(code: number): string { + const descriptions: Record = { + 0: "Clear sky", + 1: "Mainly clear", + 2: "Partly cloudy", + 3: "Overcast", + 45: "Foggy", + 48: "Depositing rime fog", + 51: "Light drizzle", + 53: "Moderate drizzle", + 55: "Dense drizzle", + 61: "Slight rain", + 63: "Moderate rain", + 65: "Heavy rain", + 71: "Slight snow", + 73: "Moderate snow", + 75: "Heavy snow", + 77: "Snow grains", + 80: "Slight rain showers", + 81: "Moderate rain showers", + 82: "Violent rain showers", + 85: "Slight snow showers", + 86: "Heavy snow showers", + 95: "Thunderstorm", + 96: "Thunderstorm with slight hail", + 99: "Thunderstorm with heavy hail", + }; + return descriptions[code] ?? "Unknown"; +} diff --git a/examples/svelte-chat/src/lib/utils.ts b/examples/svelte-chat/src/lib/utils.ts new file mode 100644 index 00000000..a091ef10 --- /dev/null +++ b/examples/svelte-chat/src/lib/utils.ts @@ -0,0 +1,18 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import type { Snippet } from "svelte"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// Utility types for shadcn-svelte components +export type WithElementRef = T & { + ref?: E | null; +}; + +// WithoutChild should only omit "child" (singular), not "children" +// children is the Svelte 5 snippet pattern, which is still used +export type WithoutChild = Omit & { children?: Snippet }; + +export type WithoutChildrenOrChild = Omit; diff --git a/examples/svelte-chat/src/routes/+layout.svelte b/examples/svelte-chat/src/routes/+layout.svelte new file mode 100644 index 00000000..bfd3ebdd --- /dev/null +++ b/examples/svelte-chat/src/routes/+layout.svelte @@ -0,0 +1,13 @@ + + + + + json-render Svelte Chat + + +{@render children()} diff --git a/examples/svelte-chat/src/routes/+page.svelte b/examples/svelte-chat/src/routes/+page.svelte new file mode 100644 index 00000000..efbfe801 --- /dev/null +++ b/examples/svelte-chat/src/routes/+page.svelte @@ -0,0 +1,381 @@ + + +
+ +
+
+

json-render Svelte Chat

+
+
+ {#if chat.messages.length > 0} + + {/if} +
+
+ + +
+ {#if isEmpty} + +
+
+
+

+ What would you like to explore? +

+

+ Ask about weather, GitHub repos, crypto prices, or Hacker News -- + the agent will fetch real data and build a dashboard. +

+
+ + +
+ {#each SUGGESTIONS as s} + + {/each} +
+
+
+ {:else} + +
+ {#each chat.messages as message, index} + {@const isLast = index === chat.messages.length - 1} + {@const parts = message.parts as DataPart[]} + {@const spec = getSpec(parts)} + {@const text = getText(parts)} + {@const messageHasSpec = hasSpec(parts)} + {@const { segments, specInserted } = getSegments(parts)} + + {#if message.role === "user"} + +
+ {#if text} +
+ {text} +
+ {/if} +
+ {:else} + + {@const hasAnything = segments.length > 0 || messageHasSpec} + {@const showLoader = isLast && isStreaming && !hasAnything} + {@const showSpecAtEnd = messageHasSpec && !specInserted} + +
+ {#each segments as seg, i} + {#if seg.kind === "text"} +
+ {seg.text} +
+ {:else if seg.kind === "spec"} + {#if spec} +
+ +
+ {/if} + {:else if seg.kind === "tools"} +
+ {#each seg.tools as t} + {@const toolIsLoading = + t.state !== "output-available" && + t.state !== "output-error" && + t.state !== "output-denied"} + {@const labels = TOOL_LABELS[t.toolName]} + {@const label = labels + ? toolIsLoading + ? labels[0] + : labels[1] + : t.toolName} + +
+ + {label} + +
+ {/each} +
+ {/if} + {/each} + + + {#if showLoader} +
+ Thinking... +
+ {/if} + + + {#if showSpecAtEnd && spec} +
+ +
+ {/if} +
+ {/if} + {/each} + + + {#if chat.error} +
+ {chat.error.message} +
+ {/if} +
+ {/if} +
+ + +
+ + {#if showScrollButton && !isEmpty} + + {/if} + +
+ + +
+
+
diff --git a/examples/svelte-chat/src/routes/api/generate/+server.ts b/examples/svelte-chat/src/routes/api/generate/+server.ts new file mode 100644 index 00000000..bfbf51c4 --- /dev/null +++ b/examples/svelte-chat/src/routes/api/generate/+server.ts @@ -0,0 +1,35 @@ +import { agent } from "$lib/agent"; +import { + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, + type UIMessage, +} from "ai"; +import { pipeJsonRender } from "@json-render/core"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request }) => { + const body = await request.json(); + const uiMessages: UIMessage[] = body.messages; + + if (!uiMessages || !Array.isArray(uiMessages) || uiMessages.length === 0) { + return new Response( + JSON.stringify({ error: "messages array is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const modelMessages = await convertToModelMessages(uiMessages); + const result = await agent.stream({ messages: modelMessages }); + + const stream = createUIMessageStream({ + execute: async ({ writer }) => { + writer.merge(pipeJsonRender(result.toUIMessageStream())); + }, + }); + + return createUIMessageStreamResponse({ stream }); +}; diff --git a/examples/svelte-chat/static/robots.txt b/examples/svelte-chat/static/robots.txt new file mode 100644 index 00000000..b6dd6670 --- /dev/null +++ b/examples/svelte-chat/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/examples/svelte-chat/svelte.config.js b/examples/svelte-chat/svelte.config.js new file mode 100644 index 00000000..1cc76be9 --- /dev/null +++ b/examples/svelte-chat/svelte.config.js @@ -0,0 +1,10 @@ +import adapter from "@sveltejs/adapter-auto"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter(), + }, +}; + +export default config; diff --git a/examples/svelte-chat/tsconfig.json b/examples/svelte-chat/tsconfig.json new file mode 100644 index 00000000..2c2ed3c4 --- /dev/null +++ b/examples/svelte-chat/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/examples/svelte-chat/vite.config.ts b/examples/svelte-chat/vite.config.ts new file mode 100644 index 00000000..0d65de7a --- /dev/null +++ b/examples/svelte-chat/vite.config.ts @@ -0,0 +1,10 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [sveltekit(), tailwindcss()], + optimizeDeps: { + exclude: ["@json-render/svelte"], + }, +}); diff --git a/examples/svelte/index.html b/examples/svelte/index.html new file mode 100644 index 00000000..1e0ab775 --- /dev/null +++ b/examples/svelte/index.html @@ -0,0 +1,12 @@ + + + + + + json-render svelte example + + +
+ + + diff --git a/examples/svelte/package.json b/examples/svelte/package.json new file mode 100644 index 00000000..21eceb71 --- /dev/null +++ b/examples/svelte/package.json @@ -0,0 +1,24 @@ +{ + "name": "example-svelte", + "version": "0.1.1", + "private": true, + "type": "module", + "scripts": { + "dev": "portless svelte.json-render vite", + "build": "vite build", + "preview": "vite preview", + "check-types": "svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@json-render/core": "workspace:*", + "@json-render/svelte": "workspace:*", + "svelte": "^5.49.2", + "zod": "4.3.6" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "svelte-check": "^4.3.6", + "typescript": "^5.9.3", + "vite": "^7.3.1" + } +} diff --git a/examples/svelte/src/App.svelte b/examples/svelte/src/App.svelte new file mode 100644 index 00000000..15a5e636 --- /dev/null +++ b/examples/svelte/src/App.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/examples/svelte/src/DemoRenderer.svelte b/examples/svelte/src/DemoRenderer.svelte new file mode 100644 index 00000000..ed06fb12 --- /dev/null +++ b/examples/svelte/src/DemoRenderer.svelte @@ -0,0 +1,46 @@ + + + + + + + + + diff --git a/examples/svelte/src/app.css b/examples/svelte/src/app.css new file mode 100644 index 00000000..74138d82 --- /dev/null +++ b/examples/svelte/src/app.css @@ -0,0 +1,13 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: #f9fafb; + color: #111827; + min-height: 100vh; +} diff --git a/examples/svelte/src/lib/catalog.ts b/examples/svelte/src/lib/catalog.ts new file mode 100644 index 00000000..1c5c959b --- /dev/null +++ b/examples/svelte/src/lib/catalog.ts @@ -0,0 +1,90 @@ +import { schema } from "@json-render/svelte/schema"; +import { z } from "zod"; + +export const catalog = schema.createCatalog({ + components: { + Stack: { + props: z.object({ + gap: z.number().optional(), + padding: z.number().optional(), + direction: z.enum(["vertical", "horizontal"]).optional(), + align: z.enum(["start", "center", "end"]).optional(), + }), + slots: ["default"], + description: + "Layout container that stacks children vertically or horizontally", + }, + Card: { + props: z.object({ + title: z.string().optional(), + subtitle: z.string().optional(), + }), + slots: ["default"], + description: "A card container with optional title and subtitle", + }, + Text: { + props: z.object({ + content: z.string(), + size: z.enum(["sm", "md", "lg", "xl"]).optional(), + weight: z.enum(["normal", "medium", "bold"]).optional(), + color: z.string().optional(), + }), + slots: [], + description: "Displays a text string", + }, + Button: { + props: z.object({ + label: z.string(), + variant: z.enum(["primary", "secondary", "danger"]).optional(), + disabled: z.boolean().optional(), + }), + slots: [], + description: "A clickable button that emits a 'press' event", + }, + Badge: { + props: z.object({ + label: z.string(), + color: z.string().optional(), + }), + slots: [], + description: "A small badge/tag label", + }, + ListItem: { + props: z.object({ + title: z.string(), + description: z.string().optional(), + completed: z.boolean().optional(), + }), + slots: [], + description: "A single item in a list", + }, + Input: { + props: z.object({ + value: z.string().optional(), + placeholder: z.string().optional(), + }), + slots: [], + description: "A text input field that supports two-way state binding", + }, + }, + actions: { + increment: { + params: z.object({}), + description: "Increment the counter by 1", + }, + decrement: { + params: z.object({}), + description: "Decrement the counter by 1", + }, + reset: { + params: z.object({}), + description: "Reset the counter to 0", + }, + toggleItem: { + params: z.object({ + index: z.number(), + }), + description: "Toggle the completed state of a todo item", + }, + }, +}); diff --git a/examples/svelte/src/lib/components/Badge.svelte b/examples/svelte/src/lib/components/Badge.svelte new file mode 100644 index 00000000..a1169973 --- /dev/null +++ b/examples/svelte/src/lib/components/Badge.svelte @@ -0,0 +1,24 @@ + + + + {props.label} + diff --git a/examples/svelte/src/lib/components/Button.svelte b/examples/svelte/src/lib/components/Button.svelte new file mode 100644 index 00000000..0e49ce74 --- /dev/null +++ b/examples/svelte/src/lib/components/Button.svelte @@ -0,0 +1,44 @@ + + + diff --git a/examples/svelte/src/lib/components/Card.svelte b/examples/svelte/src/lib/components/Card.svelte new file mode 100644 index 00000000..6bb68cad --- /dev/null +++ b/examples/svelte/src/lib/components/Card.svelte @@ -0,0 +1,47 @@ + + +
+ {#if props.title} +

+ {props.title} +

+ {/if} + {#if props.subtitle} +

+ {props.subtitle} +

+ {/if} + {#if children} + {@render children()} + {/if} +
diff --git a/examples/svelte/src/lib/components/Input.svelte b/examples/svelte/src/lib/components/Input.svelte new file mode 100644 index 00000000..59c4941a --- /dev/null +++ b/examples/svelte/src/lib/components/Input.svelte @@ -0,0 +1,29 @@ + + + diff --git a/examples/svelte/src/lib/components/ListItem.svelte b/examples/svelte/src/lib/components/ListItem.svelte new file mode 100644 index 00000000..82fd30ae --- /dev/null +++ b/examples/svelte/src/lib/components/ListItem.svelte @@ -0,0 +1,49 @@ + + + diff --git a/examples/svelte/src/lib/components/Stack.svelte b/examples/svelte/src/lib/components/Stack.svelte new file mode 100644 index 00000000..ffe8fd7c --- /dev/null +++ b/examples/svelte/src/lib/components/Stack.svelte @@ -0,0 +1,35 @@ + + +
+ {#if children} + {@render children()} + {/if} +
diff --git a/examples/svelte/src/lib/components/Text.svelte b/examples/svelte/src/lib/components/Text.svelte new file mode 100644 index 00000000..ff5fab92 --- /dev/null +++ b/examples/svelte/src/lib/components/Text.svelte @@ -0,0 +1,34 @@ + + + + {String(props.content ?? "")} + diff --git a/examples/svelte/src/lib/registry.ts b/examples/svelte/src/lib/registry.ts new file mode 100644 index 00000000..6d75653e --- /dev/null +++ b/examples/svelte/src/lib/registry.ts @@ -0,0 +1,22 @@ +import { type Components, defineRegistry } from "@json-render/svelte"; +import { catalog } from "./catalog"; + +import Stack from "./components/Stack.svelte"; +import Card from "./components/Card.svelte"; +import Text from "./components/Text.svelte"; +import Button from "./components/Button.svelte"; +import Badge from "./components/Badge.svelte"; +import ListItem from "./components/ListItem.svelte"; +import Input from "./components/Input.svelte"; + +const components: Components = { + Stack, + Card, + Text, + Button, + Badge, + ListItem, + Input, +}; + +export const { registry } = defineRegistry(catalog, { components }); diff --git a/examples/svelte/src/lib/spec.ts b/examples/svelte/src/lib/spec.ts new file mode 100644 index 00000000..925b1ee0 --- /dev/null +++ b/examples/svelte/src/lib/spec.ts @@ -0,0 +1,130 @@ +import type { Spec } from "@json-render/core"; + +export const demoSpec: Spec = { + root: "root", + state: { + count: 0, + name: "", + todos: [ + { id: 1, title: "Learn Svelte 5", completed: true }, + { id: 2, title: "Try @json-render/svelte", completed: false }, + { id: 3, title: "Build something awesome", completed: false }, + ], + }, + elements: { + root: { + type: "Stack", + props: { gap: 24, padding: 24, direction: "vertical" }, + children: [ + "header", + "counter-card", + "milestone-badge", + "todos-card", + "input-card", + ], + }, + header: { + type: "Text", + props: { + content: "@json-render/svelte demo", + size: "xl", + weight: "bold", + }, + }, + "counter-card": { + type: "Card", + props: { + title: "Counter", + subtitle: "Click the buttons to change the count", + }, + children: ["counter-body"], + }, + "counter-body": { + type: "Stack", + props: { gap: 12, direction: "horizontal", align: "center" }, + children: [ + "decrement-btn", + "counter-value", + "increment-btn", + "reset-btn", + ], + }, + "decrement-btn": { + type: "Button", + props: { label: "−", variant: "secondary" }, + on: { press: { action: "decrement" } }, + }, + "counter-value": { + type: "Text", + props: { + content: { $state: "/count" }, + size: "xl", + weight: "bold", + }, + }, + "increment-btn": { + type: "Button", + props: { label: "+", variant: "primary" }, + on: { press: { action: "increment" } }, + }, + "reset-btn": { + type: "Button", + props: { label: "Reset", variant: "danger" }, + on: { press: { action: "reset" } }, + }, + "milestone-badge": { + type: "Badge", + props: { label: "Milestone reached: 10!", color: "#10b981" }, + visible: { $state: "/count", gte: 10 }, + }, + "todos-card": { + type: "Card", + props: { title: "Todo List", subtitle: "Your tasks" }, + children: ["todos-list"], + }, + "todos-list": { + type: "Stack", + props: { gap: 8, direction: "vertical" }, + repeat: { statePath: "/todos", key: "id" }, + children: ["todo-item"], + }, + "todo-item": { + type: "ListItem", + props: { + title: { $item: "title" }, + completed: { $item: "completed" }, + }, + on: { + press: { action: "toggleItem", params: { index: { $index: true } } }, + }, + }, + "input-card": { + type: "Card", + props: { + title: "Bound Input", + subtitle: "Type to update state and see reactive text", + }, + children: ["input-body"], + }, + "input-body": { + type: "Stack", + props: { gap: 12, direction: "vertical" }, + children: ["name-input", "name-display"], + }, + "name-input": { + type: "Input", + props: { + value: { $bindState: "/name" }, + placeholder: "Enter your name…", + }, + }, + "name-display": { + type: "Text", + props: { + content: { $state: "/name" }, + size: "md", + color: "#6b7280", + }, + }, + }, +}; diff --git a/examples/svelte/src/main.ts b/examples/svelte/src/main.ts new file mode 100644 index 00000000..817f168d --- /dev/null +++ b/examples/svelte/src/main.ts @@ -0,0 +1,9 @@ +import { mount } from "svelte"; +import App from "./App.svelte"; +import "./app.css"; + +const app = mount(App, { + target: document.getElementById("app")!, +}); + +export default app; diff --git a/examples/svelte/src/vite-env.d.ts b/examples/svelte/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/svelte/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/svelte/svelte.config.js b/examples/svelte/svelte.config.js new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/examples/svelte/svelte.config.js @@ -0,0 +1 @@ +export default {}; diff --git a/examples/svelte/tsconfig.json b/examples/svelte/tsconfig.json new file mode 100644 index 00000000..85649d6d --- /dev/null +++ b/examples/svelte/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "skipLibCheck": true + }, + "files": ["src/DemoRenderer.svelte"], + "exclude": ["node_modules"] + // "include": ["src/**/*.ts", "src/**/*.svelte"] +} diff --git a/examples/svelte/vite.config.ts b/examples/svelte/vite.config.ts new file mode 100644 index 00000000..8a6f4b5b --- /dev/null +++ b/examples/svelte/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +export default defineConfig({ + plugins: [svelte()], +}); diff --git a/examples/vite-renderers/package.json b/examples/vite-renderers/package.json index eafe7608..cc47867b 100644 --- a/examples/vite-renderers/package.json +++ b/examples/vite-renderers/package.json @@ -2,6 +2,7 @@ "name": "vite-renderers", "version": "0.1.2", "private": true, + "type": "module", "scripts": { "predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)", "dev": "portless vite-renderers.json-render vite", @@ -11,13 +12,16 @@ "dependencies": { "@json-render/core": "workspace:*", "@json-render/react": "workspace:*", + "@json-render/svelte": "workspace:*", "@json-render/vue": "workspace:*", "react": "^19.2.4", "react-dom": "^19.2.4", + "svelte": "^5.49.2", "vue": "^3.5.29", - "zod": "^4.3.6" + "zod": "4.3.6" }, "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", diff --git a/examples/vite-renderers/src/main.ts b/examples/vite-renderers/src/main.ts index 70ae8909..d764e2ec 100644 --- a/examples/vite-renderers/src/main.ts +++ b/examples/vite-renderers/src/main.ts @@ -1,7 +1,7 @@ import "./shared/styles.css"; import { demoSpec } from "./spec"; -type Renderer = "vue" | "react"; +type Renderer = "vue" | "react" | "svelte"; const container = document.getElementById("renderer-root") as HTMLElement; @@ -14,10 +14,14 @@ async function switchTo(renderer: Renderer) { const mod = await import("./vue/mount.ts"); mod.mount(container, renderer, demoSpec); unmountCurrent = mod.unmount; - } else { + } else if (renderer === "react") { const mod = await import("./react/mount.tsx"); mod.mount(container, renderer, demoSpec); unmountCurrent = mod.unmount; + } else { + const mod = await import("./svelte/mount.ts"); + mod.mount(container, renderer, demoSpec); + unmountCurrent = mod.unmount; } } diff --git a/examples/vite-renderers/src/react/registry.tsx b/examples/vite-renderers/src/react/registry.tsx index 14ab5b77..378deb24 100644 --- a/examples/vite-renderers/src/react/registry.tsx +++ b/examples/vite-renderers/src/react/registry.tsx @@ -119,7 +119,11 @@ export const components: Components = { RendererBadge: ({ props }) => ( - {props.renderer === "vue" ? "Rendered with Vue" : "Rendered with React"} + {props.renderer === "vue" + ? "Rendered with Vue" + : props.renderer === "react" + ? "Rendered with React" + : "Rendered with Svelte"} ), @@ -149,6 +153,17 @@ export const components: Components = { > React + ), diff --git a/examples/vite-renderers/src/shared/catalog-def.ts b/examples/vite-renderers/src/shared/catalog-def.ts index e18bd487..d271f5e8 100644 --- a/examples/vite-renderers/src/shared/catalog-def.ts +++ b/examples/vite-renderers/src/shared/catalog-def.ts @@ -65,7 +65,7 @@ export const catalogDef = { props: z.object({ renderer: z.string() }), slots: [], description: - "Segmented tab control for switching between Vue and React renderers", + "Segmented tab control for switching between Vue, React, and Svelte renderers", }, RendererBadge: { props: z.object({ renderer: z.string() }), @@ -95,5 +95,9 @@ export const catalogDef = { params: z.object({}), description: "Switch to the React renderer", }, + switchToSvelte: { + params: z.object({}), + description: "Switch to the Svelte renderer", + }, }, }; diff --git a/examples/vite-renderers/src/shared/handlers.ts b/examples/vite-renderers/src/shared/handlers.ts index 5f83d29a..8bdc0149 100644 --- a/examples/vite-renderers/src/shared/handlers.ts +++ b/examples/vite-renderers/src/shared/handlers.ts @@ -9,6 +9,7 @@ export const actionStubs = { toggleItem: async () => {}, switchToVue: async () => {}, switchToReact: async () => {}, + switchToSvelte: async () => {}, }; /** Creates action handlers that close over the state store's get/set */ @@ -46,5 +47,10 @@ export function makeHandlers(get: Get, set: Set) { new CustomEvent("switch-renderer", { detail: "react" }), ); }, + switchToSvelte: async () => { + document.dispatchEvent( + new CustomEvent("switch-renderer", { detail: "svelte" }), + ); + }, }; } diff --git a/examples/vite-renderers/src/shared/styles.css b/examples/vite-renderers/src/shared/styles.css index c97ab6cb..414d11fb 100644 --- a/examples/vite-renderers/src/shared/styles.css +++ b/examples/vite-renderers/src/shared/styles.css @@ -246,3 +246,18 @@ background-color: #149eca; color: white; } + +.renderer-svelte .json-render-renderer-badge { + color: #ff3e00; + background-color: #ff3e0018; + border-color: #ff3e0040; +} + +.renderer-svelte .json-render-renderer-dot { + background-color: #ff3e00; +} + +.renderer-svelte .json-render-renderer-tab--active { + background-color: #ff3e00; + color: white; +} diff --git a/examples/vite-renderers/src/spec.ts b/examples/vite-renderers/src/spec.ts index e318143b..9b88256e 100644 --- a/examples/vite-renderers/src/spec.ts +++ b/examples/vite-renderers/src/spec.ts @@ -9,7 +9,8 @@ export const demoSpec: Spec = { { id: 1, title: "Learn JSON Render", completed: true }, { id: 2, - title: "Try @json-render/vue or @json-render/react", + title: + "Try @json-render/vue, @json-render/react, and @json-render/svelte", completed: false, }, { id: 3, title: "Build something awesome", completed: false }, @@ -47,6 +48,7 @@ export const demoSpec: Spec = { on: { pressVue: { action: "switchToVue" }, pressReact: { action: "switchToReact" }, + pressSvelte: { action: "switchToSvelte" }, }, }, diff --git a/examples/vite-renderers/src/svelte/App.svelte b/examples/vite-renderers/src/svelte/App.svelte new file mode 100644 index 00000000..05e7b075 --- /dev/null +++ b/examples/vite-renderers/src/svelte/App.svelte @@ -0,0 +1,22 @@ + + +
+ + + +
diff --git a/examples/vite-renderers/src/svelte/DemoRenderer.svelte b/examples/vite-renderers/src/svelte/DemoRenderer.svelte new file mode 100644 index 00000000..32006ae4 --- /dev/null +++ b/examples/vite-renderers/src/svelte/DemoRenderer.svelte @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/examples/vite-renderers/src/svelte/catalog.ts b/examples/vite-renderers/src/svelte/catalog.ts new file mode 100644 index 00000000..4ab7b8a9 --- /dev/null +++ b/examples/vite-renderers/src/svelte/catalog.ts @@ -0,0 +1,4 @@ +import { schema } from "@json-render/svelte/schema"; +import { catalogDef } from "../shared/catalog-def"; + +export const catalog = schema.createCatalog(catalogDef); diff --git a/examples/vite-renderers/src/svelte/components/Badge.svelte b/examples/vite-renderers/src/svelte/components/Badge.svelte new file mode 100644 index 00000000..a1169973 --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/Badge.svelte @@ -0,0 +1,24 @@ + + + + {props.label} + diff --git a/examples/vite-renderers/src/svelte/components/Button.svelte b/examples/vite-renderers/src/svelte/components/Button.svelte new file mode 100644 index 00000000..0e49ce74 --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/Button.svelte @@ -0,0 +1,44 @@ + + + diff --git a/examples/vite-renderers/src/svelte/components/Card.svelte b/examples/vite-renderers/src/svelte/components/Card.svelte new file mode 100644 index 00000000..6bb68cad --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/Card.svelte @@ -0,0 +1,47 @@ + + +
+ {#if props.title} +

+ {props.title} +

+ {/if} + {#if props.subtitle} +

+ {props.subtitle} +

+ {/if} + {#if children} + {@render children()} + {/if} +
diff --git a/examples/vite-renderers/src/svelte/components/Input.svelte b/examples/vite-renderers/src/svelte/components/Input.svelte new file mode 100644 index 00000000..59c4941a --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/Input.svelte @@ -0,0 +1,29 @@ + + + diff --git a/examples/vite-renderers/src/svelte/components/ListItem.svelte b/examples/vite-renderers/src/svelte/components/ListItem.svelte new file mode 100644 index 00000000..82fd30ae --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/ListItem.svelte @@ -0,0 +1,49 @@ + + + diff --git a/examples/vite-renderers/src/svelte/components/RendererBadge.svelte b/examples/vite-renderers/src/svelte/components/RendererBadge.svelte new file mode 100644 index 00000000..4a35f847 --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/RendererBadge.svelte @@ -0,0 +1,16 @@ + + + + + {props.renderer === "vue" + ? "Rendered with Vue" + : props.renderer === "react" + ? "Rendered with React" + : "Rendered with Svelte"} + diff --git a/examples/vite-renderers/src/svelte/components/RendererTabs.svelte b/examples/vite-renderers/src/svelte/components/RendererTabs.svelte new file mode 100644 index 00000000..2d81d66d --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/RendererTabs.svelte @@ -0,0 +1,46 @@ + + +
+ Render +
+ + + +
+
diff --git a/examples/vite-renderers/src/svelte/components/Stack.svelte b/examples/vite-renderers/src/svelte/components/Stack.svelte new file mode 100644 index 00000000..ffe8fd7c --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/Stack.svelte @@ -0,0 +1,35 @@ + + +
+ {#if children} + {@render children()} + {/if} +
diff --git a/examples/vite-renderers/src/svelte/components/Text.svelte b/examples/vite-renderers/src/svelte/components/Text.svelte new file mode 100644 index 00000000..ff5fab92 --- /dev/null +++ b/examples/vite-renderers/src/svelte/components/Text.svelte @@ -0,0 +1,34 @@ + + + + {String(props.content ?? "")} + diff --git a/examples/vite-renderers/src/svelte/mount.ts b/examples/vite-renderers/src/svelte/mount.ts new file mode 100644 index 00000000..bf0d4fea --- /dev/null +++ b/examples/vite-renderers/src/svelte/mount.ts @@ -0,0 +1,19 @@ +import { mount as mountComponent, unmount as unmountComponent } from "svelte"; +import type { Spec } from "@json-render/core"; +import App from "./App.svelte"; + +let app: ReturnType | null = null; + +export function mount(container: HTMLElement, renderer: string, spec: Spec) { + app = mountComponent(App, { + target: container, + props: { initialRenderer: renderer, spec }, + }); +} + +export function unmount() { + if (app) { + unmountComponent(app); + app = null; + } +} diff --git a/examples/vite-renderers/src/svelte/registry.ts b/examples/vite-renderers/src/svelte/registry.ts new file mode 100644 index 00000000..95291b8e --- /dev/null +++ b/examples/vite-renderers/src/svelte/registry.ts @@ -0,0 +1,28 @@ +import { defineRegistry, type ComponentRegistry } from "@json-render/svelte"; +import { catalog } from "./catalog"; +import { actionStubs } from "../shared/handlers"; + +import Stack from "./components/Stack.svelte"; +import Card from "./components/Card.svelte"; +import Text from "./components/Text.svelte"; +import Button from "./components/Button.svelte"; +import Badge from "./components/Badge.svelte"; +import ListItem from "./components/ListItem.svelte"; +import RendererBadge from "./components/RendererBadge.svelte"; +import RendererTabs from "./components/RendererTabs.svelte"; + +const components: ComponentRegistry = { + Stack, + Card, + Text, + Button, + Badge, + ListItem, + RendererBadge, + RendererTabs, +}; + +export const { registry } = defineRegistry(catalog, { + components, + actions: actionStubs, +}); diff --git a/examples/vite-renderers/src/vue/registry.ts b/examples/vite-renderers/src/vue/registry.ts index 6bbf6f2d..1368e878 100644 --- a/examples/vite-renderers/src/vue/registry.ts +++ b/examples/vite-renderers/src/vue/registry.ts @@ -128,7 +128,11 @@ export const components: Components = { RendererBadge: ({ props }) => h("span", { class: "json-render-renderer-badge" }, [ h("span", { class: "json-render-renderer-dot" }), - props.renderer === "vue" ? "Rendered with Vue" : "Rendered with React", + props.renderer === "vue" + ? "Rendered with Vue" + : props.renderer === "react" + ? "Rendered with React" + : "Rendered with Svelte", ]), RendererTabs: ({ props, emit }) => @@ -161,6 +165,19 @@ export const components: Components = { }, "React", ), + h( + "button", + { + onClick: () => emit("pressSvelte"), + class: [ + "json-render-renderer-tab", + props.renderer === "svelte" && "json-render-renderer-tab--active", + ] + .filter(Boolean) + .join(" "), + }, + "Svelte", + ), ]), ]), }; diff --git a/examples/vite-renderers/svelte.config.js b/examples/vite-renderers/svelte.config.js new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/examples/vite-renderers/svelte.config.js @@ -0,0 +1 @@ +export default {}; diff --git a/examples/vite-renderers/tsconfig.json b/examples/vite-renderers/tsconfig.json index cfe58fa9..f78d80c1 100644 --- a/examples/vite-renderers/tsconfig.json +++ b/examples/vite-renderers/tsconfig.json @@ -13,5 +13,5 @@ "jsx": "react-jsx", "strict": true }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.svelte"] } diff --git a/examples/vite-renderers/vite.config.ts b/examples/vite-renderers/vite.config.ts index f4825774..4ef87262 100644 --- a/examples/vite-renderers/vite.config.ts +++ b/examples/vite-renderers/vite.config.ts @@ -1,7 +1,8 @@ import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import react from "@vitejs/plugin-react"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; export default defineConfig({ - plugins: [vue(), react({ include: /\.tsx$/ })], + plugins: [svelte(), vue(), react({ include: /\.tsx$/ })], }); diff --git a/examples/vue/package.json b/examples/vue/package.json index ba44de1e..6fcb8cdf 100644 --- a/examples/vue/package.json +++ b/examples/vue/package.json @@ -12,7 +12,7 @@ "@json-render/core": "workspace:*", "@json-render/vue": "workspace:*", "vue": "^3.5.0", - "zod": "^4.3.6" + "zod": "4.3.6" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.4", diff --git a/examples/vue/src/DemoRenderer.vue b/examples/vue/src/DemoRenderer.vue index 04358f89..d211b7e3 100644 --- a/examples/vue/src/DemoRenderer.vue +++ b/examples/vue/src/DemoRenderer.vue @@ -49,6 +49,7 @@ const handlers = { }> ).slice(); const item = todos[index]; + console.log("item", item); if (item) { todos[index] = { ...item, completed: !item.completed }; } diff --git a/package.json b/package.json index be28ae9c..1cde8173 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,10 @@ }, "devDependencies": { "@changesets/cli": "2.29.8", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.1", + "@testing-library/svelte": "^5.2.0", "@types/react": "^19.2.3", "husky": "^9.1.7", "jsdom": "^27.4.0", @@ -38,6 +40,7 @@ "prettier": "^3.7.4", "react": "^19.2.4", "react-dom": "^19.2.4", + "svelte": "^5.0.0", "turbo": "^2.7.4", "typescript": "5.9.2", "vitest": "^4.0.17" diff --git a/packages/svelte/package.json b/packages/svelte/package.json new file mode 100644 index 00000000..615a9544 --- /dev/null +++ b/packages/svelte/package.json @@ -0,0 +1,70 @@ +{ + "name": "@json-render/svelte", + "version": "0.6.1", + "license": "Apache-2.0", + "description": "Svelte 5 renderer for @json-render/core. JSON becomes Svelte components.", + "keywords": [ + "json", + "ui", + "svelte", + "svelte5", + "ai", + "generative-ui", + "llm", + "renderer", + "streaming", + "components", + "runes" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/json-render.git", + "directory": "packages/svelte" + }, + "homepage": "https://github.com/vercel-labs/json-render#readme", + "bugs": { + "url": "https://github.com/vercel-labs/json-render/issues" + }, + "publishConfig": { + "access": "public" + }, + "type": "module", + "svelte": "./dist/index.js", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js", + "default": "./dist/index.js" + }, + "./schema": { + "types": "./dist/schema.d.ts", + "svelte": "./dist/schema.js", + "default": "./dist/schema.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "svelte-package -i src -o dist", + "dev": "svelte-package -i src -o dist --watch", + "typecheck": "svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@json-render/core": "workspace:*" + }, + "devDependencies": { + "@internal/typescript-config": "workspace:*", + "@sveltejs/package": "^2.3.0", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.4.5" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } +} diff --git a/packages/svelte/src/CatalogRenderer.svelte b/packages/svelte/src/CatalogRenderer.svelte new file mode 100644 index 00000000..2fa9c398 --- /dev/null +++ b/packages/svelte/src/CatalogRenderer.svelte @@ -0,0 +1,55 @@ + + + + + + + diff --git a/packages/svelte/src/ConfirmDialog.svelte b/packages/svelte/src/ConfirmDialog.svelte new file mode 100644 index 00000000..b354bb95 --- /dev/null +++ b/packages/svelte/src/ConfirmDialog.svelte @@ -0,0 +1,98 @@ + + + + +
+ + +
e.stopPropagation()}> +

{confirm.title}

+

{confirm.message}

+
+ + +
+
+
+ + diff --git a/packages/svelte/src/ConfirmDialogManager.svelte b/packages/svelte/src/ConfirmDialogManager.svelte new file mode 100644 index 00000000..54f4c370 --- /dev/null +++ b/packages/svelte/src/ConfirmDialogManager.svelte @@ -0,0 +1,15 @@ + + +{#if pendingConfirmation?.action.confirm} + +{/if} diff --git a/packages/svelte/src/ElementRenderer.svelte b/packages/svelte/src/ElementRenderer.svelte new file mode 100644 index 00000000..be56b43a --- /dev/null +++ b/packages/svelte/src/ElementRenderer.svelte @@ -0,0 +1,147 @@ + + +{#if isVisible && Component} + { + console.error( + `[json-render] Rendering error in <${resolvedElement.type}>:`, + error, + ); + }}> + + {#if resolvedElement.repeat} + + {:else if resolvedElement.children} + {#each resolvedElement.children as childKey (childKey)} + {#if spec.elements[childKey]} + + {:else if !loading} + {console.warn( + `[json-render] Missing element "${childKey}" referenced as child of "${resolvedElement.type}". This element will not render.`, + )} + {/if} + {/each} + {/if} + + {#snippet failed()} + + {/snippet} + +{/if} diff --git a/packages/svelte/src/JsonUIProvider.svelte b/packages/svelte/src/JsonUIProvider.svelte new file mode 100644 index 00000000..32cc6199 --- /dev/null +++ b/packages/svelte/src/JsonUIProvider.svelte @@ -0,0 +1,68 @@ + + + + + + + + + + {@render children()} + + + + + + diff --git a/packages/svelte/src/Renderer.svelte b/packages/svelte/src/Renderer.svelte new file mode 100644 index 00000000..606a83cc --- /dev/null +++ b/packages/svelte/src/Renderer.svelte @@ -0,0 +1,34 @@ + + + + +{#if spec && rootElement} + +{/if} diff --git a/packages/svelte/src/RendererWithProvider.test.svelte b/packages/svelte/src/RendererWithProvider.test.svelte new file mode 100644 index 00000000..e62cb555 --- /dev/null +++ b/packages/svelte/src/RendererWithProvider.test.svelte @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/packages/svelte/src/RepeatChildren.svelte b/packages/svelte/src/RepeatChildren.svelte new file mode 100644 index 00000000..e23967d4 --- /dev/null +++ b/packages/svelte/src/RepeatChildren.svelte @@ -0,0 +1,50 @@ + + +{#each items as itemValue, index (element.repeat?.key && typeof itemValue === "object" && itemValue !== null ? String((itemValue as any)[element.repeat.key] ?? index) : String(index))} + {@const basePath = `${element.repeat!.statePath}/${index}`} + + {#if element.children} + + {#each element.children as childKey (childKey)} + {#if spec.elements[childKey]} + + {:else if !loading} + {console.warn( + `[json-render] Missing element "${childKey}" referenced as child of "${element.type}". This element will not render.`, + )} + {/if} + {/each} + + {/if} +{/each} diff --git a/packages/svelte/src/TestButton.svelte b/packages/svelte/src/TestButton.svelte new file mode 100644 index 00000000..4fc17e01 --- /dev/null +++ b/packages/svelte/src/TestButton.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/svelte/src/TestContainer.svelte b/packages/svelte/src/TestContainer.svelte new file mode 100644 index 00000000..c101e088 --- /dev/null +++ b/packages/svelte/src/TestContainer.svelte @@ -0,0 +1,19 @@ + + +
+ {#if props.title} +

{props.title}

+ {/if} + {#if children} + {@render children()} + {/if} +
diff --git a/packages/svelte/src/TestText.svelte b/packages/svelte/src/TestText.svelte new file mode 100644 index 00000000..ac06e594 --- /dev/null +++ b/packages/svelte/src/TestText.svelte @@ -0,0 +1,9 @@ + + +{props.text ?? ""} diff --git a/packages/svelte/src/catalog-types.ts b/packages/svelte/src/catalog-types.ts new file mode 100644 index 00000000..c2768793 --- /dev/null +++ b/packages/svelte/src/catalog-types.ts @@ -0,0 +1,104 @@ +import type { Component, Snippet } from "svelte"; +import type { + Catalog, + InferCatalogComponents, + InferCatalogActions, + InferComponentProps, + InferActionParams, + StateModel, +} from "@json-render/core"; + +export type { StateModel }; + +// ============================================================================= +// State Types +// ============================================================================= + +/** + * State setter function for updating application state + */ +export type SetState = ( + updater: (prev: Record) => Record, +) => void; + +// ============================================================================= +// Component Types +// ============================================================================= + +/** + * Handle returned by the `on()` function for a specific event. + * Provides metadata about the event binding and a method to fire it. + */ +export interface EventHandle { + /** Fire the event (resolve action bindings) */ + emit: () => void; + /** Whether any binding requested preventDefault */ + shouldPreventDefault: boolean; + /** Whether any handler is bound to this event */ + bound: boolean; +} + +/** + * Catalog-agnostic base type for component render function arguments. + * Use this when building reusable component libraries. + */ +export interface BaseComponentProps

> { + props: P; + children?: Snippet; + /** Simple event emitter (shorthand). Fires the event and returns void. */ + emit: (event: string) => void; + /** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */ + on: (event: string) => EventHandle; + /** + * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions. + * Maps prop name → absolute state path for write-back. + */ + bindings?: Record; + loading?: boolean; +} + +/** + * Context passed to component render functions + */ +export interface ComponentContext< + C extends Catalog, + K extends keyof InferCatalogComponents, +> extends BaseComponentProps> {} + +/** + * Component render function type for Svelte + */ +export type ComponentFn< + C extends Catalog, + K extends keyof InferCatalogComponents, +> = Component>>; + +/** + * Registry of Svelte component constructors for a catalog + */ +export type Components = { + [K in keyof InferCatalogComponents]: ComponentFn; +}; + +// ============================================================================= +// Action Types +// ============================================================================= + +/** + * Action handler function type + */ +export type ActionFn< + C extends Catalog, + K extends keyof InferCatalogActions, +> = ( + params: InferActionParams | undefined, + setState: SetState, + state: StateModel, +) => Promise; + +/** + * Registry of all action handlers for a catalog + */ +export type Actions = { + [K in keyof InferCatalogActions]: ActionFn; +}; diff --git a/packages/svelte/src/contexts/ActionProvider.svelte b/packages/svelte/src/contexts/ActionProvider.svelte new file mode 100644 index 00000000..48a4c1cd --- /dev/null +++ b/packages/svelte/src/contexts/ActionProvider.svelte @@ -0,0 +1,344 @@ + + + + +{@render children?.()} diff --git a/packages/svelte/src/contexts/FunctionsContextProvider.svelte b/packages/svelte/src/contexts/FunctionsContextProvider.svelte new file mode 100644 index 00000000..8ab0aeb9 --- /dev/null +++ b/packages/svelte/src/contexts/FunctionsContextProvider.svelte @@ -0,0 +1,44 @@ + + + + +{@render children?.()} diff --git a/packages/svelte/src/contexts/RepeatScopeProvider.svelte b/packages/svelte/src/contexts/RepeatScopeProvider.svelte new file mode 100644 index 00000000..da156c72 --- /dev/null +++ b/packages/svelte/src/contexts/RepeatScopeProvider.svelte @@ -0,0 +1,49 @@ + + + + +{@render children()} diff --git a/packages/svelte/src/contexts/StateProvider.svelte b/packages/svelte/src/contexts/StateProvider.svelte new file mode 100644 index 00000000..2bc83293 --- /dev/null +++ b/packages/svelte/src/contexts/StateProvider.svelte @@ -0,0 +1,182 @@ + + + + +{@render children?.()} diff --git a/packages/svelte/src/contexts/ValidationProvider.svelte b/packages/svelte/src/contexts/ValidationProvider.svelte new file mode 100644 index 00000000..7fc9b1ca --- /dev/null +++ b/packages/svelte/src/contexts/ValidationProvider.svelte @@ -0,0 +1,198 @@ + + + + +{@render children?.()} diff --git a/packages/svelte/src/contexts/VisibilityProvider.svelte b/packages/svelte/src/contexts/VisibilityProvider.svelte new file mode 100644 index 00000000..c7650e44 --- /dev/null +++ b/packages/svelte/src/contexts/VisibilityProvider.svelte @@ -0,0 +1,82 @@ + + + + +{@render children?.()} diff --git a/packages/svelte/src/contexts/actions.test.ts b/packages/svelte/src/contexts/actions.test.ts new file mode 100644 index 00000000..d6673be8 --- /dev/null +++ b/packages/svelte/src/contexts/actions.test.ts @@ -0,0 +1,278 @@ +import { describe, it, expect, vi } from "vitest"; +import { mount, unmount } from "svelte"; +import StateProvider, { getStateContext } from "./StateProvider.svelte"; +import ActionProvider, { getActionContext } from "./ActionProvider.svelte"; +import ValidationProvider, { + getValidationContext, +} from "./ValidationProvider.svelte"; + +function component( + runTest: () => Promise, + options: { + initialState?: Record; + handlers?: Record< + string, + (params: Record) => Promise | unknown + >; + withValidation?: boolean; + } = {}, +) { + return async () => { + let promise: Promise; + const c = mount( + ((_anchor: any) => { + (StateProvider as any)(_anchor, { + initialState: options.initialState ?? {}, + children: ((_inner: any) => { + if (options.withValidation) { + (ValidationProvider as any)(_inner, { + children: ((__inner: any) => { + (ActionProvider as any)(__inner, { + handlers: options.handlers ?? {}, + children: (() => { + promise = runTest(); + }) as any, + }); + }) as any, + }); + return; + } + + (ActionProvider as any)(_inner, { + handlers: options.handlers ?? {}, + children: (() => { + promise = runTest(); + }) as any, + }); + }) as any, + }); + }) as any, + { target: document.body }, + ); + await promise!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + unmount(c); + }; +} + +describe("createActionContext", () => { + it( + "executes built-in setState action", + component( + async () => { + const stateCtx = getStateContext(); + const actionCtx = getActionContext(); + + await actionCtx.execute({ + action: "setState", + params: { statePath: "/count", value: 5 }, + }); + + expect(stateCtx.state.count).toBe(5); + }, + { initialState: { count: 0 } }, + ), + ); + + it( + "executes built-in pushState action", + component( + async () => { + const stateCtx = getStateContext(); + const actionCtx = getActionContext(); + + await actionCtx.execute({ + action: "pushState", + params: { statePath: "/items", value: "c" }, + }); + + expect(stateCtx.state.items).toEqual(["a", "b", "c"]); + }, + { initialState: { items: ["a", "b"] } }, + ), + ); + + it( + "pushState creates array if missing", + component(async () => { + const stateCtx = getStateContext(); + const actionCtx = getActionContext(); + + await actionCtx.execute({ + action: "pushState", + params: { statePath: "/newList", value: "first" }, + }); + + expect(stateCtx.get("/newList")).toEqual(["first"]); + }), + ); + + it( + "executes built-in removeState action", + component( + async () => { + const stateCtx = getStateContext(); + const actionCtx = getActionContext(); + + await actionCtx.execute({ + action: "removeState", + params: { statePath: "/items", index: 1 }, + }); + + expect(stateCtx.state.items).toEqual(["a", "c"]); + }, + { initialState: { items: ["a", "b", "c"] } }, + ), + ); + + it( + "executes push navigation action", + component( + async () => { + const stateCtx = getStateContext(); + const actionCtx = getActionContext(); + + await actionCtx.execute({ + action: "push", + params: { screen: "settings" }, + }); + + expect(stateCtx.get("/currentScreen")).toBe("settings"); + expect(stateCtx.get("/navStack")).toEqual(["home"]); + }, + { initialState: { currentScreen: "home" } }, + ), + ); + + it( + "executes pop navigation action", + component( + async () => { + const stateCtx = getStateContext(); + const actionCtx = getActionContext(); + + await actionCtx.execute({ action: "pop" }); + + expect(stateCtx.get("/currentScreen")).toBe("home"); + expect(stateCtx.get("/navStack")).toEqual([]); + }, + { initialState: { currentScreen: "settings", navStack: ["home"] } }, + ), + ); + + it( + "executes custom handlers", + (() => { + const customHandler = vi.fn().mockResolvedValue(undefined); + return component( + async () => { + const actionCtx = getActionContext(); + + await actionCtx.execute({ + action: "myAction", + params: { foo: "bar" }, + }); + + expect(customHandler).toHaveBeenCalledWith({ foo: "bar" }); + }, + { handlers: { myAction: customHandler } }, + ); + })(), + ); + + it( + "warns when no handler registered", + component(async () => { + const actionCtx = getActionContext(); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await actionCtx.execute({ action: "unknownAction" }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("unknownAction"), + ); + warnSpy.mockRestore(); + }), + ); + + it( + "tracks loading state for actions", + (() => { + let resolveHandler: () => void; + const slowHandler = vi.fn( + () => + new Promise((resolve) => { + resolveHandler = resolve; + }), + ); + return component( + async () => { + const actionCtx = getActionContext(); + const executePromise = actionCtx.execute({ action: "slowAction" }); + + expect(actionCtx.loadingActions.has("slowAction")).toBe(true); + + resolveHandler!(); + await executePromise; + + expect(actionCtx.loadingActions.has("slowAction")).toBe(false); + }, + { + handlers: { + slowAction: slowHandler, + }, + }, + ); + })(), + ); + + it( + "allows registering handlers dynamically", + component(async () => { + const actionCtx = getActionContext(); + const dynamicHandler = vi.fn(); + + actionCtx.registerHandler("dynamicAction", dynamicHandler); + await actionCtx.execute({ action: "dynamicAction", params: { x: 1 } }); + + expect(dynamicHandler).toHaveBeenCalledWith({ x: 1 }); + }), + ); + + it( + "executes validateForm and writes result to /formValidation", + component( + async () => { + const stateCtx = getStateContext(); + const actionCtx = getActionContext(); + const validationCtx = getValidationContext(); + + validationCtx.registerField("/form/email", { + checks: [{ type: "required", message: "Required" }], + }); + + await actionCtx.execute({ action: "validateForm" }); + + expect(stateCtx.get("/formValidation")).toEqual({ + valid: false, + errors: { "/form/email": ["Required"] }, + }); + }, + { withValidation: true }, + ), + ); + + it( + "validateForm defaults to warning when validation context is missing", + component(async () => { + const actionCtx = getActionContext(); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await actionCtx.execute({ action: "validateForm" }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("validateForm action was dispatched"), + ); + warnSpy.mockRestore(); + }), + ); +}); diff --git a/packages/svelte/src/contexts/state.test.ts b/packages/svelte/src/contexts/state.test.ts new file mode 100644 index 00000000..62f5a213 --- /dev/null +++ b/packages/svelte/src/contexts/state.test.ts @@ -0,0 +1,207 @@ +import { describe, it, expect, vi } from "vitest"; +import { mount, unmount } from "svelte"; +import { createStateStore } from "@json-render/core"; +import StateProvider, { getStateContext } from "./StateProvider.svelte"; + +function component( + runTest: () => void, + props: { + initialState?: Record; + onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void; + store?: ReturnType; + } = {}, +) { + return () => { + const c = mount( + ((_anchor: any) => { + (StateProvider as any)(_anchor, { + ...props, + children: (() => { + runTest(); + }) as any, + }); + }) as any, + { target: document.body }, + ); + unmount(c); + }; +} + +describe("StateProvider", () => { + it( + "provides initial state to consumers", + component( + () => { + const ctx = getStateContext(); + expect(ctx.state).toEqual({ user: { name: "John" } }); + }, + { initialState: { user: { name: "John" } } }, + ), + ); + + it( + "provides empty object when no initial state", + component(() => { + const ctx = getStateContext(); + expect(ctx.state).toEqual({}); + }), + ); +}); + +describe("StateContext.get", () => { + it( + "retrieves values by path", + component( + () => { + const ctx = getStateContext(); + expect(ctx.get("/user/name")).toBe("John"); + expect(ctx.get("/user/age")).toBe(30); + }, + { initialState: { user: { name: "John", age: 30 } } }, + ), + ); + + it( + "returns undefined for missing path", + component( + () => { + const ctx = getStateContext(); + expect(ctx.get("/user/email")).toBeUndefined(); + expect(ctx.get("/nonexistent")).toBeUndefined(); + }, + { initialState: { user: { name: "John" } } }, + ), + ); +}); + +describe("StateContext.set", () => { + it( + "updates values at path", + component( + () => { + const ctx = getStateContext(); + ctx.set("/count", 5); + expect(ctx.state.count).toBe(5); + }, + { initialState: { count: 0 } }, + ), + ); + + it( + "creates nested paths", + component(() => { + const ctx = getStateContext(); + ctx.set("/user/name", "Jane"); + expect(ctx.get("/user/name")).toBe("Jane"); + }), + ); + + it( + "calls onStateChange callback with change entries", + component( + () => { + const ctx = getStateContext(); + ctx.set("/value", 2); + }, + { + initialState: { value: 1 }, + onStateChange: vi.fn((changes) => { + expect(changes).toEqual([{ path: "/value", value: 2 }]); + }), + }, + ), + ); +}); + +describe("StateContext.update", () => { + it( + "handles multiple values at once", + component( + () => { + const ctx = getStateContext(); + ctx.update({ "/a": 10, "/b": 20 }); + expect(ctx.state.a).toBe(10); + expect(ctx.state.b).toBe(20); + }, + { initialState: { a: 1, b: 2 } }, + ), + ); + + it( + "calls onStateChange once with all changed updates", + component( + () => { + const ctx = getStateContext(); + ctx.update({ "/x": 1, "/y": 2 }); + }, + { + initialState: { x: 0, y: 0 }, + onStateChange: vi.fn((changes) => { + expect(changes).toEqual([ + { path: "/x", value: 1 }, + { path: "/y", value: 2 }, + ]); + }), + }, + ), + ); +}); + +describe("StateContext nested paths", () => { + it( + "handles deeply nested state paths", + component( + () => { + const ctx = getStateContext(); + expect(ctx.get("/app/settings/theme")).toBe("light"); + expect(ctx.get("/app/settings/notifications/enabled")).toBe(true); + ctx.set("/app/settings/theme", "dark"); + expect(ctx.get("/app/settings/theme")).toBe("dark"); + }, + { + initialState: { + app: { + settings: { + theme: "light", + notifications: { enabled: true }, + }, + }, + }, + }, + ), + ); + + it( + "handles array indices in paths", + component( + () => { + const ctx = getStateContext(); + expect(ctx.get("/items/0")).toBe("a"); + expect(ctx.get("/items/1")).toBe("b"); + ctx.set("/items/1", "B"); + expect(ctx.get("/items/1")).toBe("B"); + }, + { initialState: { items: ["a", "b", "c"] } }, + ), + ); +}); + +describe("controlled mode", () => { + it( + "reads and writes through external StateStore", + (() => { + const store = createStateStore({ count: 1 }); + const onStateChange = vi.fn(); + return component( + () => { + const ctx = getStateContext(); + expect(ctx.get("/count")).toBe(1); + ctx.set("/count", 2); + expect(store.get("/count")).toBe(2); + expect(onStateChange).not.toHaveBeenCalled(); + }, + { store, onStateChange }, + ); + })(), + ); +}); diff --git a/packages/svelte/src/contexts/visibility.test.ts b/packages/svelte/src/contexts/visibility.test.ts new file mode 100644 index 00000000..e481d8b0 --- /dev/null +++ b/packages/svelte/src/contexts/visibility.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect } from "vitest"; +import { mount, unmount } from "svelte"; +import StateProvider, { getStateContext } from "./StateProvider.svelte"; +import VisibilityProvider, { + getVisibilityContext, +} from "./VisibilityProvider.svelte"; + +function component( + runTest: () => void, + initialState: Record = {}, +) { + return () => { + const c = mount( + ((_anchor: any) => { + (StateProvider as any)(_anchor, { + initialState, + children: ((_inner: any) => { + (VisibilityProvider as any)(_inner, { + children: (() => { + runTest(); + }) as any, + }); + }) as any, + }); + }) as any, + { target: document.body }, + ); + unmount(c); + }; +} + +describe("VisibilityProvider", () => { + it( + "provides isVisible function", + component(() => { + const visCtx = getVisibilityContext(); + + expect(typeof visCtx.isVisible).toBe("function"); + }), + ); + + it( + "provides visibility context", + component( + () => { + const visCtx = getVisibilityContext(); + + expect(visCtx.ctx).toBeDefined(); + expect(visCtx.ctx.stateModel).toEqual({ value: true }); + }, + { value: true }, + ), + ); +}); + +describe("isVisible", () => { + it( + "returns true for undefined condition", + component(() => { + const visCtx = getVisibilityContext(); + + expect(visCtx.isVisible(undefined)).toBe(true); + }), + ); + + it( + "returns true for true condition", + component(() => { + const visCtx = getVisibilityContext(); + + expect(visCtx.isVisible(true)).toBe(true); + }), + ); + + it( + "returns false for false condition", + component(() => { + const visCtx = getVisibilityContext(); + + expect(visCtx.isVisible(false)).toBe(false); + }), + ); + + it( + "evaluates $state conditions against data", + component( + () => { + const stateCtx = getStateContext(); + const visCtx = getVisibilityContext(); + + expect(visCtx.isVisible({ $state: "/isLoggedIn" })).toBe(true); + + stateCtx.set("/isLoggedIn", false); + + expect(visCtx.isVisible({ $state: "/isLoggedIn" })).toBe(false); + }, + { isLoggedIn: true }, + ), + ); + + it( + "evaluates equality conditions", + component( + () => { + const visCtx = getVisibilityContext(); + + expect(visCtx.isVisible({ $state: "/tab", eq: "home" })).toBe(true); + expect(visCtx.isVisible({ $state: "/tab", eq: "settings" })).toBe( + false, + ); + }, + { tab: "home" }, + ), + ); + + it( + "evaluates array conditions (implicit AND)", + component( + () => { + const visCtx = getVisibilityContext(); + + expect(visCtx.isVisible([{ $state: "/a" }, { $state: "/b" }])).toBe( + true, + ); + + expect(visCtx.isVisible([{ $state: "/a" }, { $state: "/c" }])).toBe( + false, + ); + }, + { a: true, b: true, c: false }, + ), + ); + + it( + "evaluates $and conditions", + component( + () => { + const visCtx = getVisibilityContext(); + + expect( + visCtx.isVisible({ $and: [{ $state: "/x" }, { $state: "/y" }] }), + ).toBe(false); + }, + { x: true, y: false }, + ), + ); + + it( + "evaluates $or conditions", + component( + () => { + const visCtx = getVisibilityContext(); + + expect( + visCtx.isVisible({ $or: [{ $state: "/x" }, { $state: "/y" }] }), + ).toBe(true); + }, + { x: true, y: false }, + ), + ); +}); diff --git a/packages/svelte/src/index.ts b/packages/svelte/src/index.ts new file mode 100644 index 00000000..a112a13a --- /dev/null +++ b/packages/svelte/src/index.ts @@ -0,0 +1,130 @@ +// ============================================================================= +// Contexts +// ============================================================================= + +export { + default as StateProvider, + getStateContext, + getStateValue, + getBoundProp, + type StateContext, +} from "./contexts/StateProvider.svelte"; + +export { + default as VisibilityProvider, + getVisibilityContext, + isVisible, + type VisibilityContext, +} from "./contexts/VisibilityProvider.svelte"; + +export { + default as ActionProvider, + getActionContext, + getAction, + type ActionContext, + type PendingConfirmation, +} from "./contexts/ActionProvider.svelte"; + +export { + default as ValidationProvider, + getValidationContext, + getOptionalValidationContext, + getFieldValidation, + type ValidationContext, + type FieldValidationState, +} from "./contexts/ValidationProvider.svelte"; + +export { + default as RepeatScopeProvider, + getRepeatScope, + type RepeatScopeValue, +} from "./contexts/RepeatScopeProvider.svelte"; + +export { + default as FunctionsContextProvider, + getFunctions, + type FunctionsContext, +} from "./contexts/FunctionsContextProvider.svelte"; + +// ============================================================================= +// Schema +// ============================================================================= + +export { schema, type SvelteSchema, type SvelteSpec } from "./schema.js"; + +// ============================================================================= +// Catalog Types +// ============================================================================= + +export type { + EventHandle, + BaseComponentProps, + SetState, + StateModel, + ComponentContext, + ComponentFn, + Components, + ActionFn, + Actions, +} from "./catalog-types.js"; + +// ============================================================================= +// Utilities +// ============================================================================= + +export { + flatToTree, + buildSpecFromParts, + getTextFromParts, + type DataPart, +} from "./utils.svelte.js"; + +// ============================================================================= +// Streaming +// ============================================================================= + +export { + createUIStream, + createChatUI, + type UIStreamOptions, + type UIStreamReturn, + type UIStreamState, + type ChatUIOptions, + type ChatUIReturn, + type ChatMessage, + type TokenUsage, +} from "./streaming.svelte.js"; + +// ============================================================================= +// Registry +// ============================================================================= + +export { + defineRegistry, + createRenderer, + type DefineRegistryResult, + type ComponentRenderer, + type ComponentRegistry, +} from "./renderer.js"; +export { default as Renderer, type RendererProps } from "./Renderer.svelte"; +export { + default as CatalogRenderer, + type CatalogRendererProps, +} from "./CatalogRenderer.svelte"; +export { + default as JsonUIProvider, + type JSONUIProviderProps, +} from "./JsonUIProvider.svelte"; +export { default as ConfirmDialog } from "./ConfirmDialog.svelte"; +export { default as ConfirmDialogManager } from "./ConfirmDialogManager.svelte"; + +// ============================================================================= +// Re-exports from core +// ============================================================================= + +export type { + Spec, + UIElement, + ActionBinding, + ActionHandler, +} from "@json-render/core"; diff --git a/packages/svelte/src/renderer.test.ts b/packages/svelte/src/renderer.test.ts new file mode 100644 index 00000000..eb1ddbef --- /dev/null +++ b/packages/svelte/src/renderer.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { render, cleanup } from "@testing-library/svelte"; +import type { Spec } from "@json-render/core"; +import RendererWithProvider from "./RendererWithProvider.test.svelte"; +import TestContainer from "./TestContainer.svelte"; +import TestText from "./TestText.svelte"; +import TestButton from "./TestButton.svelte"; +import { defineRegistry } from "./renderer.js"; + +describe("Renderer", () => { + afterEach(() => { + cleanup(); + }); + + const { registry } = defineRegistry(null as any, { + components: { + Container: TestContainer, + Text: TestText, + Button: TestButton, + }, + }); + + function mountRenderer( + spec: Spec | null, + options: { loading?: boolean } = {}, + ) { + return render(RendererWithProvider, { + props: { + spec, + registry, + loading: options.loading ?? false, + initialState: spec?.state ?? {}, + }, + }); + } + + it("renders nothing for null spec", () => { + const { container } = mountRenderer(null); + + // Should have no content rendered from Renderer + expect(container.querySelector(".test-container")).toBeNull(); + expect(container.querySelector(".test-text")).toBeNull(); + }); + + it("renders nothing for spec with empty root", () => { + const spec: Spec = { root: "", elements: {} }; + const { container } = mountRenderer(spec); + + expect(container.querySelector(".test-container")).toBeNull(); + }); + + it("renders a single element", () => { + const spec: Spec = { + root: "text1", + elements: { + text1: { + type: "Text", + props: { text: "Hello World" }, + children: [], + }, + }, + }; + const { container } = mountRenderer(spec); + + const textEl = container.querySelector(".test-text"); + expect(textEl).not.toBeNull(); + expect(textEl?.textContent).toBe("Hello World"); + }); + + it("renders nested elements", () => { + const spec: Spec = { + root: "container", + elements: { + container: { + type: "Container", + props: { title: "My Container" }, + children: ["text1", "text2"], + }, + text1: { + type: "Text", + props: { text: "First" }, + children: [], + }, + text2: { + type: "Text", + props: { text: "Second" }, + children: [], + }, + }, + }; + const { container } = mountRenderer(spec); + + const containerEl = container.querySelector(".test-container"); + expect(containerEl).not.toBeNull(); + expect(containerEl?.querySelector("h2")?.textContent).toBe("My Container"); + + const texts = container.querySelectorAll(".test-text"); + expect(texts).toHaveLength(2); + expect(texts[0]?.textContent).toBe("First"); + expect(texts[1]?.textContent).toBe("Second"); + }); + + it("renders deeply nested elements", () => { + const spec: Spec = { + root: "outer", + elements: { + outer: { + type: "Container", + props: { title: "Outer" }, + children: ["inner"], + }, + inner: { + type: "Container", + props: { title: "Inner" }, + children: ["text"], + }, + text: { + type: "Text", + props: { text: "Deep text" }, + children: [], + }, + }, + }; + const { container } = mountRenderer(spec); + + const containers = container.querySelectorAll(".test-container"); + expect(containers).toHaveLength(2); + + const text = container.querySelector(".test-text"); + expect(text?.textContent).toBe("Deep text"); + }); + + it("passes loading prop to components", () => { + const spec: Spec = { + root: "container", + elements: { + container: { + type: "Container", + props: {}, + children: [], + }, + }, + }; + const { container } = mountRenderer(spec, { loading: true }); + + const containerEl = container.querySelector(".test-container"); + expect(containerEl?.getAttribute("data-loading")).toBe("true"); + }); + + it("renders nothing for unknown component types without fallback", () => { + const spec: Spec = { + root: "unknown", + elements: { + unknown: { + type: "UnknownType", + props: {}, + children: [], + }, + }, + }; + const { container } = mountRenderer(spec); + + // No elements should be rendered for unknown type + expect(container.querySelector(".test-container")).toBeNull(); + expect(container.querySelector(".test-text")).toBeNull(); + }); + + it("skips missing child elements gracefully", () => { + const spec: Spec = { + root: "container", + elements: { + container: { + type: "Container", + props: { title: "Parent" }, + children: ["existing", "missing"], + }, + existing: { + type: "Text", + props: { text: "I exist" }, + children: [], + }, + // "missing" element is not defined + }, + }; + const { container } = mountRenderer(spec); + + const containerEl = container.querySelector(".test-container"); + expect(containerEl).not.toBeNull(); + + const texts = container.querySelectorAll(".test-text"); + expect(texts).toHaveLength(1); + expect(texts[0]?.textContent).toBe("I exist"); + }); +}); diff --git a/packages/svelte/src/renderer.ts b/packages/svelte/src/renderer.ts new file mode 100644 index 00000000..1f4d773f --- /dev/null +++ b/packages/svelte/src/renderer.ts @@ -0,0 +1,274 @@ +import type { + Catalog, + ComputedFunction, + SchemaDefinition, + Spec, + StateStore, + UIElement, +} from "@json-render/core"; +import type { Component, Snippet } from "svelte"; +import type { + BaseComponentProps, + EventHandle, + SetState, + StateModel, +} from "./catalog-types.js"; +import CatalogRenderer from "./CatalogRenderer.svelte"; + +/** + * Props passed to component renderers + */ +export interface ComponentRenderProps

> { + /** The element being rendered */ + element: UIElement; + /** Rendered children snippet */ + children?: Snippet; + /** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. */ + emit: (event: string) => void; + /** Get an event handle with metadata */ + on: (event: string) => EventHandle; + /** + * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions. + * Maps prop name → absolute state path for write-back. + */ + bindings?: Record; + /** Whether the parent is loading */ + loading?: boolean; +} + +/** + * Component renderer type - a Svelte component that receives ComponentRenderProps + */ +export type ComponentRenderer

> = Component< + ComponentRenderProps

+>; + +/** + * Registry of component renderers. + * Maps component type names to Svelte components. + */ +export type ComponentRegistry = Record>; + +/** + * Action handler function for defineRegistry + */ +type DefineRegistryActionFn = ( + params: Record | undefined, + setState: SetState, + state: StateModel, +) => Promise; + +/** + * Result returned by defineRegistry + */ +export interface DefineRegistryResult { + /** Component registry for Renderer */ + registry: ComponentRegistry; + /** + * Create ActionProvider-compatible handlers. + */ + handlers: ( + getSetState: () => SetState | undefined, + getState: () => StateModel, + ) => Record) => Promise>; + /** + * Execute an action by name imperatively + */ + executeAction: ( + actionName: string, + params: Record | undefined, + setState: SetState, + state?: StateModel, + ) => Promise; +} + +/** + * Create a registry from a catalog with Svelte components and/or actions. + * + * Components must accept `BaseComponentProps` as their props interface. + * + * @example + * ```ts + * import { defineRegistry } from "@json-render/svelte"; + * import Card from "./components/Card.svelte"; + * import Button from "./components/Button.svelte"; + * import { myCatalog } from "./catalog"; + * + * const { registry, handlers } = defineRegistry(myCatalog, { + * components: { + * Card, + * Button, + * }, + * actions: { + * submit: async (params, setState) => { + * // handle action + * }, + * }, + * }); + * ``` + */ +export function defineRegistry< + C extends Catalog, + TComponents extends Record>>, +>( + _catalog: C, + options: { + /** Svelte components that accept BaseComponentProps */ + components?: TComponents; + /** Action handlers */ + actions?: Record; + }, +): DefineRegistryResult { + const registry: ComponentRegistry = {}; + + if (options.components) { + for (const [name, componentFn] of Object.entries(options.components)) { + registry[name] = (_, props) => + (componentFn as Component>)(_, { + get props() { + return props.element.props; + }, + get children() { + return props.children; + }, + get emit() { + return props.emit; + }, + get on() { + return props.on; + }, + get bindings() { + return props.bindings; + }, + get loading() { + return props.loading; + }, + }); + } + } + + // Build action helpers + const actionMap = options.actions + ? (Object.entries(options.actions) as Array< + [string, DefineRegistryActionFn] + >) + : []; + + const handlers = ( + getSetState: () => SetState | undefined, + getState: () => StateModel, + ): Record) => Promise> => { + const result: Record< + string, + (params: Record) => Promise + > = {}; + for (const [name, actionFn] of actionMap) { + result[name] = async (params) => { + const setState = getSetState(); + const state = getState(); + if (setState) { + await actionFn(params, setState, state); + } + }; + } + return result; + }; + + const executeAction = async ( + actionName: string, + params: Record | undefined, + setState: SetState, + state: StateModel = {}, + ): Promise => { + const entry = actionMap.find(([name]) => name === actionName); + if (entry) { + await entry[1](params, setState, state); + } else { + console.warn(`Unknown action: ${actionName}`); + } + }; + + return { registry, handlers, executeAction }; +} + +// ============================================================================ +// createRenderer +// ============================================================================ + +/** + * Props for renderers created with createRenderer + */ +export interface CreateRendererProps { + spec: Spec | null; + store?: StateStore; + state?: Record; + onAction?: (actionName: string, params?: Record) => void; + onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void; + /** Named functions for `$computed` expressions in props */ + functions?: Record; + loading?: boolean; + fallback?: Component; +} + +/** + * Component map type — maps component names to Vue components + */ +export type ComponentMap< + TComponents extends Record, +> = { + [K in keyof TComponents]: Component; +}; + +/** + * Create a renderer from a catalog + * + * @example + * ```typescript + * const DashboardRenderer = createRenderer(dashboardCatalog, { + * Card, + * Metric, + * }); + * + * // Usage in template + * + * ``` + */ +export function createRenderer< + TDef extends SchemaDefinition, + TCatalog extends { components: Record }, +>( + _catalog: Catalog, + components: ComponentMap, +): Component { + const registry: ComponentRegistry = + components as unknown as ComponentRegistry; + + return (_, props: CreateRendererProps) => + CatalogRenderer(_, { + registry, + get spec() { + return props.spec; + }, + get store() { + return props.store; + }, + get state() { + return props.state; + }, + get onAction() { + return props.onAction; + }, + get onStateChange() { + return props.onStateChange; + }, + get functions() { + return props.functions; + }, + get loading() { + return props.loading; + }, + get fallback() { + return props.fallback; + }, + }); +} diff --git a/packages/svelte/src/schema.ts b/packages/svelte/src/schema.ts new file mode 100644 index 00000000..fd73e18a --- /dev/null +++ b/packages/svelte/src/schema.ts @@ -0,0 +1,109 @@ +import { defineSchema } from "@json-render/core"; + +/** + * The schema for @json-render/svelte + * + * Defines: + * - Spec: A flat tree of elements with keys, types, props, and children references + * - Catalog: Components with props schemas, and optional actions + */ +export const schema = defineSchema( + (s) => ({ + // What the AI-generated SPEC looks like + spec: s.object({ + /** Root element key */ + root: s.string(), + /** Flat map of elements by key */ + elements: s.record( + s.object({ + /** Component type from catalog */ + type: s.ref("catalog.components"), + /** Component props */ + props: s.propsOf("catalog.components"), + /** Child element keys (flat reference) */ + children: s.array(s.string()), + /** Visibility condition */ + visible: s.any(), + }), + ), + }), + + // What the CATALOG must provide + catalog: s.object({ + /** Component definitions */ + components: s.map({ + /** Zod schema for component props */ + props: s.zod(), + /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */ + slots: s.array(s.string()), + /** Description for AI generation hints */ + description: s.string(), + /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */ + example: s.any(), + }), + /** Action definitions (optional) */ + actions: s.map({ + /** Zod schema for action params */ + params: s.zod(), + /** Description for AI generation hints */ + description: s.string(), + }), + }), + }), + { + builtInActions: [ + { + name: "setState", + description: + "Update a value in the state model at the given statePath. Params: { statePath: string, value: any }", + }, + { + name: "pushState", + description: + 'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {"$state":"/path"} refs and "$id" for auto IDs.', + }, + { + name: "removeState", + description: + "Remove an item from an array in state by index. Params: { statePath: string, index: number }", + }, + { + name: "validateForm", + description: + "Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record }.", + }, + ], + defaultRules: [ + // Element integrity + "CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.", + "SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.", + + // Field placement + 'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"","props":{},"visible":{"$state":"/tab","eq":"home"},"children":[...]}.', + 'CRITICAL: The "on" field goes on the ELEMENT object, NOT inside "props". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.', + + // State and data + "When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).", + 'When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on a container element. Example: { "type": "", "props": {}, "repeat": { "statePath": "/posts", "key": "id" }, "children": ["post-card"] }. Replace with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index. For two-way binding to an item field use { "$bindItem": "completed" }. Do NOT hardcode individual elements for each array item.', + + // Design quality + "Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.", + "For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.", + "Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.", + ], + }, +); + +/** + * Type for the Svelte schema + */ +export type SvelteSchema = typeof schema; + +/** + * Infer the spec type from a catalog + */ +export type SvelteSpec = typeof schema extends { + createCatalog: (catalog: TCatalog) => { _specType: infer S }; +} + ? S + : never; diff --git a/packages/svelte/src/streaming.svelte.ts b/packages/svelte/src/streaming.svelte.ts new file mode 100644 index 00000000..cc39016f --- /dev/null +++ b/packages/svelte/src/streaming.svelte.ts @@ -0,0 +1,533 @@ +import type { Spec, JsonPatch } from "@json-render/core"; +import { + setByPath, + getByPath, + removeByPath, + createMixedStreamParser, + applySpecPatch, +} from "@json-render/core"; + +/** + * Token usage metadata from AI generation + */ +export interface TokenUsage { + promptTokens: number; + completionTokens: number; + totalTokens: number; +} + +/** + * UI Stream state + */ +export interface UIStreamState { + spec: Spec | null; + isStreaming: boolean; + error: Error | null; + usage: TokenUsage | null; + rawLines: string[]; +} + +/** + * UI Stream return type + */ +export interface UIStreamReturn { + readonly spec: Spec | null; + readonly isStreaming: boolean; + readonly error: Error | null; + readonly usage: TokenUsage | null; + readonly rawLines: string[]; + send: (prompt: string, context?: Record) => Promise; + clear: () => void; +} + +/** + * Options for createUIStream + */ +export interface UIStreamOptions { + api: string; + onComplete?: (spec: Spec) => void; + onError?: (error: Error) => void; +} + +type ParsedLine = + | { type: "patch"; patch: JsonPatch } + | { type: "usage"; usage: TokenUsage } + | null; + +function parseLine(line: string): ParsedLine { + try { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("//")) { + return null; + } + const parsed = JSON.parse(trimmed); + + if (parsed.__meta === "usage") { + return { + type: "usage", + usage: { + promptTokens: parsed.promptTokens ?? 0, + completionTokens: parsed.completionTokens ?? 0, + totalTokens: parsed.totalTokens ?? 0, + }, + }; + } + + return { type: "patch", patch: parsed as JsonPatch }; + } catch { + return null; + } +} + +function setSpecValue(newSpec: Spec, path: string, value: unknown): void { + if (path === "/root") { + newSpec.root = value as string; + return; + } + + if (path === "/state") { + newSpec.state = value as Record; + return; + } + + if (path.startsWith("/state/")) { + if (!newSpec.state) newSpec.state = {}; + const statePath = path.slice("/state".length); + setByPath(newSpec.state as Record, statePath, value); + return; + } + + if (path.startsWith("/elements/")) { + const pathParts = path.slice("/elements/".length).split("/"); + const elementKey = pathParts[0]; + if (!elementKey) return; + + if (pathParts.length === 1) { + newSpec.elements[elementKey] = value as Spec["elements"][string]; + } else { + const element = newSpec.elements[elementKey]; + if (element) { + const propPath = "/" + pathParts.slice(1).join("/"); + const newElement = { ...element }; + setByPath( + newElement as unknown as Record, + propPath, + value, + ); + newSpec.elements[elementKey] = newElement; + } + } + } +} + +function removeSpecValue(newSpec: Spec, path: string): void { + if (path === "/state") { + delete newSpec.state; + return; + } + + if (path.startsWith("/state/") && newSpec.state) { + const statePath = path.slice("/state".length); + removeByPath(newSpec.state as Record, statePath); + return; + } + + if (path.startsWith("/elements/")) { + const pathParts = path.slice("/elements/".length).split("/"); + const elementKey = pathParts[0]; + if (!elementKey) return; + + if (pathParts.length === 1) { + const { [elementKey]: _, ...rest } = newSpec.elements; + newSpec.elements = rest; + } else { + const element = newSpec.elements[elementKey]; + if (element) { + const propPath = "/" + pathParts.slice(1).join("/"); + const newElement = { ...element }; + removeByPath( + newElement as unknown as Record, + propPath, + ); + newSpec.elements[elementKey] = newElement; + } + } + } +} + +function getSpecValue(spec: Spec, path: string): unknown { + if (path === "/root") return spec.root; + if (path === "/state") return spec.state; + if (path.startsWith("/state/") && spec.state) { + const statePath = path.slice("/state".length); + return getByPath(spec.state as Record, statePath); + } + return getByPath(spec as unknown as Record, path); +} + +function applyPatch(spec: Spec, patch: JsonPatch): Spec { + const newSpec = { + ...spec, + elements: { ...spec.elements }, + ...(spec.state ? { state: { ...spec.state } } : {}), + }; + + switch (patch.op) { + case "add": + case "replace": { + setSpecValue(newSpec, patch.path, patch.value); + break; + } + case "remove": { + removeSpecValue(newSpec, patch.path); + break; + } + case "move": { + if (!patch.from) break; + const moveValue = getSpecValue(newSpec, patch.from); + removeSpecValue(newSpec, patch.from); + setSpecValue(newSpec, patch.path, moveValue); + break; + } + case "copy": { + if (!patch.from) break; + const copyValue = getSpecValue(newSpec, patch.from); + setSpecValue(newSpec, patch.path, copyValue); + break; + } + case "test": { + break; + } + } + + return newSpec; +} + +/** + * Create a streaming UI generator using Svelte 5 $state + */ +export function createUIStream({ + api, + onComplete, + onError, +}: UIStreamOptions): UIStreamReturn { + let spec = $state(null); + let isStreaming = $state(false); + let error = $state(null); + let usage = $state(null); + let rawLines = $state([]); + let abortController: AbortController | null = null; + + const clear = () => { + spec = null; + error = null; + usage = null; + rawLines = []; + }; + + const send = async ( + prompt: string, + context?: Record, + ): Promise => { + abortController?.abort(); + abortController = new AbortController(); + + isStreaming = true; + error = null; + usage = null; + rawLines = []; + + const previousSpec = context?.previousSpec as Spec | undefined; + let currentSpec: Spec = + previousSpec && previousSpec.root + ? { ...previousSpec, elements: { ...previousSpec.elements } } + : { root: "", elements: {} }; + spec = currentSpec; + + try { + const response = await fetch(api, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ prompt, context, currentSpec }), + signal: abortController.signal, + }); + + if (!response.ok) { + let errorMessage = `HTTP error: ${response.status}`; + try { + const errorData = await response.json(); + if (errorData.message) errorMessage = errorData.message; + else if (errorData.error) errorMessage = errorData.error; + } catch { + // Ignore + } + throw new Error(errorMessage); + } + + const reader = response.body?.getReader(); + if (!reader) throw new Error("No response body"); + + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + const result = parseLine(trimmed); + if (!result) continue; + if (result.type === "usage") { + usage = result.usage; + } else { + rawLines = [...rawLines, trimmed]; + currentSpec = applyPatch(currentSpec, result.patch); + spec = { ...currentSpec }; + } + } + } + + if (buffer.trim()) { + const trimmed = buffer.trim(); + const result = parseLine(trimmed); + if (result) { + if (result.type === "usage") { + usage = result.usage; + } else { + rawLines = [...rawLines, trimmed]; + currentSpec = applyPatch(currentSpec, result.patch); + spec = { ...currentSpec }; + } + } + } + + onComplete?.(currentSpec); + } catch (err) { + if ((err as Error).name === "AbortError") return; + const e = err instanceof Error ? err : new Error(String(err)); + error = e; + onError?.(e); + } finally { + isStreaming = false; + } + }; + + return { + get spec() { + return spec; + }, + get isStreaming() { + return isStreaming; + }, + get error() { + return error; + }, + get usage() { + return usage; + }, + get rawLines() { + return rawLines; + }, + send, + clear, + }; +} + +/** + * Chat message type + */ +export interface ChatMessage { + id: string; + role: "user" | "assistant"; + text: string; + spec: Spec | null; +} + +/** + * Chat UI options + */ +export interface ChatUIOptions { + api: string; + onComplete?: (message: ChatMessage) => void; + onError?: (error: Error) => void; +} + +/** + * Chat UI return type + */ +export interface ChatUIReturn { + readonly messages: ChatMessage[]; + readonly isStreaming: boolean; + readonly error: Error | null; + send: (text: string) => Promise; + clear: () => void; +} + +let chatMessageIdCounter = 0; +function generateChatId(): string { + if ( + typeof crypto !== "undefined" && + typeof crypto.randomUUID === "function" + ) { + return crypto.randomUUID(); + } + chatMessageIdCounter += 1; + return `msg-${Date.now()}-${chatMessageIdCounter}`; +} + +/** + * Create a chat UI with streaming support + */ +export function createChatUI({ + api, + onComplete, + onError, +}: ChatUIOptions): ChatUIReturn { + let messages = $state([]); + let isStreaming = $state(false); + let error = $state(null); + let abortController: AbortController | null = null; + + const clear = () => { + messages = []; + error = null; + }; + + const send = async (text: string): Promise => { + if (!text.trim()) return; + + abortController?.abort(); + abortController = new AbortController(); + + const userMessage: ChatMessage = { + id: generateChatId(), + role: "user", + text: text.trim(), + spec: null, + }; + + const assistantId = generateChatId(); + const assistantMessage: ChatMessage = { + id: assistantId, + role: "assistant", + text: "", + spec: null, + }; + + messages = [...messages, userMessage, assistantMessage]; + isStreaming = true; + error = null; + + const historyForApi = messages + .slice(0, -1) + .map((m) => ({ role: m.role, content: m.text })); + + let accumulatedText = ""; + let currentSpec: Spec = { root: "", elements: {} }; + let hasSpec = false; + + try { + const response = await fetch(api, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ messages: historyForApi }), + signal: abortController.signal, + }); + + if (!response.ok) { + let errorMessage = `HTTP error: ${response.status}`; + try { + const errorData = await response.json(); + if (errorData.message) errorMessage = errorData.message; + else if (errorData.error) errorMessage = errorData.error; + } catch { + // Ignore + } + throw new Error(errorMessage); + } + + const reader = response.body?.getReader(); + if (!reader) throw new Error("No response body"); + + const decoder = new TextDecoder(); + + const parser = createMixedStreamParser({ + onPatch(patch) { + hasSpec = true; + applySpecPatch(currentSpec, patch); + messages = messages.map((m) => + m.id === assistantId + ? { + ...m, + spec: { + root: currentSpec.root, + elements: { ...currentSpec.elements }, + ...(currentSpec.state + ? { state: { ...currentSpec.state } } + : {}), + }, + } + : m, + ); + }, + onText(line) { + accumulatedText += (accumulatedText ? "\n" : "") + line; + messages = messages.map((m) => + m.id === assistantId ? { ...m, text: accumulatedText } : m, + ); + }, + }); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + parser.push(decoder.decode(value, { stream: true })); + } + parser.flush(); + + const finalMessage: ChatMessage = { + id: assistantId, + role: "assistant", + text: accumulatedText, + spec: hasSpec + ? { + root: currentSpec.root, + elements: { ...currentSpec.elements }, + ...(currentSpec.state ? { state: { ...currentSpec.state } } : {}), + } + : null, + }; + onComplete?.(finalMessage); + } catch (err) { + if ((err as Error).name === "AbortError") return; + const e = err instanceof Error ? err : new Error(String(err)); + error = e; + messages = messages.filter( + (m) => m.id !== assistantId || m.text.length > 0, + ); + onError?.(e); + } finally { + isStreaming = false; + } + }; + + return { + get messages() { + return messages; + }, + get isStreaming() { + return isStreaming; + }, + get error() { + return error; + }, + send, + clear, + }; +} diff --git a/packages/svelte/src/utils.svelte.ts b/packages/svelte/src/utils.svelte.ts new file mode 100644 index 00000000..4c3bf801 --- /dev/null +++ b/packages/svelte/src/utils.svelte.ts @@ -0,0 +1,124 @@ +import type { + Spec, + UIElement, + FlatElement, + SpecDataPart, +} from "@json-render/core"; +import { + applySpecPatch, + nestedToFlat, + SPEC_DATA_PART_TYPE, +} from "@json-render/core"; + +/** + * A single part from an AI response. Minimal structural type for library helpers. + */ +export interface DataPart { + type: string; + text?: string; + data?: unknown; +} + +/** + * Convert a flat element list to a Spec. + * Input elements use key/parentKey to establish identity and relationships. + * Output spec uses the map-based format where key is the map entry key + * and parent-child relationships are expressed through children arrays. + */ +export function flatToTree(elements: FlatElement[]): Spec { + const elementMap: Record = {}; + let root = ""; + + // First pass: add all elements to map + for (const element of elements) { + elementMap[element.key] = { + type: element.type, + props: element.props, + children: [], + visible: element.visible, + }; + } + + // Second pass: build parent-child relationships + for (const element of elements) { + if (element.parentKey) { + const parent = elementMap[element.parentKey]; + if (parent) { + if (!parent.children) { + parent.children = []; + } + parent.children.push(element.key); + } + } else { + root = element.key; + } + } + + return { root, elements: elementMap }; +} + +/** + * Type guard that validates a data part payload looks like a valid SpecDataPart. + */ +function isSpecDataPart(data: unknown): data is SpecDataPart { + if (typeof data !== "object" || data === null) return false; + const obj = data as Record; + switch (obj.type) { + case "patch": + return typeof obj.patch === "object" && obj.patch !== null; + case "flat": + case "nested": + return typeof obj.spec === "object" && obj.spec !== null; + default: + return false; + } +} + +/** + * Build a `Spec` by replaying all spec data parts from a message's parts array. + * Returns `null` if no spec data parts are present. + */ +export function buildSpecFromParts( + parts: DataPart[], + snapshot = true, +): Spec | null { + const spec: Spec = { root: "", elements: {} }; + let hasSpec = false; + + for (const part of parts) { + if (part.type === SPEC_DATA_PART_TYPE) { + if (!isSpecDataPart(part.data)) continue; + const payload = part.data; + if (payload.type === "patch") { + hasSpec = true; + applySpecPatch( + spec, + snapshot ? $state.snapshot(payload.patch) : payload.patch, + ); + } else if (payload.type === "flat") { + hasSpec = true; + Object.assign(spec, payload.spec); + } else if (payload.type === "nested") { + hasSpec = true; + const flat = nestedToFlat(payload.spec); + Object.assign(spec, flat); + } + } + } + + return hasSpec ? spec : null; +} + +/** + * Extract and join all text content from a message's parts array. + */ +export function getTextFromParts(parts: DataPart[]): string { + return parts + .filter( + (p): p is DataPart & { text: string } => + p.type === "text" && typeof p.text === "string", + ) + .map((p) => p.text.trim()) + .filter(Boolean) + .join("\n\n"); +} diff --git a/packages/svelte/src/utils.test.ts b/packages/svelte/src/utils.test.ts new file mode 100644 index 00000000..afe595f8 --- /dev/null +++ b/packages/svelte/src/utils.test.ts @@ -0,0 +1,379 @@ +import { describe, it, expect } from "vitest"; +import { + flatToTree, + buildSpecFromParts, + getTextFromParts, +} from "./utils.svelte.js"; +import type { FlatElement, SpecDataPart } from "@json-render/core"; +import { SPEC_DATA_PART_TYPE } from "@json-render/core"; + +describe("flatToTree", () => { + it("converts array of elements to tree structure", () => { + const elements: FlatElement[] = [ + { key: "root", type: "Container", props: {}, parentKey: undefined }, + { + key: "child1", + type: "Text", + props: { text: "Hello" }, + parentKey: "root", + }, + ]; + + const spec = flatToTree(elements); + + expect(spec.root).toBe("root"); + expect(spec.elements["root"]).toBeDefined(); + expect(spec.elements["child1"]).toBeDefined(); + }); + + it("builds parent-child relationships", () => { + const elements: FlatElement[] = [ + { key: "root", type: "Container", props: {}, parentKey: undefined }, + { key: "child1", type: "Text", props: {}, parentKey: "root" }, + { key: "child2", type: "Text", props: {}, parentKey: "root" }, + ]; + + const spec = flatToTree(elements); + + expect(spec.elements["root"]?.children).toEqual(["child1", "child2"]); + }); + + it("handles single root element", () => { + const elements: FlatElement[] = [ + { + key: "only", + type: "Text", + props: { text: "Solo" }, + parentKey: undefined, + }, + ]; + + const spec = flatToTree(elements); + + expect(spec.root).toBe("only"); + expect(spec.elements["only"]?.children).toEqual([]); + }); + + it("handles deeply nested elements", () => { + const elements: FlatElement[] = [ + { key: "root", type: "Container", props: {}, parentKey: undefined }, + { key: "level1", type: "Container", props: {}, parentKey: "root" }, + { key: "level2", type: "Container", props: {}, parentKey: "level1" }, + { key: "level3", type: "Text", props: {}, parentKey: "level2" }, + ]; + + const spec = flatToTree(elements); + + expect(spec.elements["root"]?.children).toEqual(["level1"]); + expect(spec.elements["level1"]?.children).toEqual(["level2"]); + expect(spec.elements["level2"]?.children).toEqual(["level3"]); + expect(spec.elements["level3"]?.children).toEqual([]); + }); + + it("preserves element props", () => { + const elements: FlatElement[] = [ + { + key: "root", + type: "Card", + props: { title: "Hello", value: 42 }, + parentKey: undefined, + }, + ]; + + const spec = flatToTree(elements); + + expect(spec.elements["root"]?.props).toEqual({ title: "Hello", value: 42 }); + }); + + it("preserves visibility conditions", () => { + const elements: FlatElement[] = [ + { + key: "root", + type: "Container", + props: {}, + parentKey: undefined, + visible: { $state: "/isVisible" }, + }, + ]; + + const spec = flatToTree(elements); + + expect(spec.elements["root"]?.visible).toEqual({ $state: "/isVisible" }); + }); + + it("handles elements with undefined parentKey as root", () => { + const elements: FlatElement[] = [ + { key: "a", type: "Text", props: {}, parentKey: undefined }, + ]; + + const spec = flatToTree(elements); + + expect(spec.root).toBe("a"); + }); + + it("handles empty elements array", () => { + const spec = flatToTree([]); + + expect(spec.root).toBe(""); + expect(spec.elements).toEqual({}); + }); + + it("handles multiple children correctly", () => { + const elements: FlatElement[] = [ + { key: "root", type: "Container", props: {}, parentKey: undefined }, + { key: "a", type: "Text", props: {}, parentKey: "root" }, + { key: "b", type: "Text", props: {}, parentKey: "root" }, + { key: "c", type: "Text", props: {}, parentKey: "root" }, + ]; + + const spec = flatToTree(elements); + + expect(spec.elements["root"]?.children).toHaveLength(3); + expect(spec.elements["root"]?.children).toContain("a"); + expect(spec.elements["root"]?.children).toContain("b"); + expect(spec.elements["root"]?.children).toContain("c"); + }); +}); + +describe("buildSpecFromParts", () => { + it("returns null when no data-spec parts are present", () => { + const parts = [ + { type: "text", text: "Hello world" }, + { type: "other", data: {} }, + ]; + + const spec = buildSpecFromParts(parts); + + expect(spec).toBeNull(); + }); + + it("builds a spec from patch parts", () => { + const parts = [ + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "patch", + patch: { op: "add", path: "/root", value: "main" }, + } satisfies SpecDataPart, + }, + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "patch", + patch: { + op: "add", + path: "/elements/main", + value: { type: "Text", props: { text: "Hi" }, children: [] }, + }, + } satisfies SpecDataPart, + }, + ]; + + const spec = buildSpecFromParts(parts); + + expect(spec).not.toBeNull(); + expect(spec?.root).toBe("main"); + expect(spec?.elements["main"]?.type).toBe("Text"); + }); + + it("handles flat spec parts", () => { + const parts = [ + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "flat", + spec: { + root: "root", + elements: { + root: { type: "Container", props: {}, children: [] }, + }, + }, + } satisfies SpecDataPart, + }, + ]; + + const spec = buildSpecFromParts(parts); + + expect(spec?.root).toBe("root"); + expect(spec?.elements["root"]?.type).toBe("Container"); + }); + + it("ignores non-spec parts", () => { + const parts = [ + { type: "text", text: "Some text" }, + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "flat", + spec: { + root: "r", + elements: { r: { type: "Text", props: {}, children: [] } }, + }, + } satisfies SpecDataPart, + }, + { type: "tool-call", data: {} }, + ]; + + const spec = buildSpecFromParts(parts); + + expect(spec?.root).toBe("r"); + }); + + it("applies patches incrementally", () => { + const parts = [ + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "patch", + patch: { op: "add", path: "/root", value: "a" }, + } satisfies SpecDataPart, + }, + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "patch", + patch: { + op: "add", + path: "/elements/a", + value: { type: "Text", props: { n: 1 }, children: [] }, + }, + } satisfies SpecDataPart, + }, + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "patch", + patch: { op: "replace", path: "/elements/a/props/n", value: 2 }, + } satisfies SpecDataPart, + }, + ]; + + const spec = buildSpecFromParts(parts); + + expect((spec?.elements["a"]?.props as { n: number }).n).toBe(2); + }); + + it("handles nested spec parts via nestedToFlat", () => { + const parts = [ + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "nested", + spec: { + type: "Container", + props: {}, + children: [{ type: "Text", props: { t: "x" } }], + }, + } satisfies SpecDataPart, + }, + ]; + + const spec = buildSpecFromParts(parts); + + expect(spec).not.toBeNull(); + expect(Object.keys(spec?.elements ?? {}).length).toBeGreaterThan(0); + }); + + it("handles mixed patch + flat + nested parts in sequence", () => { + const parts = [ + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "patch", + patch: { op: "add", path: "/root", value: "initial" }, + } satisfies SpecDataPart, + }, + { + type: SPEC_DATA_PART_TYPE, + data: { + type: "flat", + spec: { + root: "replaced", + elements: { replaced: { type: "Box", props: {}, children: [] } }, + }, + } satisfies SpecDataPart, + }, + ]; + + const spec = buildSpecFromParts(parts); + + expect(spec?.root).toBe("replaced"); + }); + + it("returns empty elements map from empty parts list", () => { + const spec = buildSpecFromParts([]); + + expect(spec).toBeNull(); + }); +}); + +describe("getTextFromParts", () => { + it("extracts text from text parts", () => { + const parts = [ + { type: "text", text: "Hello" }, + { type: "text", text: "World" }, + ]; + + const text = getTextFromParts(parts); + + expect(text).toBe("Hello\n\nWorld"); + }); + + it("returns empty string when no text parts", () => { + const parts = [ + { type: "data", data: {} }, + { type: "tool-call", data: {} }, + ]; + + const text = getTextFromParts(parts); + + expect(text).toBe(""); + }); + + it("ignores non-text parts", () => { + const parts = [ + { type: "text", text: "Keep" }, + { type: "data", data: {} }, + { type: "text", text: "This" }, + ]; + + const text = getTextFromParts(parts); + + expect(text).toBe("Keep\n\nThis"); + }); + + it("trims whitespace from text parts", () => { + const parts = [ + { type: "text", text: " Hello " }, + { type: "text", text: "\n\nWorld\n\n" }, + ]; + + const text = getTextFromParts(parts); + + expect(text).toBe("Hello\n\nWorld"); + }); + + it("skips empty text parts", () => { + const parts = [ + { type: "text", text: "Hello" }, + { type: "text", text: " " }, + { type: "text", text: "World" }, + ]; + + const text = getTextFromParts(parts); + + expect(text).toBe("Hello\n\nWorld"); + }); + + it("ignores text parts with non-string text field", () => { + const parts = [ + { type: "text", text: "Valid" }, + { type: "text", text: 123 as unknown as string }, + { type: "text", text: "Also Valid" }, + ]; + + const text = getTextFromParts(parts); + + expect(text).toBe("Valid\n\nAlso Valid"); + }); +}); diff --git a/packages/svelte/svelte.config.js b/packages/svelte/svelte.config.js new file mode 100644 index 00000000..4abe94e8 --- /dev/null +++ b/packages/svelte/svelte.config.js @@ -0,0 +1,6 @@ +/** @type {import('@sveltejs/package').Config} */ +export default { + compilerOptions: { + runes: true, // ensure no legacy syntax sneaks into this code base + }, +}; diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json new file mode 100644 index 00000000..9dacbd5c --- /dev/null +++ b/packages/svelte/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@internal/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["svelte"], + "verbatimModuleSyntax": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "./**/*.test.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fbade29..e0e1f739 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,15 +11,21 @@ importers: '@changesets/cli': specifier: 2.29.8 version: 2.29.8(@types/node@22.19.6) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 '@testing-library/react': specifier: ^16.3.1 - version: 16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@testing-library/svelte': + specifier: ^5.2.0 + version: 5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@types/react': specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.14 husky: specifier: ^9.1.7 version: 9.1.7 @@ -38,6 +44,9 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + svelte: + specifier: ^5.0.0 + version: 5.53.5 turbo: specifier: ^2.7.4 version: 2.7.4 @@ -46,13 +55,13 @@ importers: version: 5.9.2 vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) apps/web: dependencies: '@ai-sdk/gateway': specifier: ^3.0.13 - version: 3.0.13(zod@4.3.6) + version: 3.0.57(zod@4.3.6) '@ai-sdk/react': specifier: 3.0.79 version: 3.0.79(react@19.2.3)(zod@4.3.6) @@ -94,19 +103,19 @@ importers: version: 1.36.1 '@vercel/analytics': specifier: ^1.6.1 - version: 1.6.1(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vue@3.5.29(typescript@5.9.2)) + version: 1.6.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.2)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(svelte@5.53.5)(vue@3.5.29(typescript@5.9.2)) '@vercel/speed-insights': specifier: ^1.3.1 - version: 1.3.1(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vue@3.5.29(typescript@5.9.2)) + version: 1.3.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.2)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(svelte@5.53.5)(vue@3.5.29(typescript@5.9.2)) '@visual-json/react': specifier: 0.1.1 version: 0.1.1(react@19.2.3) ai: specifier: ^6.0.33 - version: 6.0.33(zod@4.3.6) + version: 6.0.103(zod@4.3.6) bash-tool: specifier: 1.3.14 - version: 1.3.14(ai@6.0.33(zod@4.3.6))(just-bash@2.9.6) + version: 1.3.14(ai@6.0.103(zod@4.3.6))(just-bash@2.9.6) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -157,7 +166,7 @@ importers: version: 2.1.0(react@19.2.3) tailwind-merge: specifier: ^3.4.0 - version: 3.4.0 + version: 3.5.0 unist-util-visit: specifier: 5.1.0 version: 5.1.0 @@ -197,7 +206,7 @@ importers: version: 8.5.6 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.1 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -209,7 +218,7 @@ importers: dependencies: '@ai-sdk/gateway': specifier: ^3.0.13 - version: 3.0.42(zod@4.3.6) + version: 3.0.57(zod@4.3.6) '@ai-sdk/react': specifier: ^3.0.84 version: 3.0.84(react@19.2.4)(zod@4.3.6) @@ -233,7 +242,7 @@ importers: version: 1.0.2(react@19.2.4) ai: specifier: ^6.0.33 - version: 6.0.82(zod@4.3.6) + version: 6.0.103(zod@4.3.6) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -269,7 +278,7 @@ importers: version: 2.2.0(react@19.2.4) tailwind-merge: specifier: ^3.4.0 - version: 3.4.0 + version: 3.5.0 three: specifier: ^0.182.0 version: 0.182.0 @@ -303,19 +312,19 @@ importers: version: 8.5.6 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.1 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 typescript: specifier: ^5.7.2 - version: 5.9.2 + version: 5.9.3 examples/dashboard: dependencies: '@ai-sdk/gateway': specifier: ^3.0.13 - version: 3.0.13(zod@4.3.6) + version: 3.0.57(zod@4.3.6) '@dnd-kit/core': specifier: 6.3.1 version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -336,7 +345,7 @@ importers: version: link:../../packages/react ai: specifier: ^6.0.33 - version: 6.0.33(zod@4.3.6) + version: 6.0.103(zod@4.3.6) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -378,7 +387,7 @@ importers: version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tailwind-merge: specifier: ^3.4.0 - version: 3.4.0 + version: 3.5.0 vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -415,7 +424,7 @@ importers: version: 8.5.6 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.1 tsx: specifier: ^4.21.0 version: 4.21.0 @@ -424,7 +433,7 @@ importers: version: 1.4.0 typescript: specifier: ^5.7.2 - version: 5.9.2 + version: 5.9.3 examples/image: dependencies: @@ -472,13 +481,13 @@ importers: version: 4.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) satori: specifier: ^0.19.2 - version: 0.19.2 + version: 0.19.3 tailwind-merge: specifier: ^3.5.0 version: 3.5.0 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.1 zod: specifier: 4.3.6 version: 4.3.6 @@ -548,7 +557,7 @@ importers: version: 19.2.4(react@19.2.4) tailwind-merge: specifier: ^3.4.0 - version: 3.4.1 + version: 3.5.0 zod: specifier: 4.3.6 version: 4.3.6 @@ -576,19 +585,19 @@ importers: version: 8.5.6 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.1 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 typescript: specifier: ^5.7.2 - version: 5.9.2 + version: 5.9.3 examples/react-email: dependencies: '@ai-sdk/gateway': specifier: ^3.0.52 - version: 3.0.57(zod@4.3.5) + version: 3.0.57(zod@4.3.6) '@json-render/core': specifier: workspace:* version: link:../../packages/core @@ -600,7 +609,7 @@ importers: version: 4.1.18 ai: specifier: 6.0.94 - version: 6.0.94(zod@4.3.5) + version: 6.0.94(zod@4.3.6) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -630,10 +639,10 @@ importers: version: 3.5.0 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.1 zod: - specifier: 4.3.5 - version: 4.3.5 + specifier: 4.3.6 + version: 4.3.6 devDependencies: '@internal/typescript-config': specifier: workspace:* @@ -649,19 +658,19 @@ importers: version: 19.2.3(@types/react@19.2.3) shadcn: specifier: ^3.8.5 - version: 3.8.5(@types/node@22.19.6)(typescript@5.9.2) + version: 3.8.5(@types/node@22.19.6)(typescript@5.9.3) tw-animate-css: specifier: ^1.4.0 version: 1.4.0 typescript: specifier: ^5.7.2 - version: 5.9.2 + version: 5.9.3 examples/react-native: dependencies: '@ai-sdk/gateway': specifier: ^3.0.39 - version: 3.0.39(zod@4.3.6) + version: 3.0.57(zod@4.3.6) '@expo/vector-icons': specifier: ^15.0.3 version: 15.0.3(expo-font@14.0.11(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -673,7 +682,7 @@ importers: version: link:../../packages/react-native ai: specifier: ^6.0.77 - version: 6.0.77(zod@4.3.6) + version: 6.0.103(zod@4.3.6) expo: specifier: ~54.0.33 version: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -722,7 +731,7 @@ importers: dependencies: '@ai-sdk/gateway': specifier: ^3.0.52 - version: 3.0.52(zod@4.3.6) + version: 3.0.57(zod@4.3.6) '@json-render/core': specifier: workspace:* version: link:../../packages/core @@ -764,7 +773,7 @@ importers: version: 3.5.0 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.1 zod: specifier: 4.3.6 version: 4.3.6 @@ -783,19 +792,19 @@ importers: version: 19.2.3(@types/react@19.2.3) shadcn: specifier: ^3.8.5 - version: 3.8.5(@types/node@22.19.6)(typescript@5.9.2) + version: 3.8.5(@types/node@22.19.6)(typescript@5.9.3) tw-animate-css: specifier: ^1.4.0 version: 1.4.0 typescript: specifier: ^5.7.2 - version: 5.9.2 + version: 5.9.3 examples/remotion: dependencies: '@ai-sdk/gateway': specifier: ^3.0.13 - version: 3.0.13(zod@4.3.6) + version: 3.0.57(zod@4.3.6) '@json-render/core': specifier: workspace:* version: link:../../packages/core @@ -813,7 +822,7 @@ importers: version: 4.0.418(react-dom@19.2.3(react@19.2.3))(react@19.2.3) ai: specifier: ^6.0.70 - version: 6.0.70(zod@4.3.6) + version: 6.0.103(zod@4.3.6) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -840,7 +849,7 @@ importers: version: 3.21.0 tailwind-merge: specifier: ^3.4.0 - version: 3.4.0 + version: 3.5.0 zod: specifier: ^4.3.6 version: 4.3.6 @@ -865,19 +874,19 @@ importers: version: 8.5.6 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.1 typescript: specifier: ^5.7.2 - version: 5.9.2 + version: 5.9.3 examples/stripe-app/api: dependencies: '@ai-sdk/gateway': specifier: ^3.0.50 - version: 3.0.50(zod@4.3.6) + version: 3.0.57(zod@4.3.6) ai: specifier: ^6.0.91 - version: 6.0.91(zod@4.3.6) + version: 6.0.103(zod@4.3.6) next: specifier: ^16.1.6 version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -893,10 +902,10 @@ importers: version: 22.19.6 '@types/react': specifier: ^19.1.0 - version: 19.2.3 + version: 19.2.14 typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 examples/stripe-app/drawer-app: dependencies: @@ -954,6 +963,104 @@ importers: specifier: ^9.39.0 version: 9.39.2(jiti@2.6.1) + examples/svelte: + dependencies: + '@json-render/core': + specifier: workspace:* + version: link:../../packages/core + '@json-render/svelte': + specifier: workspace:* + version: link:../../packages/svelte + svelte: + specifier: ^5.49.2 + version: 5.53.5 + zod: + specifier: 4.3.6 + version: 4.3.6 + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + svelte-check: + specifier: ^4.3.6 + version: 4.4.3(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + + examples/svelte-chat: + dependencies: + '@ai-sdk/gateway': + specifier: ^3.0.46 + version: 3.0.57(zod@4.3.6) + '@ai-sdk/svelte': + specifier: ^4.0.96 + version: 4.0.100(svelte@5.53.5)(zod@4.3.6) + '@json-render/core': + specifier: workspace:* + version: link:../../packages/core + '@json-render/svelte': + specifier: workspace:* + version: link:../../packages/svelte + ai: + specifier: ^6.0.86 + version: 6.0.103(zod@4.3.6) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-svelte: + specifier: ^0.500.0 + version: 0.500.0(svelte@5.53.5) + tailwind-merge: + specifier: ^3.2.0 + version: 3.5.0 + zod: + specifier: 4.3.6 + version: 4.3.6 + devDependencies: + '@internationalized/date': + specifier: ^3.10.0 + version: 3.11.0 + '@lucide/svelte': + specifier: ^0.561.0 + version: 0.561.0(svelte@5.53.5) + '@sveltejs/adapter-auto': + specifier: ^7.0.0 + version: 7.0.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) + '@sveltejs/kit': + specifier: ^2.50.2 + version: 2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.2.1(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + bits-ui: + specifier: ^2.14.4 + version: 2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) + svelte: + specifier: ^5.49.2 + version: 5.53.5 + svelte-check: + specifier: ^4.3.6 + version: 4.4.3(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3) + tailwind-variants: + specifier: ^3.2.2 + version: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1) + tailwindcss: + specifier: ^4.0.0 + version: 4.2.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + examples/vite-renderers: dependencies: '@json-render/core': @@ -962,6 +1069,9 @@ importers: '@json-render/react': specifier: workspace:* version: link:../../packages/react + '@json-render/svelte': + specifier: workspace:* + version: link:../../packages/svelte '@json-render/vue': specifier: workspace:* version: link:../../packages/vue @@ -971,13 +1081,19 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + svelte: + specifier: ^5.49.2 + version: 5.53.5 vue: specifier: ^3.5.29 version: 3.5.29(typescript@5.9.3) zod: - specifier: ^4.3.6 + specifier: 4.3.6 version: 4.3.6 devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -986,16 +1102,16 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^5.1.4 - version: 5.1.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitejs/plugin-vue': specifier: ^6.0.4 - version: 6.0.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + version: 6.0.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) examples/vue: dependencies: @@ -1009,18 +1125,18 @@ importers: specifier: ^3.5.0 version: 3.5.29(typescript@5.9.3) zod: - specifier: ^4.3.6 + specifier: 4.3.6 version: 4.3.6 devDependencies: '@vitejs/plugin-vue': specifier: ^6.0.4 - version: 6.0.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + version: 6.0.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) vue-tsc: specifier: ^3.2.5 version: 3.2.5(typescript@5.9.3) @@ -1073,10 +1189,10 @@ importers: version: link:../typescript-config tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 packages/core: dependencies: @@ -1089,10 +1205,10 @@ importers: version: link:../typescript-config tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 packages/eslint-config: devDependencies: @@ -1125,10 +1241,10 @@ importers: version: 16.5.0 typescript: specifier: ^5.9.2 - version: 5.9.2 + version: 5.9.3 typescript-eslint: specifier: ^8.50.0 - version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) + version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) packages/image: dependencies: @@ -1143,7 +1259,7 @@ importers: version: 19.2.4 satori: specifier: ^0.19.2 - version: 0.19.2 + version: 0.19.3 devDependencies: '@internal/typescript-config': specifier: workspace:* @@ -1175,10 +1291,10 @@ importers: version: 2.18.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 packages/react: dependencies: @@ -1187,7 +1303,7 @@ importers: version: link:../core react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 devDependencies: '@internal/react-state': specifier: workspace:* @@ -1200,10 +1316,10 @@ importers: version: 19.2.3 tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 packages/react-email: dependencies: @@ -1228,10 +1344,10 @@ importers: version: 19.2.3 tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 zod: specifier: ^4.0.0 version: 4.3.6 @@ -1259,10 +1375,10 @@ importers: version: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 zod: specifier: ^4.3.6 version: 4.3.6 @@ -1290,10 +1406,10 @@ importers: version: 19.2.3 tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 zod: specifier: ^4.3.6 version: 4.3.6 @@ -1321,10 +1437,10 @@ importers: version: 19.2.4(react@19.2.4) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 packages/redux: dependencies: @@ -1343,10 +1459,10 @@ importers: version: 5.0.1 tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 packages/remotion: dependencies: @@ -1368,10 +1484,10 @@ importers: version: 4.0.418(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 zod: specifier: ^4.3.6 version: 4.3.6 @@ -1407,10 +1523,10 @@ importers: version: 19.2.4(react@19.2.4) tailwind-merge: specifier: ^3.4.1 - version: 3.4.1 + version: 3.5.0 tailwindcss: specifier: ^4.0.0 - version: 4.1.18 + version: 4.2.1 vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1423,14 +1539,39 @@ importers: version: 19.2.3 tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 zod: specifier: ^4.3.6 version: 4.3.6 + packages/svelte: + dependencies: + '@json-render/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@internal/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@sveltejs/package': + specifier: ^2.3.0 + version: 2.5.7(svelte@5.53.5)(typescript@5.9.3) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + svelte: + specifier: ^5.0.0 + version: 5.53.5 + svelte-check: + specifier: ^4.0.0 + version: 4.4.3(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3) + typescript: + specifier: ^5.4.5 + version: 5.9.3 + packages/typescript-config: {} packages/ui: @@ -1481,13 +1622,13 @@ importers: version: 2.4.6 tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 vue: specifier: ^3.5.0 - version: 3.5.29(typescript@5.9.2) + version: 3.5.29(typescript@5.9.3) packages/xstate: dependencies: @@ -1519,10 +1660,10 @@ importers: version: link:../typescript-config tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.5 - version: 5.9.2 + version: 5.9.3 zustand: specifier: ^5.0.11 version: 5.0.11(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) @@ -1531,7 +1672,7 @@ importers: devDependencies: '@ai-sdk/gateway': specifier: ^3.0.53 - version: 3.0.53(zod@4.3.6) + version: 3.0.57(zod@4.3.6) '@json-render/core': specifier: workspace:* version: link:../../packages/core @@ -1552,7 +1693,7 @@ importers: version: 2.11.2(react@19.2.4) ai: specifier: ^6.0.97 - version: 6.0.97(zod@4.3.6) + version: 6.0.103(zod@4.3.6) jotai: specifier: ^2.18.0 version: 2.18.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) @@ -1561,7 +1702,7 @@ importers: version: 5.0.1 vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.3.6 version: 4.3.6 @@ -1582,18 +1723,6 @@ packages: '@acemir/cssom@0.9.31': resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} - '@ai-sdk/gateway@3.0.13': - resolution: {integrity: sha512-g7nE4PFtngOZNZSy1lOPpkC+FAiHxqBJXqyRMEG7NUrEVZlz5goBdtHg1YgWRJIX776JTXAmbOI5JreAKVAsVA==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - '@ai-sdk/gateway@3.0.33': - resolution: {integrity: sha512-elnzKRxkC8ZL3IvOdklavkYTBgJhjP9l8b5MO6WYz1MBoT/0WdJoG3Jp31Olwpzk4hIac7z27S6a4q7DkhzsZg==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.39': resolution: {integrity: sha512-SeCZBAdDNbWpVUXiYgOAqis22p5MEYfrjRw0hiBa5hM+7sDGYQpMinUjkM8kbPXMkY+AhKLrHleBl+SuqpzlgA==} engines: {node: '>=18'} @@ -1606,20 +1735,14 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.50': - resolution: {integrity: sha512-Jdd1a8VgbD7l7r+COj0h5SuaYRfPvOJ/AO6l0OrmTPEcI2MUQPr3C4JttfpNkcheEN+gOdy0CtZWuG17bW2fjw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.52': resolution: {integrity: sha512-lYCXP8T3YnIDiz8DP7loAMT27wnblc3IAYzQ7igg89RCRyTUjk6ffbxHXXQ5Pmv8jrdLF0ZIJnH54Dsr1OCKHg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.53': - resolution: {integrity: sha512-QT3FEoNARMRlk8JJVR7L98exiK9C8AGfrEJVbRxBT1yIXKs/N19o/+PsjTRVsARgDJNcy9JbJp1FspKucEat0Q==} + '@ai-sdk/gateway@3.0.55': + resolution: {integrity: sha512-7xMeTJnCjwRwXKVCiv4Ly4qzWvDuW3+W1WIV0X1EFu6W83d4mEhV9bFArto10MeTw40ewuDjrbrZd21mXKohkw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -1630,12 +1753,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.13': - resolution: {integrity: sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.14': resolution: {integrity: sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==} engines: {node: '>=18'} @@ -1648,20 +1765,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.5': - resolution: {integrity: sha512-Ow/X/SEkeExTTc1x+nYLB9ZHK2WUId8+9TlkamAx7Tl9vxU+cKzWx2dwjgMHeCN6twrgwkLrrtqckQeO4mxgVA==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - '@ai-sdk/provider@3.0.2': - resolution: {integrity: sha512-HrEmNt/BH/hkQ7zpi2o6N3k1ZR1QTb7z85WYhYygiTxOQuaml4CMtHCWRbric5WPU+RNsYI7r1EpyVQMKO1pYw==} - engines: {node: '>=18'} - - '@ai-sdk/provider@3.0.7': - resolution: {integrity: sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw==} - engines: {node: '>=18'} - '@ai-sdk/provider@3.0.8': resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} @@ -1678,6 +1781,11 @@ packages: peerDependencies: react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@ai-sdk/svelte@4.0.100': + resolution: {integrity: sha512-sr9vml3DU/KY+zw8eko8LDbF/PsZHPXbJP/+EjdafNCSa3Wm+oUeKY7hzl7wbuX2/Z/i1/f0tnY5wyfTvy+NuQ==} + peerDependencies: + svelte: ^5.31.0 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1733,10 +1841,6 @@ packages: '@babel/code-frame@7.10.4': resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} - '@babel/code-frame@7.28.6': - resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -3423,6 +3527,9 @@ packages: '@types/node': optional: true + '@internationalized/date@3.11.0': + resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -3564,6 +3671,11 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lucide/svelte@0.561.0': + resolution: {integrity: sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==} + peerDependencies: + svelte: ^5 + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -3784,6 +3896,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -5627,42 +5742,123 @@ packages: '@stripe/ui-extension-tools@0.0.1': resolution: {integrity: sha512-0pOgQ3AuEUeypAgAhcJbyC9QxMaMW1OqzzxkCO4a+5ALDyIFEA6M4jDQ3H9KayNwqQ23qq+PQ0rlYY7dRACNgA==} + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-auto@7.0.1': + resolution: {integrity: sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/kit@2.53.2': + resolution: {integrity: sha512-M+MqAvFve12T1HWws/2npP/s3hFtyjw3GB/OXW/8a1jZBk48qnvPJrtgE+VOMc3RnjUMxc4mv/vQ73nvj2uNMg==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + + '@sveltejs/package@2.5.7': + resolution: {integrity: sha512-qqD9xa9H7TDiGFrF6rz7AirOR8k15qDK/9i4MIE8te4vWsv5GEogPks61rrZcLy+yWph+aI6pIj2MdoK3YI8AQ==} + engines: {node: ^16.14 || >=18} + hasBin: true + peerDependencies: + svelte: ^3.44.0 || ^4.0.0 || ^5.0.0-next.1 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': + resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.4': + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + '@tailwindcss/oxide-android-arm64@4.1.18': resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [android] + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + '@tailwindcss/oxide-darwin-arm64@4.1.18': resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + '@tailwindcss/oxide-darwin-x64@4.1.18': resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + '@tailwindcss/oxide-freebsd-x64@4.1.18': resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} engines: {node: '>= 10'} @@ -5670,6 +5866,13 @@ packages: os: [linux] libc: [glibc] + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} @@ -5677,6 +5880,13 @@ packages: os: [linux] libc: [musl] + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} @@ -5684,6 +5894,13 @@ packages: os: [linux] libc: [glibc] + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} @@ -5691,6 +5908,13 @@ packages: os: [linux] libc: [musl] + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} engines: {node: '>=14.0.0'} @@ -5703,31 +5927,64 @@ packages: - '@emnapi/wasi-threads' - tslib + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + '@tailwindcss/oxide@4.1.18': resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} engines: {node: '>= 10'} + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + '@tailwindcss/postcss@4.1.18': resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/react@16.3.1': - resolution: {integrity: sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -5741,19 +5998,23 @@ packages: '@types/react-dom': optional: true - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 + '@testing-library/svelte-core@1.0.0': + resolution: {integrity: sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==} + engines: {node: '>=16'} + peerDependencies: + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + + '@testing-library/svelte@5.3.1': + resolution: {integrity: sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w==} + engines: {node: '>= 10'} + peerDependencies: + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + vite: '*' + vitest: '*' peerDependenciesMeta: - '@types/react': + vite: optional: true - '@types/react-dom': + vitest: optional: true '@tokenizer/inflate@0.4.1': @@ -5791,6 +6052,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -5930,6 +6194,9 @@ packages: '@types/three@0.182.0': resolution: {integrity: sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -6374,20 +6641,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - ai@6.0.103: - resolution: {integrity: sha512-4eY6Ut4u41zKH+P2S/oLlZrwxeWQh4kIV1FjE34Jhoiwg+v1AyfSYM8FslXk9rTAtIIaOBimrCUqXacC5RBqJw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - ai@6.0.33: - resolution: {integrity: sha512-bVokbmy2E2QF6Efl+5hOJx5MRWoacZ/CZY/y1E+VcewknvGlgaiCzMu8Xgddz6ArFJjiMFNUPHKxAhIePE4rmg==} + ai@6.0.100: + resolution: {integrity: sha512-BIxhG7M7wvcWCF+IEnZi7WpkRLOM3jR2vJ0mMuohl2UB2i1R/ZUa1cHFel1xI8nWvyUpOoQXKqsM0BAH50EYSQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - ai@6.0.70: - resolution: {integrity: sha512-1Osgqs/HSCqKNQt+u5THWI4sBpHZefiQWZIPv+MRJfIx7tGX34IMtXBDs05tZ6yW2P06fmB03w94UkPXWfdieA==} + ai@6.0.103: + resolution: {integrity: sha512-4eY6Ut4u41zKH+P2S/oLlZrwxeWQh4kIV1FjE34Jhoiwg+v1AyfSYM8FslXk9rTAtIIaOBimrCUqXacC5RBqJw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -6404,24 +6665,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - ai@6.0.91: - resolution: {integrity: sha512-k1/8BusZMhYVxxLZt0BUZzm9HVDCCh117nyWfWUx5xjR2+tWisJbXgysL7EBMq2lgyHwgpA1jDR3tVjWSdWZXw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - ai@6.0.94: resolution: {integrity: sha512-/F9wh262HbK05b/5vILh38JvPiheonT+kBj1L97712E7VPchqmcx7aJuZN3QSk5Pj6knxUJLm2FFpYJI1pHXUA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - ai@6.0.97: - resolution: {integrity: sha512-eZIAcBymwGhBwncRH/v9pillZNMeRCDkc4BwcvwXerXd7sxjVxRis3ZNCNCpP02pVH4NLs81ljm4cElC4vbNcQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -6537,6 +6786,10 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -6598,6 +6851,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + babel-jest@27.5.1: resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6729,6 +6986,13 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + bits-ui@2.16.2: + resolution: {integrity: sha512-bgEpRRF7Ck9nRP1pbuKVxpaSMrz+8Pm0y+dmuvlkrSe+uUwIQECef29y6eslFHM6pCAubUh7STrsTLUUp8fzFQ==} + engines: {node: '>=20'} + peerDependencies: + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -6886,6 +7150,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -7084,6 +7352,10 @@ packages: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -7286,6 +7558,9 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -7369,6 +7644,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devalue@5.6.3: + resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -7612,8 +7890,8 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -7814,6 +8092,9 @@ packages: jiti: optional: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7831,6 +8112,9 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} + esrap@2.2.3: + resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -8809,6 +9093,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -9304,30 +9591,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.30.2: resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.30.2: resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.30.2: resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.30.2: resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.30.2: resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} @@ -9335,6 +9652,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} @@ -9342,6 +9666,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} @@ -9349,6 +9680,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} @@ -9356,22 +9694,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.30.2: resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.30.2: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -9403,6 +9764,9 @@ packages: resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} engines: {node: '>=8.9.0'} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -9485,6 +9849,11 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-svelte@0.500.0: + resolution: {integrity: sha512-fdCJ4z9FuMoXk/ZPv4V5JhpMzQuEsoVNARXEPB0/zTX5AXA3l0x6J5sVUy9y6Rxxi3DEFpa5PmMwcXuO/ksOGw==} + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -9868,6 +10237,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -10412,6 +10785,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -10759,6 +11133,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -10974,6 +11352,19 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -10992,8 +11383,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - satori@0.19.2: - resolution: {integrity: sha512-71plFHWcq6WJBM5sf/n0eHOmTBiKLUB/G8du7SmLTTLHKEKrV3TPHGKcEVIoyjnbhnjvu9HhLyF9MATB/zzL7g==} + satori@0.19.3: + resolution: {integrity: sha512-dKr8TNYSyceWqBoTHWntjy25xaiWMw5GF+f8QOqFsov9OpTswLs7xdbvZudGRp9jkzbhv/4mVjVZYFtpruGKiA==} engines: {node: '>=16'} sax@1.4.4: @@ -11031,6 +11422,9 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -11083,6 +11477,9 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-cookie-parser@3.0.1: + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -11166,6 +11563,10 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -11472,6 +11873,30 @@ packages: peerDependencies: react: '>=17.0' + svelte-check@4.4.3: + resolution: {integrity: sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-toolbelt@0.10.6: + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte2tsx@0.7.51: + resolution: {integrity: sha512-YbVMQi5LtQkVGOMdATTY8v3SMtkNjzYtrVDGaN3Bv+0LQ47tGXu/Oc8ryTkcYuEJWTZFJ8G2+2I8ORcQVGt9Ag==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 + + svelte@5.53.5: + resolution: {integrity: sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==} + engines: {node: '>=18'} + svg-arc-to-cubic-bezier@3.2.0: resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} @@ -11483,22 +11908,32 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - tailwind-merge@3.4.0: - resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} - - tailwind-merge@3.4.1: - resolution: {integrity: sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==} - tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + tailwind-variants@3.2.2: + resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -11513,6 +11948,7 @@ packages: tar@7.5.7: resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} @@ -11634,6 +12070,10 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -12095,6 +12535,14 @@ packages: yaml: optional: true + vitefu@1.1.2: + resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + vitest@4.0.17: resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -12442,6 +12890,9 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -12453,9 +12904,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.3.5: - resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -12503,20 +12951,6 @@ snapshots: '@acemir/cssom@0.9.31': {} - '@ai-sdk/gateway@3.0.13(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.2 - '@ai-sdk/provider-utils': 4.0.5(zod@4.3.6) - '@vercel/oidc': 3.1.0 - zod: 4.3.6 - - '@ai-sdk/gateway@3.0.33(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) - '@vercel/oidc': 3.1.0 - zod: 4.3.6 - '@ai-sdk/gateway@3.0.39(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -12531,20 +12965,6 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 4.3.6 - '@ai-sdk/gateway@3.0.50(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) - '@vercel/oidc': 3.1.0 - zod: 4.3.6 - - '@ai-sdk/gateway@3.0.52(zod@4.3.5)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.5) - '@vercel/oidc': 3.1.0 - zod: 4.3.5 - '@ai-sdk/gateway@3.0.52(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -12552,20 +12972,13 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 4.3.6 - '@ai-sdk/gateway@3.0.53(zod@4.3.6)': + '@ai-sdk/gateway@3.0.55(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) '@vercel/oidc': 3.1.0 zod: 4.3.6 - '@ai-sdk/gateway@3.0.57(zod@4.3.5)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.5) - '@vercel/oidc': 3.1.0 - zod: 4.3.5 - '@ai-sdk/gateway@3.0.57(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -12573,13 +12986,6 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 4.3.6 - '@ai-sdk/provider-utils@4.0.13(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.7 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.6 - zod: 4.3.6 - '@ai-sdk/provider-utils@4.0.14(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -12587,13 +12993,6 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.6 - '@ai-sdk/provider-utils@4.0.15(zod@4.3.5)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.6 - zod: 4.3.5 - '@ai-sdk/provider-utils@4.0.15(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -12601,21 +13000,6 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.6 - '@ai-sdk/provider-utils@4.0.5(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.2 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.6 - zod: 4.3.6 - - '@ai-sdk/provider@3.0.2': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/provider@3.0.7': - dependencies: - json-schema: 0.4.0 - '@ai-sdk/provider@3.0.8': dependencies: json-schema: 0.4.0 @@ -12640,6 +13024,14 @@ snapshots: transitivePeerDependencies: - zod + '@ai-sdk/svelte@4.0.100(svelte@5.53.5)(zod@4.3.6)': + dependencies: + '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) + ai: 6.0.100(zod@4.3.6) + svelte: 5.53.5 + transitivePeerDependencies: + - zod + '@alloc/quick-lru@5.2.0': {} '@ant-design/colors@8.0.1': @@ -12717,12 +13109,6 @@ snapshots: dependencies: '@babel/highlight': 7.25.9 - '@babel/code-frame@7.28.6': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -14256,7 +14642,7 @@ snapshots: glob: 13.0.1 hermes-parser: 0.29.1 jsc-safe-url: 0.2.4 - lightningcss: 1.30.2 + lightningcss: 1.31.1 minimatch: 9.0.5 postcss: 8.4.49 resolve-from: 5.0.0 @@ -14559,6 +14945,10 @@ snapshots: optionalDependencies: '@types/node': 22.19.6 + '@internationalized/date@3.11.0': + dependencies: + '@swc/helpers': 0.5.15 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.1': @@ -14840,6 +15230,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lucide/svelte@0.561.0(svelte@5.53.5)': + dependencies: + svelte: 5.53.5 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.6 @@ -15046,6 +15440,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@polka/url@1.0.0-next.29': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -17201,7 +17597,7 @@ snapshots: dependencies: '@react-email/text': 0.1.6(react@19.2.4) react: 19.2.4 - tailwindcss: 4.1.18 + tailwindcss: 4.2.1 optionalDependencies: '@react-email/body': 0.2.1(react@19.2.4) '@react-email/button': 0.2.1(react@19.2.4) @@ -18153,6 +18549,85 @@ snapshots: - ts-node - utf-8-validate + '@sveltejs/acorn-typescript@1.0.9(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))': + dependencies: + '@sveltejs/kit': 2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + + '@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.2)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.6.3 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.0.1 + sirv: 3.0.2 + svelte: 5.53.5 + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + typescript: 5.9.2 + optional: true + + '@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.6.3 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.0.1 + sirv: 3.0.2 + svelte: 5.53.5 + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + typescript: 5.9.3 + + '@sveltejs/package@2.5.7(svelte@5.53.5)(typescript@5.9.3)': + dependencies: + chokidar: 5.0.0 + kleur: 4.1.5 + sade: 1.8.1 + semver: 7.7.3 + svelte: 5.53.5 + svelte2tsx: 0.7.51(svelte@5.53.5)(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + obug: 2.1.1 + svelte: 5.53.5 + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.53.5 + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.2(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -18160,49 +18635,95 @@ snapshots: '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 jiti: 2.6.1 lightningcss: 1.30.2 magic-string: 0.30.21 source-map-js: 1.2.1 tailwindcss: 4.1.18 + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + '@tailwindcss/oxide-android-arm64@4.1.18': optional: true + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + '@tailwindcss/oxide-darwin-arm64@4.1.18': optional: true + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + '@tailwindcss/oxide-darwin-x64@4.1.18': optional: true + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + '@tailwindcss/oxide-freebsd-x64@4.1.18': optional: true + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': optional: true + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': optional: true + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': optional: true + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': optional: true + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + '@tailwindcss/oxide-linux-x64-musl@4.1.18': optional: true + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + '@tailwindcss/oxide-wasm32-wasi@4.1.18': optional: true + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': optional: true + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': optional: true + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + '@tailwindcss/oxide@4.1.18': optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.18 @@ -18218,6 +18739,21 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + '@tailwindcss/postcss@4.1.18': dependencies: '@alloc/quick-lru': 5.2.0 @@ -18226,9 +18762,16 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -18237,15 +18780,15 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/react@16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@babel/runtime': 7.28.6 '@testing-library/dom': 10.4.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.2.3 - '@types/react-dom': 19.2.3(@types/react@19.2.3) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: @@ -18257,6 +18800,19 @@ snapshots: '@types/react': 19.2.3 '@types/react-dom': 19.2.3(@types/react@19.2.3) + '@testing-library/svelte-core@1.0.0(svelte@5.53.5)': + dependencies: + svelte: 5.53.5 + + '@testing-library/svelte@5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/svelte-core': 1.0.0(svelte@5.53.5) + svelte: 5.53.5 + optionalDependencies: + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -18304,6 +18860,8 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/cookie@0.6.0': {} + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -18461,6 +19019,8 @@ snapshots: fflate: 0.8.2 meshoptimizer: 0.22.0 + '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -18507,19 +19067,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.53.0 eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.2) - typescript: 5.9.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -18535,24 +19095,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2)': + '@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.53.0 '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.53.0(typescript@5.9.2)': + '@typescript-eslint/project-service@8.53.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) '@typescript-eslint/types': 8.53.0 debug: 4.4.3 - typescript: 5.9.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -18566,9 +19126,9 @@ snapshots: '@typescript-eslint/types': 8.53.0 '@typescript-eslint/visitor-keys': 8.53.0 - '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.2)': + '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)': dependencies: - typescript: 5.9.2 + typescript: 5.9.3 '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@4.9.5)': dependencies: @@ -18582,15 +19142,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.2) - typescript: 5.9.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -18612,18 +19172,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.2)': + '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.53.0(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.2) + '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) '@typescript-eslint/types': 8.53.0 '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.2) - typescript: 5.9.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -18642,14 +19202,14 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2)': + '@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.53.0 '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -18697,18 +19257,22 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.4 - '@vercel/analytics@1.6.1(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vue@3.5.29(typescript@5.9.2))': + '@vercel/analytics@1.6.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.2)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(svelte@5.53.5)(vue@3.5.29(typescript@5.9.2))': optionalDependencies: + '@sveltejs/kit': 2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.2)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) next: 16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 + svelte: 5.53.5 vue: 3.5.29(typescript@5.9.2) '@vercel/oidc@3.1.0': {} - '@vercel/speed-insights@1.3.1(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vue@3.5.29(typescript@5.9.2))': + '@vercel/speed-insights@1.3.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.2)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(svelte@5.53.5)(vue@3.5.29(typescript@5.9.2))': optionalDependencies: + '@sveltejs/kit': 2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.2)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) next: 16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 + svelte: 5.53.5 vue: 3.5.29(typescript@5.9.2) '@visual-json/core@0.1.1': {} @@ -18718,7 +19282,7 @@ snapshots: '@visual-json/core': 0.1.1 react: 19.2.3 - '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -18726,14 +19290,14 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) vue: 3.5.29(typescript@5.9.3) '@vitest/expect@4.0.17': @@ -18745,23 +19309,23 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@22.19.6)(typescript@5.9.2) - vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@22.19.6)(typescript@5.9.3) - vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.17': dependencies: @@ -18858,6 +19422,7 @@ snapshots: '@vue/compiler-ssr': 3.5.29 '@vue/shared': 3.5.29 vue: 3.5.29(typescript@5.9.2) + optional: true '@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3))': dependencies: @@ -19003,27 +19568,19 @@ snapshots: agent-base@7.1.4: {} - ai@6.0.103(zod@4.3.6): + ai@6.0.100(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 3.0.57(zod@4.3.6) + '@ai-sdk/gateway': 3.0.55(zod@4.3.6) '@ai-sdk/provider': 3.0.8 '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) '@opentelemetry/api': 1.9.0 zod: 4.3.6 - ai@6.0.33(zod@4.3.6): - dependencies: - '@ai-sdk/gateway': 3.0.13(zod@4.3.6) - '@ai-sdk/provider': 3.0.2 - '@ai-sdk/provider-utils': 4.0.5(zod@4.3.6) - '@opentelemetry/api': 1.9.0 - zod: 4.3.6 - - ai@6.0.70(zod@4.3.6): + ai@6.0.103(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 3.0.33(zod@4.3.6) - '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@ai-sdk/gateway': 3.0.57(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) '@opentelemetry/api': 1.9.0 zod: 4.3.6 @@ -19043,22 +19600,6 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 - ai@6.0.91(zod@4.3.6): - dependencies: - '@ai-sdk/gateway': 3.0.50(zod@4.3.6) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) - '@opentelemetry/api': 1.9.0 - zod: 4.3.6 - - ai@6.0.94(zod@4.3.5): - dependencies: - '@ai-sdk/gateway': 3.0.52(zod@4.3.5) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.5) - '@opentelemetry/api': 1.9.0 - zod: 4.3.5 - ai@6.0.94(zod@4.3.6): dependencies: '@ai-sdk/gateway': 3.0.52(zod@4.3.6) @@ -19067,14 +19608,6 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 - ai@6.0.97(zod@4.3.6): - dependencies: - '@ai-sdk/gateway': 3.0.53(zod@4.3.6) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) - '@opentelemetry/api': 1.9.0 - zod: 4.3.6 - ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -19222,6 +19755,8 @@ snapshots: dependencies: dequal: 2.0.3 + aria-query@5.3.1: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -19301,6 +19836,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axobject-query@4.1.0: {} + babel-jest@27.5.1(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -19469,9 +20006,9 @@ snapshots: baseline-browser-mapping@2.9.14: {} - bash-tool@1.3.14(ai@6.0.33(zod@4.3.6))(just-bash@2.9.6): + bash-tool@1.3.14(ai@6.0.103(zod@4.3.6))(just-bash@2.9.6): dependencies: - ai: 6.0.33(zod@4.3.6) + ai: 6.0.103(zod@4.3.6) fast-glob: 3.3.3 gray-matter: 4.0.3 zod: 3.25.76 @@ -19494,6 +20031,19 @@ snapshots: big.js@5.2.2: {} + bits-ui@2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5): + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/dom': 1.7.5 + '@internationalized/date': 3.11.0 + esm-env: 1.2.2 + runed: 0.35.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) + svelte: 5.53.5 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) + tabbable: 6.4.0 + transitivePeerDependencies: + - '@sveltejs/kit' + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -19657,6 +20207,10 @@ snapshots: dependencies: readdirp: 4.1.2 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chownr@1.1.4: optional: true @@ -19789,7 +20343,7 @@ snapshots: compressible@2.0.18: dependencies: - mime-db: 1.52.0 + mime-db: 1.54.0 compression@1.8.1: dependencies: @@ -19840,6 +20394,8 @@ snapshots: cookie-signature@1.2.2: {} + cookie@0.6.0: {} + cookie@0.7.2: {} cookie@1.1.1: {} @@ -19853,15 +20409,6 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@9.0.0(typescript@5.9.2): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.9.2 - cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -20035,6 +20582,8 @@ snapshots: mimic-response: 3.1.0 optional: true + dedent-js@1.0.1: {} + dedent@0.7.0: {} dedent@1.7.1: {} @@ -20092,6 +20641,8 @@ snapshots: detect-node-es@1.1.0: {} + devalue@5.6.3: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -20241,7 +20792,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.18.4: + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -20693,6 +21244,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + espree@10.4.0: dependencies: acorn: 8.15.0 @@ -20711,6 +21264,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@2.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -21972,6 +22529,10 @@ snapshots: is-promise@4.0.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -22785,36 +23346,69 @@ snapshots: lightningcss-android-arm64@1.30.2: optional: true + lightningcss-android-arm64@1.31.1: + optional: true + lightningcss-darwin-arm64@1.30.2: optional: true + lightningcss-darwin-arm64@1.31.1: + optional: true + lightningcss-darwin-x64@1.30.2: optional: true + lightningcss-darwin-x64@1.31.1: + optional: true + lightningcss-freebsd-x64@1.30.2: optional: true + lightningcss-freebsd-x64@1.31.1: + optional: true + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + lightningcss-linux-arm64-gnu@1.30.2: optional: true + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + lightningcss-linux-arm64-musl@1.30.2: optional: true + lightningcss-linux-arm64-musl@1.31.1: + optional: true + lightningcss-linux-x64-gnu@1.30.2: optional: true + lightningcss-linux-x64-gnu@1.31.1: + optional: true + lightningcss-linux-x64-musl@1.30.2: optional: true + lightningcss-linux-x64-musl@1.31.1: + optional: true + lightningcss-win32-arm64-msvc@1.30.2: optional: true + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + lightningcss-win32-x64-msvc@1.30.2: optional: true + lightningcss-win32-x64-msvc@1.31.1: + optional: true + lightningcss@1.30.2: dependencies: detect-libc: 2.1.2 @@ -22831,6 +23425,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + lilconfig@3.1.3: {} linebreak@1.1.0: @@ -22869,6 +23479,8 @@ snapshots: emojis-list: 3.0.0 json5: 2.2.3 + locate-character@3.0.0: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -22942,6 +23554,10 @@ snapshots: dependencies: react: 19.2.4 + lucide-svelte@0.500.0(svelte@5.53.5): + dependencies: + svelte: 5.53.5 + lz-string@1.5.0: {} maath@0.10.8(@types/three@0.182.0)(three@0.182.0): @@ -23674,6 +24290,8 @@ snapshots: mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -23702,6 +24320,7 @@ snapshots: typescript: 5.9.2 transitivePeerDependencies: - '@types/node' + optional: true msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3): dependencies: @@ -24957,6 +25576,8 @@ snapshots: readdirp@4.1.2: {} + readdirp@5.0.0: {} + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -25280,6 +25901,19 @@ snapshots: dependencies: queue-microtask: 1.2.3 + runed@0.35.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.53.5 + optionalDependencies: + '@sveltejs/kit': 2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + + sade@1.8.1: + dependencies: + mri: 1.2.0 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -25303,7 +25937,7 @@ snapshots: safer-buffer@2.1.2: {} - satori@0.19.2: + satori@0.19.3: dependencies: '@shuding/opentype.js': 1.4.0-beta.0 css-background-parser: 0.1.0 @@ -25354,6 +25988,8 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 + scule@1.3.0: {} + section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -25433,6 +26069,8 @@ snapshots: server-only@0.0.1: {} + set-cookie-parser@3.0.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -25459,50 +26097,6 @@ snapshots: sf-symbols-typescript@2.2.0: {} - shadcn@3.8.5(@types/node@22.19.6)(typescript@5.9.2): - dependencies: - '@antfu/ni': 25.0.0 - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@dotenvx/dotenvx': 1.52.0 - '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) - '@types/validate-npm-package-name': 4.0.2 - browserslist: 4.28.1 - commander: 14.0.2 - cosmiconfig: 9.0.0(typescript@5.9.2) - dedent: 1.7.1 - deepmerge: 4.3.1 - diff: 8.0.3 - execa: 9.6.1 - fast-glob: 3.3.3 - fs-extra: 11.3.3 - fuzzysort: 3.1.0 - https-proxy-agent: 7.0.6 - kleur: 4.1.5 - msw: 2.12.10(@types/node@22.19.6)(typescript@5.9.2) - node-fetch: 3.3.2 - open: 11.0.0 - ora: 8.2.0 - postcss: 8.5.6 - postcss-selector-parser: 7.1.1 - prompts: 2.4.2 - recast: 0.23.11 - stringify-object: 5.0.0 - tailwind-merge: 3.5.0 - ts-morph: 26.0.0 - tsconfig-paths: 4.2.0 - validate-npm-package-name: 7.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - transitivePeerDependencies: - - '@cfworker/json-schema' - - '@types/node' - - babel-plugin-macros - - supports-color - - typescript - shadcn@3.8.5(@types/node@22.19.6)(typescript@5.9.3): dependencies: '@antfu/ni': 25.0.0 @@ -25654,6 +26248,12 @@ snapshots: dependencies: is-arrayish: 0.3.4 + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -25759,7 +26359,7 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 remend: 1.1.0 - tailwind-merge: 3.4.1 + tailwind-merge: 3.5.0 unified: 11.0.5 unist-util-visit: 5.1.0 transitivePeerDependencies: @@ -25779,7 +26379,7 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 remend: 1.2.0 - tailwind-merge: 3.4.1 + tailwind-merge: 3.5.0 unified: 11.0.5 unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -25986,6 +26586,53 @@ snapshots: dependencies: react: 19.2.4 + svelte-check@4.4.3(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.3) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.53.5 + typescript: 5.9.3 + transitivePeerDependencies: + - picomatch + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5): + dependencies: + clsx: 2.1.1 + runed: 0.35.1(@sveltejs/kit@2.53.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) + style-to-object: 1.0.14 + svelte: 5.53.5 + transitivePeerDependencies: + - '@sveltejs/kit' + + svelte2tsx@0.7.51(svelte@5.53.5)(typescript@5.9.3): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 5.53.5 + typescript: 5.9.3 + + svelte@5.53.5: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) + '@types/estree': 1.0.8 + '@types/trusted-types': 2.0.7 + acorn: 8.15.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.6.3 + esm-env: 1.2.2 + esrap: 2.2.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + svg-arc-to-cubic-bezier@3.2.0: {} swr@2.4.0(react@19.2.3): @@ -26002,16 +26649,22 @@ snapshots: symbol-tree@3.2.4: {} - tagged-tag@1.0.0: {} - - tailwind-merge@3.4.0: {} + tabbable@6.4.0: {} - tailwind-merge@3.4.1: {} + tagged-tag@1.0.0: {} tailwind-merge@3.5.0: {} + tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1): + dependencies: + tailwindcss: 4.2.1 + optionalDependencies: + tailwind-merge: 3.5.0 + tailwindcss@4.1.18: {} + tailwindcss@4.2.1: {} + tapable@2.3.0: {} tar-fs@2.1.4: @@ -26153,6 +26806,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + totalist@3.0.1: {} + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -26196,9 +26851,9 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.4.0(typescript@5.9.2): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: - typescript: 5.9.2 + typescript: 5.9.3 ts-interface-checker@0.1.13: {} @@ -26234,34 +26889,6 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.2)(yaml@2.8.2): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.2) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.2 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) - resolve-from: 5.0.0 - rollup: 4.55.1 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.6 - typescript: 5.9.2 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.2) @@ -26407,14 +27034,14 @@ snapshots: dependencies: is-typedarray: 1.0.0 - typescript-eslint@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2): + typescript-eslint@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -26683,7 +27310,7 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -26695,15 +27322,19 @@ snapshots: '@types/node': 22.19.6 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.31.1 terser: 5.46.0 tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vitefu@1.1.2(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + optionalDependencies: + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + + vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.2))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.17 '@vitest/runner': 4.0.17 '@vitest/snapshot': 4.0.17 @@ -26720,7 +27351,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -26739,10 +27370,10 @@ snapshots: - tsx - yaml - vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.17 '@vitest/runner': 4.0.17 '@vitest/snapshot': 4.0.17 @@ -26759,7 +27390,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -26799,6 +27430,7 @@ snapshots: '@vue/shared': 3.5.29 optionalDependencies: typescript: 5.9.2 + optional: true vue@3.5.29(typescript@5.9.3): dependencies: @@ -26865,7 +27497,7 @@ snapshots: acorn: 8.15.0 browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -26896,7 +27528,7 @@ snapshots: acorn: 8.15.0 browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -27127,6 +27759,8 @@ snapshots: yoga-layout@3.2.1: {} + zimmerframe@1.1.4: {} + zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76 @@ -27135,8 +27769,6 @@ snapshots: zod@3.25.76: {} - zod@4.3.5: {} - zod@4.3.6: {} zustand@4.5.7(@types/react@19.2.3)(immer@11.1.4)(react@19.2.4): diff --git a/skills/json-render-svelte/SKILL.md b/skills/json-render-svelte/SKILL.md new file mode 100644 index 00000000..a95ab874 --- /dev/null +++ b/skills/json-render-svelte/SKILL.md @@ -0,0 +1,284 @@ +--- +name: json-render-svelte +description: Svelte 5 renderer for json-render that turns JSON specs into Svelte component trees. Use when working with @json-render/svelte, building Svelte UIs from JSON, creating component catalogs, or rendering AI-generated specs. +--- + +# @json-render/svelte + +Svelte 5 renderer that converts json-render specs into Svelte component trees. + +## Quick Start + +```svelte + + + + + +``` + +## Creating a Catalog + +```typescript +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/svelte"; +import { z } from "zod"; + +export const catalog = defineCatalog(schema, { + components: { + Button: { + props: z.object({ + label: z.string(), + variant: z.enum(["primary", "secondary"]).nullable(), + }), + description: "Clickable button", + }, + Card: { + props: z.object({ title: z.string() }), + description: "Card container with title", + }, + }, +}); +``` + +## Defining Components + +Components should accept `BaseComponentProps`: + +```typescript +interface BaseComponentProps { + props: TProps; // Resolved props for this component + children?: Snippet; // Child elements (use {@render children()}) + emit: (event: string) => void; // Fire a named event + bindings?: Record; // Map of prop names to state paths (for $bindState) + loading?: boolean; // True while spec is streaming +} +``` + +```svelte + + + + +``` + +```svelte + + + +

+

{props.title}

+ {#if children} + {@render children()} + {/if} +
+``` + +## Creating a Registry + +```typescript +import { defineRegistry } from "@json-render/svelte"; +import { catalog } from "./catalog"; +import Card from "./components/Card.svelte"; +import Button from "./components/Button.svelte"; + +const { registry, handlers, executeAction } = defineRegistry(catalog, { + components: { + Card, + Button, + }, + actions: { + submit: async (params, setState, state) => { + // handle action + }, + }, +}); +``` + +## Spec Structure (Element Tree) + +The Svelte schema uses the element tree format: + +```json +{ + "root": "card1", + "elements": { + "card1": { + "type": "Card", + "props": { "title": "Hello" }, + "children": ["btn1"] + }, + "btn1": { + "type": "Button", + "props": { "label": "Click me" } + } + } +} +``` + +## Visibility Conditions + +Use `visible` on elements to show/hide based on state: + +- `{ "$state": "/path" }` - truthy check +- `{ "$state": "/path", "eq": value }` - equality check +- `{ "$state": "/path", "not": true }` - falsy check +- `{ "$and": [cond1, cond2] }` - AND conditions +- `{ "$or": [cond1, cond2] }` - OR conditions + +## Providers (via JsonUIProvider) + +`JsonUIProvider` composes all contexts. Individual contexts: + +| Context | Purpose | +| ------------------- | -------------------------------------------------- | +| `StateContext` | Share state across components (JSON Pointer paths) | +| `ActionContext` | Handle actions dispatched via the event system | +| `VisibilityContext` | Enable conditional rendering based on state | +| `ValidationContext` | Form field validation | + +## Event System + +Components use `emit` to fire named events. The element's `on` field maps events to action bindings: + +```svelte + + + + +``` + +```json +{ + "type": "Button", + "props": { "label": "Submit" }, + "on": { "press": { "action": "submit" } } +} +``` + +## Built-in Actions + +The `setState` action is handled automatically and updates the state model: + +```json +{ + "action": "setState", + "actionParams": { "statePath": "/activeTab", "value": "home" } +} +``` + +Other built-in actions: `pushState`, `removeState`, `push`, `pop`. + +## Dynamic Props and Two-Way Binding + +Expression forms resolved before your component receives props: + +- `{"$state": "/state/key"}` - read from state +- `{"$bindState": "/form/email"}` - read + write-back to state +- `{"$bindItem": "field"}` - read + write-back for repeat items +- `{"$cond": , "$then": , "$else": }` - conditional value + +For writable bindings inside components, use `getBoundProp`: + +```svelte + + + +``` + +## Context Helpers + +Preferred helpers: + +- `getStateValue(path)` - returns `{ current }` (read/write) +- `getBoundProp(() => value, () => bindingPath)` - returns `{ current }` (read/write when bound) +- `isVisible(condition)` - returns `{ current }` (boolean) +- `getAction(name)` - returns `{ current }` (registered handler) + +Advanced context access: + +- `getStateContext()` +- `getActionContext()` +- `getVisibilityContext()` +- `getValidationContext()` +- `getOptionalValidationContext()` +- `getFieldValidation(ctx, path, config?)` + +## Streaming UI + +Use `createUIStream` for spec streaming: + +```svelte + + + + +{#if stream.spec} + +{/if} +``` + +Use `createChatUI` for chat + UI responses: + +```typescript +const chat = createChatUI({ api: "/api/chat-ui" }); +await chat.send("Build a settings panel"); +``` diff --git a/vitest.config.ts b/vitest.config.mts similarity index 68% rename from vitest.config.ts rename to vitest.config.mts index b797e2a1..54cf731e 100644 --- a/vitest.config.ts +++ b/vitest.config.mts @@ -1,8 +1,15 @@ import { defineConfig } from "vitest/config"; import path from "path"; +import { fileURLToPath } from "url"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ + plugins: [svelte({ hot: false })], resolve: { + // Ensure Svelte resolves to browser bundle, not server + conditions: ["browser"], // Deduplicate React and Vue so tests don't get two copies // (pnpm strict resolution can cause packages to resolve different copies) alias: { @@ -18,7 +25,7 @@ export default defineConfig({ coverage: { provider: "v8", reporter: ["text", "json", "html"], - include: ["packages/*/src/**/*.{ts,tsx}"], + include: ["packages/*/src/**/*.{ts,tsx,svelte}"], exclude: ["**/*.test.{ts,tsx}", "**/index.ts"], }, }, From c019cd6d87a7dc80580436e85e3d34edc53d7fb1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 6 Mar 2026 19:16:17 -0600 Subject: [PATCH 4/9] fix(svelte): correct JSDoc comment and add missing zod peer dependency (#183) Fixes #182 --- packages/svelte/package.json | 3 ++- packages/svelte/src/renderer.ts | 2 +- pnpm-lock.yaml | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 615a9544..e92b9477 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -65,6 +65,7 @@ "typescript": "^5.4.5" }, "peerDependencies": { - "svelte": "^5.0.0" + "svelte": "^5.0.0", + "zod": "^4.0.0" } } diff --git a/packages/svelte/src/renderer.ts b/packages/svelte/src/renderer.ts index 1f4d773f..36d903ee 100644 --- a/packages/svelte/src/renderer.ts +++ b/packages/svelte/src/renderer.ts @@ -211,7 +211,7 @@ export interface CreateRendererProps { } /** - * Component map type — maps component names to Vue components + * Component map type — maps component names to Svelte components */ export type ComponentMap< TComponents extends Record, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0e1f739..c1187d35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1552,6 +1552,9 @@ importers: '@json-render/core': specifier: workspace:* version: link:../core + zod: + specifier: ^4.0.0 + version: 4.3.6 devDependencies: '@internal/typescript-config': specifier: workspace:* From ae0d732dce70eb87c4cb0bb1d4ec6ee9aa572058 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 6 Mar 2026 19:41:27 -0600 Subject: [PATCH 5/9] mcp (#184) * mcp * fix lint --- .changeset/config.json | 3 +- .cursor/mcp.json | 8 + .vscode/mcp.json | 9 + README.md | 1 + apps/web/app/(main)/docs/api/mcp/page.mdx | 247 ++++++++++++++++ apps/web/app/api/docs-chat/route.ts | 2 +- apps/web/lib/docs-navigation.ts | 6 + apps/web/lib/page-titles.ts | 1 + examples/mcp/README.md | 63 ++++ examples/mcp/index.html | 13 + examples/mcp/package.json | 34 +++ examples/mcp/server.ts | 86 ++++++ examples/mcp/src/catalog.ts | 10 + examples/mcp/src/globals.css | 106 +++++++ examples/mcp/src/main.tsx | 269 ++++++++++++++++++ examples/mcp/src/mcp-app-view.tsx | 89 ++++++ examples/mcp/tsconfig.json | 17 ++ examples/mcp/vite.config.ts | 15 + .../svelte-chat/src/lib/render/registry.ts | 5 +- packages/mcp/README.md | 124 ++++++++ packages/mcp/package.json | 78 +++++ packages/mcp/src/app.ts | 30 ++ packages/mcp/src/build-app-html.ts | 71 +++++ packages/mcp/src/index.ts | 12 + packages/mcp/src/server.ts | 158 ++++++++++ packages/mcp/src/types.ts | 62 ++++ packages/mcp/src/use-json-render-app.ts | 139 +++++++++ packages/mcp/tsconfig.json | 9 + packages/mcp/tsup.config.ts | 17 ++ packages/svelte/src/JsonUIProvider.svelte | 4 +- pnpm-lock.yaml | 247 +++++++++++++++- skills/json-render-mcp/SKILL.md | 128 +++++++++ 32 files changed, 2054 insertions(+), 9 deletions(-) create mode 100644 .cursor/mcp.json create mode 100644 .vscode/mcp.json create mode 100644 apps/web/app/(main)/docs/api/mcp/page.mdx create mode 100644 examples/mcp/README.md create mode 100644 examples/mcp/index.html create mode 100644 examples/mcp/package.json create mode 100644 examples/mcp/server.ts create mode 100644 examples/mcp/src/catalog.ts create mode 100644 examples/mcp/src/globals.css create mode 100644 examples/mcp/src/main.tsx create mode 100644 examples/mcp/src/mcp-app-view.tsx create mode 100644 examples/mcp/tsconfig.json create mode 100644 examples/mcp/vite.config.ts create mode 100644 packages/mcp/README.md create mode 100644 packages/mcp/package.json create mode 100644 packages/mcp/src/app.ts create mode 100644 packages/mcp/src/build-app-html.ts create mode 100644 packages/mcp/src/index.ts create mode 100644 packages/mcp/src/server.ts create mode 100644 packages/mcp/src/types.ts create mode 100644 packages/mcp/src/use-json-render-app.ts create mode 100644 packages/mcp/tsconfig.json create mode 100644 packages/mcp/tsup.config.ts create mode 100644 skills/json-render-mcp/SKILL.md diff --git a/.changeset/config.json b/.changeset/config.json index e63cf2ab..31bbfd8a 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -17,7 +17,8 @@ "@json-render/jotai", "@json-render/vue", "@json-render/xstate", - "@json-render/image" + "@json-render/image", + "@json-render/mcp" ] ], "linked": [], diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..5f211d4f --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "json-render": { + "command": "npx", + "args": ["tsx", "examples/mcp/server.ts", "--stdio"] + } + } +} diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 00000000..b2876a02 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,9 @@ +{ + "servers": { + "json-render": { + "type": "stdio", + "command": "npx", + "args": ["tsx", "examples/mcp/server.ts", "--stdio"] + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index b458e236..36a408d7 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ function Dashboard({ spec }) { | `@json-render/zustand` | Zustand adapter for `StateStore` | | `@json-render/jotai` | Jotai 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 diff --git a/apps/web/app/(main)/docs/api/mcp/page.mdx b/apps/web/app/(main)/docs/api/mcp/page.mdx new file mode 100644 index 00000000..2bb49487 --- /dev/null +++ b/apps/web/app/(main)/docs/api/mcp/page.mdx @@ -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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeDescription
namestringServer name shown in client UIs
versionstringServer version
catalogCatalogjson-render catalog defining available components
htmlstringSelf-contained HTML for the iframe UI
toolMcpToolOptionsOptional tool name/title/description overrides
+ +### 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
Error: {error.message}
; + if (!spec) return
Waiting...
; + + return ( + + + + ); +} +``` + +#### UseJsonRenderAppReturn + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
spec{'Spec | null'}Current json-render spec
loadingbooleanWhether the spec is still being received
connectedbooleanWhether connected to the host
connectingbooleanWhether currently connecting
error{'Error | null'}Connection error, if any
app{'App | null'}The underlying MCP App instance
callServerTool{'(name, args?) => Promise'}Call an MCP server tool and update spec from result
+ +### 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. diff --git a/apps/web/app/api/docs-chat/route.ts b/apps/web/app/api/docs-chat/route.ts index 49242876..6156e8ec 100644 --- a/apps/web/app/api/docs-chat/route.ts +++ b/apps/web/app/api/docs-chat/route.ts @@ -16,7 +16,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render GitHub repository: https://github.com/vercel-labs/json-render Documentation: https://json-render.dev/docs -npm packages: @json-render/core, @json-render/react, @json-render/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/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp 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. diff --git a/apps/web/lib/docs-navigation.ts b/apps/web/lib/docs-navigation.ts index cb700b22..ff261eda 100644 --- a/apps/web/lib/docs-navigation.ts +++ b/apps/web/lib/docs-navigation.ts @@ -95,6 +95,11 @@ export const docsNavigation: NavSection[] = [ href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers", external: true, }, + { + title: "MCP App", + href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp", + external: true, + }, ], }, { @@ -128,6 +133,7 @@ export const docsNavigation: NavSection[] = [ { 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/mcp", href: "/docs/api/mcp" }, ], }, ]; diff --git a/apps/web/lib/page-titles.ts b/apps/web/lib/page-titles.ts index 2dd35139..f08cba21 100644 --- a/apps/web/lib/page-titles.ts +++ b/apps/web/lib/page-titles.ts @@ -50,6 +50,7 @@ export const PAGE_TITLES: Record = { "docs/api/image": "@json-render/image API", "docs/api/remotion": "@json-render/remotion API", "docs/api/shadcn": "@json-render/shadcn API", + "docs/api/mcp": "@json-render/mcp API", }; /** diff --git a/examples/mcp/README.md b/examples/mcp/README.md new file mode 100644 index 00000000..8f637362 --- /dev/null +++ b/examples/mcp/README.md @@ -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 diff --git a/examples/mcp/index.html b/examples/mcp/index.html new file mode 100644 index 00000000..4f104654 --- /dev/null +++ b/examples/mcp/index.html @@ -0,0 +1,13 @@ + + + + + + + json-render MCP App + + +
+ + + diff --git a/examples/mcp/package.json b/examples/mcp/package.json new file mode 100644 index 00000000..11961880 --- /dev/null +++ b/examples/mcp/package.json @@ -0,0 +1,34 @@ +{ + "name": "example-mcp", + "version": "0.1.0", + "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.2.3", + "react-dom": "19.2.3", + "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" + } +} diff --git a/examples/mcp/server.ts b/examples/mcp/server.ts new file mode 100644 index 00000000..772ceecd --- /dev/null +++ b/examples/mcp/server.ts @@ -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); + }); +} diff --git a/examples/mcp/src/catalog.ts b/examples/mcp/src/catalog.ts new file mode 100644 index 00000000..3ae4eb68 --- /dev/null +++ b/examples/mcp/src/catalog.ts @@ -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: {}, +}); diff --git a/examples/mcp/src/globals.css b/examples/mcp/src/globals.css new file mode 100644 index 00000000..8b322d60 --- /dev/null +++ b/examples/mcp/src/globals.css @@ -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; +} diff --git a/examples/mcp/src/main.tsx b/examples/mcp/src/main.tsx new file mode 100644 index 00000000..f24b59bb --- /dev/null +++ b/examples/mcp/src/main.tsx @@ -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 }) => { + 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 ( +
+ {showImg ? ( + {name} + ) : ( + + {initials} + + )} +
+ ); + }, + }, +}); + +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 ( +
+ Error: {this.state.error.message} +
+ ); + } + 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 }; +}) { + 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(null); + const [error, setError] = useState(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 | undefined; + if (!data || typeof data !== "object") return; + const method = data.method as string | undefined; + const params = data.params as Record | undefined; + + // Cursor sends the spec via tool-input + if (method === "ui/notifications/tool-input" && params?.arguments) { + const args = params.arguments as Record; + 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[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[0]); + + // Fallback: if host didn't provide theme, detect via media query + if (!ctx || !(ctx as Record).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 ( +
+ {error} +
+ ); + } + + if (!spec) { + return ( +
+ Loading... +
+ ); + } + + return ( + +
+ +
+
+ ); +} + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/mcp/src/mcp-app-view.tsx b/examples/mcp/src/mcp-app-view.tsx new file mode 100644 index 00000000..7e853da7 --- /dev/null +++ b/examples/mcp/src/mcp-app-view.tsx @@ -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(["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 ( +
+
+ Connection error: {error.message} +
+
{logs.join("\n")}
+
+ ); + } + + if (!spec) { + return ( +
+
+ {connecting + ? "Connecting to host..." + : loading + ? "Waiting for UI spec..." + : "No spec received."} +
+
{logs.join("\n")}
+
+ ); + } + + return ( +
+ + + +
+ ); +} diff --git a/examples/mcp/tsconfig.json b/examples/mcp/tsconfig.json new file mode 100644 index 00000000..458d4162 --- /dev/null +++ b/examples/mcp/tsconfig.json @@ -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"] +} diff --git a/examples/mcp/vite.config.ts b/examples/mcp/vite.config.ts new file mode 100644 index 00000000..185077f7 --- /dev/null +++ b/examples/mcp/vite.config.ts @@ -0,0 +1,15 @@ +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()], + build: { + outDir: "dist", + emptyOutDir: false, + rollupOptions: { + input: "index.html", + }, + }, +}); diff --git a/examples/svelte-chat/src/lib/render/registry.ts b/examples/svelte-chat/src/lib/render/registry.ts index 7802bb1f..1209ad30 100644 --- a/examples/svelte-chat/src/lib/render/registry.ts +++ b/examples/svelte-chat/src/lib/render/registry.ts @@ -1,7 +1,6 @@ -import { defineRegistry, type ComponentRegistry } from "@json-render/svelte"; +import { defineRegistry } from "@json-render/svelte"; import { explorerCatalog } from "./catalog"; -// Import render components import StackComponent from "./components/Stack.svelte"; import CardComponent from "./components/Card.svelte"; import GridComponent from "./components/Grid.svelte"; @@ -28,7 +27,7 @@ import SelectInputComponent from "./components/SelectInput.svelte"; import TextInputComponent from "./components/TextInput.svelte"; import ButtonComponent from "./components/Button.svelte"; -const components: ComponentRegistry = { +const components = { Stack: StackComponent, Card: CardComponent, Grid: GridComponent, diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 00000000..c2b79d34 --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,124 @@ +# @json-render/mcp + +MCP Apps integration for [json-render](https://github.com/vercel-labs/json-render). Serve json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. + +## What are MCP Apps? + +[MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) is an extension to the Model Context Protocol that lets MCP servers return interactive HTML UIs rendered directly inside chat conversations. Instead of text-only tool responses, users get full interactive interfaces -- dashboards, forms, data visualizations -- embedded inline. + +## Installation + +```bash +npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk +``` + +## Quick Start + +### 1. Define your catalog + +```ts +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/react/schema"; +import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog"; + +const catalog = defineCatalog(schema, { + components: { ...shadcnComponentDefinitions }, + actions: {}, +}); +``` + +### 2. Create the MCP server + +```ts +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, + html: fs.readFileSync("dist/index.html", "utf-8"), +}); + +await server.connect(new StdioServerTransport()); +``` + +### 3. Build the UI (iframe) + +Create a React app that uses `useJsonRenderApp` from `@json-render/mcp/app`: + +```tsx +import { useJsonRenderApp } from "@json-render/mcp/app"; +import { JSONUIProvider, Renderer } from "@json-render/react"; + +function McpAppView({ registry }) { + const { spec, loading, connected, error } = useJsonRenderApp(); + + if (error) return
Error: {error.message}
; + if (!spec) return
Waiting for spec...
; + + return ( + + + + ); +} +``` + +Bundle with Vite + `vite-plugin-singlefile` into a single HTML file, then pass it to `createMcpApp` as the `html` option. + +### 4. Connect to a client + +Add to `.cursor/mcp.json` or Claude Desktop config: + +```json +{ + "mcpServers": { + "my-app": { + "command": "node", + "args": ["./server.js", "--stdio"] + } + } +} +``` + +## API Reference + +### Server Side (main export) + +#### `createMcpApp(options)` + +Creates a fully-configured `McpServer` with a json-render tool and UI resource. + +| Option | Type | Description | +|--------|------|-------------| +| `name` | `string` | Server name shown in client UIs | +| `version` | `string` | Server version | +| `catalog` | `Catalog` | json-render catalog defining available components | +| `html` | `string` | Bundled HTML for the iframe UI | +| `tool` | `McpToolOptions` | Optional tool name/title/description overrides | + +#### `registerJsonRenderTool(server, options)` + +Register a json-render tool on an existing `McpServer`. + +#### `registerJsonRenderResource(server, options)` + +Register a json-render UI resource on an existing `McpServer`. + +### Client Side (`@json-render/mcp/app`) + +#### `useJsonRenderApp(options?)` + +React hook for the iframe-side app. Connects to the MCP host, receives tool results, and maintains the current json-render spec. + +Returns `{ spec, loading, connected, connecting, error, app, callServerTool }`. + +#### `buildAppHtml(options)` + +Generate a self-contained HTML string from bundled JS/CSS for use as a UI resource. + +## Client Support + +MCP Apps are supported by Claude, ChatGPT, VS Code (Copilot), Cursor, Goose, and Postman. diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 00000000..07909a83 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,78 @@ +{ + "name": "@json-render/mcp", + "version": "0.11.0", + "license": "Apache-2.0", + "description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.", + "keywords": [ + "json", + "ui", + "mcp", + "model-context-protocol", + "mcp-apps", + "ai", + "generative-ui", + "llm", + "renderer", + "claude", + "chatgpt", + "cursor" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/json-render.git", + "directory": "packages/mcp" + }, + "homepage": "https://github.com/vercel-labs/json-render#readme", + "bugs": { + "url": "https://github.com/vercel-labs/json-render/issues" + }, + "publishConfig": { + "access": "public" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./app": { + "types": "./dist/app.d.ts", + "import": "./dist/app.mjs", + "require": "./dist/app.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@json-render/core": "workspace:*", + "@modelcontextprotocol/ext-apps": "^1.2.0", + "@modelcontextprotocol/sdk": "^1.27.1" + }, + "devDependencies": { + "@internal/typescript-config": "workspace:*", + "@types/react": "19.2.3", + "tsup": "^8.0.2", + "typescript": "^5.4.5" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } +} diff --git a/packages/mcp/src/app.ts b/packages/mcp/src/app.ts new file mode 100644 index 00000000..c00b0345 --- /dev/null +++ b/packages/mcp/src/app.ts @@ -0,0 +1,30 @@ +/** + * Client-side (iframe) utilities for rendering json-render specs + * inside an MCP App view. + * + * This module is intended to run **inside the sandboxed iframe** that + * MCP hosts render. It connects to the host via the MCP Apps protocol, + * receives tool results containing json-render specs, and provides + * React hooks / helpers to render them. + * + * @example + * ```tsx + * import { useJsonRenderApp } from "@json-render/mcp/app"; + * import { Renderer } from "@json-render/react"; + * + * function McpAppView({ registry }) { + * const { spec, loading } = useJsonRenderApp(); + * return ; + * } + * ``` + * + * @packageDocumentation + */ + +export { useJsonRenderApp } from "./use-json-render-app.js"; +export type { + UseJsonRenderAppOptions, + UseJsonRenderAppReturn, +} from "./use-json-render-app.js"; +export { buildAppHtml } from "./build-app-html.js"; +export type { BuildAppHtmlOptions } from "./build-app-html.js"; diff --git a/packages/mcp/src/build-app-html.ts b/packages/mcp/src/build-app-html.ts new file mode 100644 index 00000000..71a87fb1 --- /dev/null +++ b/packages/mcp/src/build-app-html.ts @@ -0,0 +1,71 @@ +/** + * Options for `buildAppHtml`. + */ +export interface BuildAppHtmlOptions { + /** Title for the HTML page. Defaults to `"json-render"`. */ + title?: string; + /** + * Inline CSS to inject into the page ` + + +
+ + +`; +} + +function escapeHtml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 00000000..93883733 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,12 @@ +export type { + CreateMcpAppOptions, + McpToolOptions, + RegisterToolOptions, + RegisterResourceOptions, +} from "./types.js"; + +export { + createMcpApp, + registerJsonRenderTool, + registerJsonRenderResource, +} from "./server.js"; diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts new file mode 100644 index 00000000..57300015 --- /dev/null +++ b/packages/mcp/src/server.ts @@ -0,0 +1,158 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { + CreateMcpAppOptions, + RegisterToolOptions, + RegisterResourceOptions, +} from "./types.js"; + +const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; + +/** + * Dynamically import the ext-apps server helpers to avoid CJS/ESM type + * mismatches at compile time while still getting the proper runtime + * `_meta.ui` normalization that hosts require. + */ +async function getExtApps() { + const mod = await import("@modelcontextprotocol/ext-apps/server"); + return mod; +} + +/** + * Register a json-render tool on an existing MCP server. + * + * Uses `registerAppTool` from `@modelcontextprotocol/ext-apps/server` + * so that MCP Apps-capable hosts (Claude, VS Code, Cursor, ChatGPT) + * see the `_meta.ui.resourceUri` in the tool listing and know to + * fetch and render the `ui://` resource as an interactive iframe. + * + * The tool accepts a json-render spec as input and returns it as text + * content, which the iframe receives via `ontoolresult`. + */ +export async function registerJsonRenderTool( + server: McpServer, + options: RegisterToolOptions, +): Promise { + const { catalog, name, title, description, resourceUri } = options; + const { registerAppTool } = await getExtApps(); + + const specZodSchema = catalog.zodSchema(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (registerAppTool as any)( + server, + name, + { + title, + description, + inputSchema: { spec: specZodSchema }, + _meta: { ui: { resourceUri } }, + }, + async (args: { spec?: unknown }) => { + const spec = args.spec; + const validation = catalog.validate(spec); + const validSpec = validation.success ? validation.data : spec; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(validSpec), + }, + ], + }; + }, + ); +} + +/** + * Register a json-render UI resource on an existing MCP server. + * + * The resource serves the self-contained HTML page that renders + * json-render specs received from tool results. + */ +export async function registerJsonRenderResource( + server: McpServer, + options: RegisterResourceOptions, +): Promise { + const { resourceUri, html } = options; + const { registerAppResource, RESOURCE_MIME_TYPE: mimeType } = + await getExtApps(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (registerAppResource as any)( + server, + resourceUri, + resourceUri, + { mimeType }, + async () => ({ + contents: [ + { + uri: resourceUri, + mimeType, + text: html, + _meta: { + ui: { + csp: { + resourceDomains: ["https:"], + connectDomains: ["https:"], + }, + }, + }, + }, + ], + }), + ); +} + +/** + * Create a fully-configured MCP server that serves a json-render catalog + * as an MCP App. + * + * This is the main entry point for most users. It creates an `McpServer`, + * registers the render tool and UI resource, and returns the server + * ready for transport connection. + * + * @example + * ```ts + * import { createMcpApp } from "@json-render/mcp"; + * + * const server = createMcpApp({ + * name: "My Dashboard", + * version: "1.0.0", + * catalog: myCatalog, + * html: myBundledHtml, + * }); + * + * // Connect via stdio + * import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + * await server.connect(new StdioServerTransport()); + * ``` + */ +export async function createMcpApp( + options: CreateMcpAppOptions, +): Promise { + const { name, version, catalog, html, tool } = options; + + const toolName = tool?.name ?? "render-ui"; + const toolTitle = tool?.title ?? "Render UI"; + const resourceUri = `ui://${toolName}/view.html`; + + const catalogPrompt = catalog.prompt(); + const toolDescription = + tool?.description ?? + `Render an interactive UI. The spec argument must be a json-render spec conforming to the catalog.\n\n${catalogPrompt}`; + + const server = new McpServer({ name, version }); + + await registerJsonRenderTool(server, { + catalog, + name: toolName, + title: toolTitle, + description: toolDescription, + resourceUri, + }); + + await registerJsonRenderResource(server, { resourceUri, html }); + + return server; +} diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts new file mode 100644 index 00000000..38bc3e56 --- /dev/null +++ b/packages/mcp/src/types.ts @@ -0,0 +1,62 @@ +import type { Catalog } from "@json-render/core"; + +/** + * Options for creating an MCP App server backed by a json-render catalog. + */ +export interface CreateMcpAppOptions { + /** Display name for the MCP server shown in client UIs. */ + name: string; + /** Semantic version of the MCP server. */ + version: string; + /** The json-render catalog defining available components and actions. */ + catalog: Catalog; + /** + * Pre-built HTML string for the UI resource. + * Generate this with `buildAppHtml` from `@json-render/mcp/app` or + * provide your own self-contained HTML page. + */ + html: string; + /** + * Optional tool configuration overrides. + */ + tool?: McpToolOptions; +} + +/** + * Options for configuring the MCP tool that renders json-render specs. + */ +export interface McpToolOptions { + /** Tool name exposed to the LLM. Defaults to `"render-ui"`. */ + name?: string; + /** Human-readable title. Defaults to `"Render UI"`. */ + title?: string; + /** Tool description shown to the LLM. When omitted, a description is + * auto-generated from the catalog prompt. */ + description?: string; +} + +/** + * Options for registering the MCP App tool. + */ +export interface RegisterToolOptions { + /** The json-render catalog. */ + catalog: Catalog; + /** Tool name. */ + name: string; + /** Tool title. */ + title: string; + /** Tool description. */ + description: string; + /** The `ui://` resource URI this tool's view is served from. */ + resourceUri: string; +} + +/** + * Options for registering the MCP App UI resource. + */ +export interface RegisterResourceOptions { + /** The `ui://` resource URI. */ + resourceUri: string; + /** Self-contained HTML string for the view. */ + html: string; +} diff --git a/packages/mcp/src/use-json-render-app.ts b/packages/mcp/src/use-json-render-app.ts new file mode 100644 index 00000000..c85a9294 --- /dev/null +++ b/packages/mcp/src/use-json-render-app.ts @@ -0,0 +1,139 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import type { Spec } from "@json-render/core"; +import { App } from "@modelcontextprotocol/ext-apps"; + +/** + * Options for the `useJsonRenderApp` hook. + */ +export interface UseJsonRenderAppOptions { + /** App name shown during initialization. Defaults to `"json-render"`. */ + name?: string; + /** App version. Defaults to `"1.0.0"`. */ + version?: string; +} + +/** + * Return value of `useJsonRenderApp`. + */ +export interface UseJsonRenderAppReturn { + /** The current json-render spec (null until the first tool result). */ + spec: Spec | null; + /** Whether the app is still connecting to the host. */ + connecting: boolean; + /** Whether the app is connected to the host. */ + connected: boolean; + /** Connection error, if any. */ + error: Error | null; + /** Whether the spec is still being received / parsed. */ + loading: boolean; + /** The underlying MCP App instance. */ + app: App | null; + /** + * Call a tool on the MCP server and update the spec from the result. + * Useful for refresh / drill-down interactions. + */ + callServerTool: ( + name: string, + args?: Record, + ) => Promise; +} + +interface ToolResultContent { + type: string; + text?: string; +} + +function parseSpecFromToolResult(result: { + content?: ToolResultContent[]; +}): Spec | null { + const textContent = result.content?.find( + (c: ToolResultContent) => c.type === "text", + ); + if (!textContent?.text) return null; + try { + const parsed = JSON.parse(textContent.text); + if (parsed && typeof parsed === "object" && "spec" in parsed) { + return parsed.spec as Spec; + } + return parsed as Spec; + } catch { + return null; + } +} + +/** + * React hook that connects to the MCP host, listens for tool results, + * and maintains the current json-render spec. + * + * Follows the official MCP Apps pattern: create an `App` instance, + * register the `ontoolresult` handler, then call `app.connect()` + * which internally creates a PostMessageTransport to the host. + */ +export function useJsonRenderApp( + options: UseJsonRenderAppOptions = {}, +): UseJsonRenderAppReturn { + const { name = "json-render", version = "1.0.0" } = options; + + const [spec, setSpec] = useState(null); + const [loading, setLoading] = useState(true); + const [connected, setConnected] = useState(false); + const [error, setError] = useState(null); + const appRef = useRef(null); + + useEffect(() => { + const app = new App({ name, version }); + appRef.current = app; + + app.ontoolresult = (result: { content?: ToolResultContent[] }) => { + const parsed = parseSpecFromToolResult(result); + if (parsed) { + setSpec(parsed); + setLoading(false); + } + }; + + // Let the App class handle transport creation internally, + // matching the official MCP Apps quickstart pattern. + app + .connect() + .then(() => { + setConnected(true); + }) + .catch((err: unknown) => { + setError(err instanceof Error ? err : new Error(String(err))); + }); + + return () => { + app.close().catch(() => {}); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const callServerTool = useCallback( + async (toolName: string, args: Record = {}) => { + if (!appRef.current) return; + setLoading(true); + try { + const result = await appRef.current.callServerTool({ + name: toolName, + arguments: args, + }); + const parsed = parseSpecFromToolResult(result); + if (parsed) setSpec(parsed); + } finally { + setLoading(false); + } + }, + [], + ); + + return { + spec, + connecting: !connected && !error, + connected, + error, + loading, + app: appRef.current, + callServerTool, + }; +} diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 00000000..6c6204c0 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@internal/typescript-config/react-library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/mcp/tsup.config.ts b/packages/mcp/tsup.config.ts new file mode 100644 index 00000000..ad5ce414 --- /dev/null +++ b/packages/mcp/tsup.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts", "src/app.ts"], + format: ["cjs", "esm"], + dts: true, + sourcemap: true, + clean: true, + external: [ + "react", + "react-dom", + "@json-render/core", + "@json-render/react", + "@modelcontextprotocol/sdk", + "@modelcontextprotocol/ext-apps", + ], +}); diff --git a/packages/svelte/src/JsonUIProvider.svelte b/packages/svelte/src/JsonUIProvider.svelte index 32cc6199..8162e94c 100644 --- a/packages/svelte/src/JsonUIProvider.svelte +++ b/packages/svelte/src/JsonUIProvider.svelte @@ -4,8 +4,8 @@ * Props for JSONUIProvider */ export interface JSONUIProviderProps { - /** Component registry */ - registry: ComponentRegistry; + /** Component registry (passed through for convenience; not used internally) */ + registry?: ComponentRegistry; /** * External store (controlled mode). When provided, `initialState` and * `onStateChange` are ignored. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1187d35..9ba252ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -520,6 +520,64 @@ importers: specifier: ^5.7.2 version: 5.9.3 + examples/mcp: + dependencies: + '@json-render/core': + specifier: workspace:* + version: link:../../packages/core + '@json-render/mcp': + specifier: workspace:* + version: link:../../packages/mcp + '@json-render/react': + specifier: workspace:* + version: link:../../packages/react + '@json-render/shadcn': + specifier: workspace:* + version: link:../../packages/shadcn + '@modelcontextprotocol/ext-apps': + specifier: ^1.2.0 + version: 1.2.0(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.6) + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.27.1(zod@4.3.6) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@types/react': + specifier: 19.2.3 + version: 19.2.3 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.3) + '@vitejs/plugin-react': + specifier: ^5.1.4 + version: 5.1.4(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + tailwindcss: + specifier: ^4.2.1 + version: 4.2.1 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vite: + specifier: ^6.3.5 + version: 6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite-plugin-singlefile: + specifier: ^2.3.0 + version: 2.3.0(rollup@4.55.1)(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + examples/no-ai: dependencies: '@json-render/core': @@ -621,7 +679,7 @@ importers: version: 0.575.0(react@19.2.4) next: specifier: 16.1.6 - version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -755,7 +813,7 @@ importers: version: 0.575.0(react@19.2.4) next: specifier: 16.1.6 - version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1296,6 +1354,37 @@ importers: specifier: ^5.4.5 version: 5.9.3 + packages/mcp: + dependencies: + '@json-render/core': + specifier: workspace:* + version: link:../core + '@modelcontextprotocol/ext-apps': + specifier: ^1.2.0 + version: 1.2.0(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.27.1(zod@4.3.6) + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + devDependencies: + '@internal/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/react': + specifier: 19.2.3 + version: 19.2.3 + tsup: + specifier: ^8.0.2 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.4.5 + version: 5.9.3 + packages/react: dependencies: '@json-render/core': @@ -3708,6 +3797,19 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/ext-apps@1.2.0': + resolution: {integrity: sha512-ijUQJX/FmNq8PWgOLzph/BAfy84sUZxoIRuHzr+F37wYtWjhdl8pliBJybapYolppY+XJ8oqjFZmTOuMqxwbWQ==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.24.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@modelcontextprotocol/sdk@1.26.0': resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} engines: {node: '>=18'} @@ -3718,6 +3820,16 @@ packages: '@cfworker/json-schema': optional: true + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mongodb-js/zstd@7.0.0': resolution: {integrity: sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA==} engines: {node: '>= 20.19.0'} @@ -12498,6 +12610,53 @@ packages: resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} engines: {node: '>= 6'} + vite-plugin-singlefile@2.3.0: + resolution: {integrity: sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.44.1 + vite: ^5.4.11 || ^6.0.0 || ^7.0.0 + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -15302,6 +15461,22 @@ snapshots: '@mixmark-io/domino@2.2.0': {} + '@modelcontextprotocol/ext-apps@1.2.0(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.6)': + dependencies: + '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6) + zod: 4.3.6 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@modelcontextprotocol/ext-apps@1.2.0(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)': + dependencies: + '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6) + zod: 4.3.6 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.9(hono@4.12.0) @@ -15324,6 +15499,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.12.0) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.12.0 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@mongodb-js/zstd@7.0.0': dependencies: node-addon-api: 8.5.0 @@ -18765,6 +18962,13 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 + '@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.2.1 @@ -19285,6 +19489,18 @@ snapshots: '@visual-json/core': 0.1.1 react: 19.2.3 + '@vitejs/plugin-react@5.1.4(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 @@ -27313,6 +27529,29 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + vite-plugin-singlefile@2.3.0(rollup@4.55.1)(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + micromatch: 4.0.8 + rollup: 4.55.1 + vite: 6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + + vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.6 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + terser: 5.46.0 + tsx: 4.21.0 + yaml: 2.8.2 + vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 @@ -27768,6 +28007,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@3.22.3: {} zod@3.25.76: {} diff --git a/skills/json-render-mcp/SKILL.md b/skills/json-render-mcp/SKILL.md new file mode 100644 index 00000000..680eae9c --- /dev/null +++ b/skills/json-render-mcp/SKILL.md @@ -0,0 +1,128 @@ +--- +name: json-render-mcp +description: MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol. +--- + +# @json-render/mcp + +MCP Apps integration that serves json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. + +## Quick Start + +### Server (Node.js) + +```typescript +import { createMcpApp } from "@json-render/mcp"; +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/react/schema"; +import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import fs from "node:fs"; + +const catalog = defineCatalog(schema, { + components: { ...shadcnComponentDefinitions }, + actions: {}, +}); + +const server = createMcpApp({ + name: "My App", + version: "1.0.0", + catalog, + html: fs.readFileSync("dist/index.html", "utf-8"), +}); + +await server.connect(new StdioServerTransport()); +``` + +### Client (React, inside iframe) + +```tsx +import { useJsonRenderApp } from "@json-render/mcp/app"; +import { JSONUIProvider, Renderer } from "@json-render/react"; + +function McpAppView({ registry }) { + const { spec, loading, error } = useJsonRenderApp(); + if (error) return
Error: {error.message}
; + if (!spec) return
Waiting...
; + return ( + + + + ); +} +``` + +## Architecture + +1. `createMcpApp()` creates an `McpServer` that registers a `render-ui` tool and a `ui://` HTML resource +2. The tool description includes the catalog prompt so the LLM knows how to generate valid specs +3. The HTML resource is a Vite-bundled single-file React app with json-render renderers +4. Inside the iframe, `useJsonRenderApp()` connects to the host via `postMessage` and renders specs + +## Server API + +- `createMcpApp(options)` - main entry, creates a full MCP server +- `registerJsonRenderTool(server, options)` - register a json-render tool on an existing server +- `registerJsonRenderResource(server, options)` - register the UI resource + +## Client API (`@json-render/mcp/app`) + +- `useJsonRenderApp(options?)` - React hook, returns `{ spec, loading, connected, error, callServerTool }` +- `buildAppHtml(options)` - generate HTML from bundled JS/CSS + +## Building the iframe HTML + +Bundle the React app into a single self-contained HTML file using Vite + `vite-plugin-singlefile`: + +```typescript +// vite.config.ts +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +export default defineConfig({ + plugins: [react(), viteSingleFile()], + build: { outDir: "dist" }, +}); +``` + +## Client Configuration + +### Cursor (`.cursor/mcp.json`) + +```json +{ + "mcpServers": { + "my-app": { + "command": "npx", + "args": ["tsx", "server.ts", "--stdio"] + } + } +} +``` + +### Claude Desktop + +```json +{ + "mcpServers": { + "my-app": { + "command": "npx", + "args": ["tsx", "/path/to/server.ts", "--stdio"] + } + } +} +``` + +## Dependencies + +```bash +# Server +npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk + +# Client (iframe) +npm install @json-render/react @json-render/shadcn react react-dom + +# Build tools +npm install -D vite @vitejs/plugin-react vite-plugin-singlefile +``` From 938b19b42a305880d76965b364cac48d3e13ed4a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 6 Mar 2026 19:58:08 -0600 Subject: [PATCH 6/9] prepare 0.12.0 (#185) # Conflicts: # README.md --- .changeset/config.json | 3 +- .changeset/v0-12-release.md | 18 ++ README.md | 23 ++- apps/web/app/(main)/docs/api/jotai/page.mdx | 104 ++++++++++ apps/web/app/(main)/docs/api/redux/page.mdx | 104 ++++++++++ apps/web/app/(main)/docs/api/xstate/page.mdx | 77 ++++++++ apps/web/app/(main)/docs/api/zustand/page.mdx | 108 +++++++++++ apps/web/app/api/docs-chat/route.ts | 2 +- apps/web/lib/docs-navigation.ts | 4 + apps/web/lib/page-titles.ts | 4 + packages/svelte/README.md | 155 +++++++++++++++ skills/json-render-codegen/SKILL.md | 112 +++++++++++ skills/json-render-jotai/SKILL.md | 66 +++++++ skills/json-render-react-pdf/SKILL.md | 142 ++++++++++++++ skills/json-render-redux/SKILL.md | 64 +++++++ skills/json-render-vue/SKILL.md | 177 ++++++++++++++++++ skills/json-render-xstate/SKILL.md | 45 +++++ skills/json-render-zustand/SKILL.md | 65 +++++++ 18 files changed, 1270 insertions(+), 3 deletions(-) create mode 100644 .changeset/v0-12-release.md create mode 100644 apps/web/app/(main)/docs/api/jotai/page.mdx create mode 100644 apps/web/app/(main)/docs/api/redux/page.mdx create mode 100644 apps/web/app/(main)/docs/api/xstate/page.mdx create mode 100644 apps/web/app/(main)/docs/api/zustand/page.mdx create mode 100644 packages/svelte/README.md create mode 100644 skills/json-render-codegen/SKILL.md create mode 100644 skills/json-render-jotai/SKILL.md create mode 100644 skills/json-render-react-pdf/SKILL.md create mode 100644 skills/json-render-redux/SKILL.md create mode 100644 skills/json-render-vue/SKILL.md create mode 100644 skills/json-render-xstate/SKILL.md create mode 100644 skills/json-render-zustand/SKILL.md diff --git a/.changeset/config.json b/.changeset/config.json index 31bbfd8a..b59ba4da 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -18,7 +18,8 @@ "@json-render/vue", "@json-render/xstate", "@json-render/image", - "@json-render/mcp" + "@json-render/mcp", + "@json-render/svelte" ] ], "linked": [], diff --git a/.changeset/v0-12-release.md b/.changeset/v0-12-release.md new file mode 100644 index 00000000..e385243b --- /dev/null +++ b/.changeset/v0-12-release.md @@ -0,0 +1,18 @@ +--- +"@json-render/core": minor +"@json-render/svelte": minor +"@json-render/react-email": minor +"@json-render/mcp": minor +--- + +Add Svelte renderer, React Email renderer, and MCP Apps integration. + +### New: + +- **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers. +- **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support. +- **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility. + +### Fixed: + +- **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency. diff --git a/README.md b/README.md index 36a408d7..bd3fdc31 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,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 # or for Vue npm install @json-render/core @json-render/vue +# or for Svelte +npm install @json-render/core @json-render/svelte ``` ## Why json-render? @@ -30,7 +32,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur - **Guardrailed** - AI can only use components in your catalog - **Predictable** - JSON output matches your schema, every time - **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 and 50+ Ant Design components ready to use ## Quick Start @@ -118,6 +120,7 @@ function Dashboard({ spec }) { | `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities | | `@json-render/react` | React renderer, contexts, hooks | | `@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/antd` | 50+ pre-built Ant Design components | | `@json-render/react-native` | React Native renderer with standard mobile components | @@ -125,6 +128,7 @@ function Dashboard({ spec }) { | `@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/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/zustand` | Zustand adapter for `StateStore` | | `@json-render/jotai` | Jotai adapter for `StateStore` | @@ -181,6 +185,23 @@ const { registry } = defineRegistry(catalog, { // ``` +### 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: +// +``` + ### shadcn/ui (Web) ```tsx diff --git a/apps/web/app/(main)/docs/api/jotai/page.mdx b/apps/web/app/(main)/docs/api/jotai/page.mdx new file mode 100644 index 00000000..d713b857 --- /dev/null +++ b/apps/web/app/(main)/docs/api/jotai/page.mdx @@ -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 + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeRequiredDescription
atom{'WritableAtom'}YesA writable atom holding the state model.
storeJotai StoreNoThe Jotai store instance. Defaults to a new store created internally. Pass your own to share state with {''}.
+ +### Example + +```typescript +import { atom } from "jotai"; +import { jotaiStateStore } from "@json-render/jotai"; +import { StateProvider } from "@json-render/react"; + +const uiAtom = atom>({ count: 0 }); +const store = jotaiStateStore({ atom: uiAtom }); +``` + +```tsx + + {/* json-render reads/writes go through Jotai */} + +``` + +### Shared Jotai Store + +If your app already uses a Jotai `` 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>({ count: 0 }); +const store = jotaiStateStore({ atom: uiAtom, store: jStore }); +``` + +```tsx + + + {/* Both json-render and useAtom() see the same state */} + + +``` + +## Re-exports + + + + + + + + + + + + + + +
ExportSource
StateStore@json-render/core
diff --git a/apps/web/app/(main)/docs/api/redux/page.mdx b/apps/web/app/(main)/docs/api/redux/page.mdx new file mode 100644 index 00000000..0bdddcb5 --- /dev/null +++ b/apps/web/app/(main)/docs/api/redux/page.mdx @@ -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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeRequiredDescription
storeStoreYesThe Redux store instance.
selector{'(state: S) => StateModel'}NoSelect the json-render slice from the Redux state tree. Defaults to {'(state) => state'}.
dispatch{'(nextState: StateModel, store: Store) => void'}YesDispatch an action that replaces the selected slice with the next state.
+ +### 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, + 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 + + {/* json-render reads/writes go through Redux */} + +``` + +## Re-exports + + + + + + + + + + + + + + +
ExportSource
StateStore@json-render/core
diff --git a/apps/web/app/(main)/docs/api/xstate/page.mdx b/apps/web/app/(main)/docs/api/xstate/page.mdx new file mode 100644 index 00000000..d804e322 --- /dev/null +++ b/apps/web/app/(main)/docs/api/xstate/page.mdx @@ -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 + + + + + + + + + + + + + + + + + + +
OptionTypeRequiredDescription
atom{'Atom'}YesAn @xstate/store atom (from createAtom) holding the json-render state model.
+ +### 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 + + {/* json-render reads/writes go through @xstate/store */} + +``` + +## Re-exports + + + + + + + + + + + + + + +
ExportSource
StateStore@json-render/core
diff --git a/apps/web/app/(main)/docs/api/zustand/page.mdx b/apps/web/app/(main)/docs/api/zustand/page.mdx new file mode 100644 index 00000000..1126a988 --- /dev/null +++ b/apps/web/app/(main)/docs/api/zustand/page.mdx @@ -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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeRequiredDescription
store{'StoreApi'}YesA Zustand vanilla store (from createStore in zustand/vanilla).
selector{'(state: S) => StateModel'}NoSelect the json-render slice from the store state. Defaults to the entire state.
updater{'(nextState: StateModel, store: StoreApi) => void'}NoApply a state change back to the store. Defaults to a shallow merge.
+ +### 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 + + {/* json-render reads/writes go through Zustand */} + +``` + +### 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 + + + + + + + + + + + + + + +
ExportSource
StateStore@json-render/core
diff --git a/apps/web/app/api/docs-chat/route.ts b/apps/web/app/api/docs-chat/route.ts index 6156e8ec..5c6b968f 100644 --- a/apps/web/app/api/docs-chat/route.ts +++ b/apps/web/app/api/docs-chat/route.ts @@ -16,7 +16,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render GitHub repository: https://github.com/vercel-labs/json-render Documentation: https://json-render.dev/docs -npm packages: @json-render/core, @json-render/react, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp +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 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. diff --git a/apps/web/lib/docs-navigation.ts b/apps/web/lib/docs-navigation.ts index ff261eda..ec93e6ee 100644 --- a/apps/web/lib/docs-navigation.ts +++ b/apps/web/lib/docs-navigation.ts @@ -134,6 +134,10 @@ export const docsNavigation: NavSection[] = [ { title: "@json-render/svelte", href: "/docs/api/svelte" }, { 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" }, ], }, ]; diff --git a/apps/web/lib/page-titles.ts b/apps/web/lib/page-titles.ts index f08cba21..2e92cdf1 100644 --- a/apps/web/lib/page-titles.ts +++ b/apps/web/lib/page-titles.ts @@ -51,6 +51,10 @@ export const PAGE_TITLES: Record = { "docs/api/remotion": "@json-render/remotion 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", }; /** diff --git a/packages/svelte/README.md b/packages/svelte/README.md new file mode 100644 index 00000000..32f56dd6 --- /dev/null +++ b/packages/svelte/README.md @@ -0,0 +1,155 @@ +# @json-render/svelte + +Svelte 5 renderer for `@json-render/core`. Turn JSON specs into Svelte components with runes-based reactivity. + +## Installation + +```bash +npm install @json-render/core @json-render/svelte +``` + +Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`. + +## Quick Start + +### 1. Create a Catalog + +```typescript +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/svelte/schema"; +import { z } from "zod"; + +export const catalog = defineCatalog(schema, { + components: { + Card: { + props: z.object({ + title: z.string(), + description: z.string().nullable(), + }), + description: "A card container", + }, + Button: { + props: z.object({ + label: z.string(), + action: z.string(), + }), + description: "A clickable button", + }, + }, + actions: { + submit: { description: "Submit the form" }, + }, +}); +``` + +### 2. Define Component Implementations + +```typescript +import { defineRegistry } from "@json-render/svelte"; +import { catalog } from "./catalog"; + +export const { registry } = defineRegistry(catalog, { + components: { + Card: ({ props, children }) => /* Svelte 5 snippet */, + Button: ({ props, emit }) => /* Svelte 5 snippet */, + }, + actions: { + submit: async (params) => { /* handle submit */ }, + }, +}); +``` + +### 3. Render Specs + +```svelte + + + + + + + +``` + +## Providers + +| Provider | Purpose | +|----------|---------| +| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. | +| `ActionProvider` | Handle actions dispatched via the event system | +| `VisibilityProvider` | Enable conditional rendering based on state | +| `ValidationProvider` | Form field validation | +| `RepeatScopeProvider` | Repeat scope for list rendering | +| `FunctionsContextProvider` | Register `$computed` functions | +| `JsonUIProvider` | All-in-one provider combining state, actions, visibility, validation, and functions | + +## Context Accessors + +Svelte 5 uses `getContext`-based accessors instead of hooks: + +| Accessor | Purpose | +|----------|---------| +| `getStateContext()` | Access state context (`state`, `get`, `set`) | +| `getStateValue(path)` | Get single value from state | +| `getBoundProp(value, binding)` | Two-way binding for `$bindState`/`$bindItem` | +| `getVisibilityContext()` | Access visibility context | +| `isVisible(condition)` | Check if a visibility condition is met | +| `getActionContext()` | Access action context | +| `getAction(binding)` | Get a single action dispatch function | +| `getValidationContext()` | Access validation context | +| `getFieldValidation(path, config)` | Field validation state | +| `getRepeatScope()` | Access current repeat scope | +| `getFunctions()` | Access registered computed functions | + +## Streaming + +```typescript +import { createUIStream, createChatUI } from "@json-render/svelte"; + +const stream = createUIStream({ endpoint: "/api/generate" }); + +// Or for chat-style UI: +const chat = createChatUI({ endpoint: "/api/chat" }); +``` + +## Key Exports + +| Export | Purpose | +|--------|---------| +| `defineRegistry` | Create a type-safe component registry from a catalog | +| `createRenderer` | Create a renderer function from a registry | +| `Renderer` | Render a spec using a registry | +| `CatalogRenderer` | Render with automatic provider wiring | +| `JsonUIProvider` | All-in-one provider | +| `schema` | Element tree schema (includes built-in state actions) | +| `createUIStream` | Stream specs from an API endpoint | +| `createChatUI` | Chat-style streaming interface | +| `flatToTree` | Convert flat spec to tree format | +| `ConfirmDialog` | Confirmation dialog component | +| `ConfirmDialogManager` | Manage multiple confirmation dialogs | + +### Types + +| Export | Purpose | +|--------|---------| +| `ComponentContext` | Typed component render function context (catalog-aware) | +| `BaseComponentProps` | Catalog-agnostic base type for reusable component libraries | +| `EventHandle` | Event handle with `emit()`, `shouldPreventDefault`, `bound` | +| `ComponentFn` | Component render function type | +| `SetState` | State setter type | +| `StateModel` | State model type | +| `SvelteSchema` | Schema type for the Svelte renderer | +| `SvelteSpec` | Spec type for the Svelte renderer | + +## Documentation + +Full API reference: [json-render.dev/docs/api/svelte](https://json-render.dev/docs/api/svelte). + +## License + +Apache-2.0 diff --git a/skills/json-render-codegen/SKILL.md b/skills/json-render-codegen/SKILL.md new file mode 100644 index 00000000..b0fd5ec3 --- /dev/null +++ b/skills/json-render-codegen/SKILL.md @@ -0,0 +1,112 @@ +--- +name: json-render-codegen +description: Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing specs, or serializing props for @json-render/codegen. +--- + +# @json-render/codegen + +Framework-agnostic utilities for generating code from json-render UI trees. Use these to build custom code exporters for Next.js, Remix, or other frameworks. + +## Installation + +```bash +npm install @json-render/codegen +``` + +## Tree Traversal + +```typescript +import { + traverseSpec, + collectUsedComponents, + collectStatePaths, + collectActions, +} from "@json-render/codegen"; + +// Walk the spec depth-first +traverseSpec(spec, (element, key, depth, parent) => { + console.log(`${" ".repeat(depth * 2)}${key}: ${element.type}`); +}); + +// Get all component types used +const components = collectUsedComponents(spec); +// Set { "Card", "Metric", "Button" } + +// Get all state paths referenced +const statePaths = collectStatePaths(spec); +// Set { "analytics/revenue", "user/name" } + +// Get all action names +const actions = collectActions(spec); +// Set { "submit_form", "refresh_data" } +``` + +## Serialization + +```typescript +import { + serializePropValue, + serializeProps, + escapeString, + type SerializeOptions, +} from "@json-render/codegen"; + +// Serialize a single value +serializePropValue("hello"); +// { value: '"hello"', needsBraces: false } + +serializePropValue({ $state: "/user/name" }); +// { value: '{ $state: "/user/name" }', needsBraces: true } + +// Serialize props for JSX +serializeProps({ title: "Dashboard", columns: 3, disabled: true }); +// 'title="Dashboard" columns={3} disabled' + +// Escape strings for code +escapeString('hello "world"'); +// 'hello \"world\"' +``` + +### SerializeOptions + +```typescript +interface SerializeOptions { + quotes?: "single" | "double"; + indent?: number; +} +``` + +## Types + +```typescript +import type { GeneratedFile, CodeGenerator } from "@json-render/codegen"; + +const myGenerator: CodeGenerator = { + generate(spec) { + return [ + { path: "package.json", content: "..." }, + { path: "app/page.tsx", content: "..." }, + ]; + }, +}; +``` + +## Building a Custom Generator + +```typescript +import { + collectUsedComponents, + collectStatePaths, + traverseSpec, + serializeProps, + type GeneratedFile, +} from "@json-render/codegen"; +import type { Spec } from "@json-render/core"; + +export function generateNextJSProject(spec: Spec): GeneratedFile[] { + const files: GeneratedFile[] = []; + const components = collectUsedComponents(spec); + // Generate package.json, component files, main page... + return files; +} +``` diff --git a/skills/json-render-jotai/SKILL.md b/skills/json-render-jotai/SKILL.md new file mode 100644 index 00000000..a1c24196 --- /dev/null +++ b/skills/json-render-jotai/SKILL.md @@ -0,0 +1,66 @@ +--- +name: json-render-jotai +description: Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai. +--- + +# @json-render/jotai + +Jotai adapter for json-render's `StateStore` interface. Wire a Jotai atom as the state backend for json-render. + +## Installation + +```bash +npm install @json-render/jotai @json-render/core @json-render/react jotai +``` + +## Usage + +```tsx +import { atom } from "jotai"; +import { jotaiStateStore } from "@json-render/jotai"; +import { StateProvider } from "@json-render/react"; + +// 1. Create an atom that holds the json-render state +const uiAtom = atom>({ count: 0 }); + +// 2. Create the json-render StateStore adapter +const store = jotaiStateStore({ atom: uiAtom }); + +// 3. Use it + + {/* json-render reads/writes go through Jotai */} + +``` + +### With a Shared Jotai Store + +When your app already uses a Jotai `` with a custom store, pass it so both json-render and your components share the same state: + +```tsx +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>({ count: 0 }); + +const store = jotaiStateStore({ atom: uiAtom, store: jStore }); + + + + {/* Both json-render and useAtom() see the same state */} + + +``` + +## API + +### `jotaiStateStore(options)` + +Creates a `StateStore` backed by a Jotai atom. + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `atom` | `WritableAtom` | Yes | A writable atom holding the state model | +| `store` | Jotai `Store` | No | The Jotai store instance. Defaults to a new store. Pass your own to share state with ``. | diff --git a/skills/json-render-react-pdf/SKILL.md b/skills/json-render-react-pdf/SKILL.md new file mode 100644 index 00000000..0fed196b --- /dev/null +++ b/skills/json-render-react-pdf/SKILL.md @@ -0,0 +1,142 @@ +--- +name: json-render-react-pdf +description: React PDF renderer for json-render. Use when generating PDF documents from JSON specs, working with @json-render/react-pdf, or rendering specs to PDF buffers/streams/files. +--- + +# @json-render/react-pdf + +React PDF renderer that generates PDF documents from JSON specs using `@react-pdf/renderer`. + +## Installation + +```bash +npm install @json-render/core @json-render/react-pdf +``` + +## Quick Start + +```typescript +import { renderToBuffer } from "@json-render/react-pdf"; +import type { Spec } from "@json-render/core"; + +const spec: Spec = { + root: "doc", + elements: { + doc: { type: "Document", props: { title: "Invoice" }, children: ["page"] }, + page: { + type: "Page", + props: { size: "A4" }, + children: ["heading", "table"], + }, + heading: { + type: "Heading", + props: { text: "Invoice #1234", level: "h1" }, + children: [], + }, + table: { + type: "Table", + props: { + columns: [ + { header: "Item", width: "60%" }, + { header: "Price", width: "40%", align: "right" }, + ], + rows: [ + ["Widget A", "$10.00"], + ["Widget B", "$25.00"], + ], + }, + children: [], + }, + }, +}; + +const buffer = await renderToBuffer(spec); +``` + +## Render APIs + +```typescript +import { renderToBuffer, renderToStream, renderToFile } from "@json-render/react-pdf"; + +// In-memory buffer +const buffer = await renderToBuffer(spec); + +// Readable stream (pipe to HTTP response) +const stream = await renderToStream(spec); +stream.pipe(res); + +// Direct to file +await renderToFile(spec, "./output.pdf"); +``` + +All render functions accept an optional second argument: `{ registry?, state?, handlers? }`. + +## Standard Components + +| Component | Description | +|-----------|-------------| +| `Document` | Top-level PDF wrapper (must be root) | +| `Page` | Page with size (A4, LETTER), orientation, margins | +| `View` | Generic container (padding, margin, background, border) | +| `Row`, `Column` | Flex layout with gap, align, justify | +| `Heading` | h1-h4 heading text | +| `Text` | Body text (fontSize, color, weight, alignment) | +| `Image` | Image from URL or base64 | +| `Link` | Hyperlink with text and href | +| `Table` | Data table with typed columns and rows | +| `List` | Ordered or unordered list | +| `Divider` | Horizontal line separator | +| `Spacer` | Empty vertical space | +| `PageNumber` | Current page number and total pages | + +## Custom Catalog + +```typescript +import { defineCatalog } from "@json-render/core"; +import { schema, defineRegistry, renderToBuffer } from "@json-render/react-pdf"; +import { standardComponentDefinitions } from "@json-render/react-pdf/catalog"; +import { z } from "zod"; + +const catalog = defineCatalog(schema, { + components: { + ...standardComponentDefinitions, + Badge: { + props: z.object({ label: z.string(), color: z.string().nullable() }), + slots: [], + description: "A colored badge label", + }, + }, + actions: {}, +}); + +const { registry } = defineRegistry(catalog, { + components: { + Badge: ({ props }) => ( + + {props.label} + + ), + }, +}); + +const buffer = await renderToBuffer(spec, { registry }); +``` + +## External Store (Controlled Mode) + +Pass a `StateStore` for full control over state: + +```typescript +import { createStateStore } from "@json-render/react-pdf"; + +const store = createStateStore({ invoice: { total: 100 } }); +store.set("/invoice/total", 200); +``` + +## Server-Safe Import + +Import schema and catalog without pulling in React: + +```typescript +import { schema, standardComponentDefinitions } from "@json-render/react-pdf/server"; +``` diff --git a/skills/json-render-redux/SKILL.md b/skills/json-render-redux/SKILL.md new file mode 100644 index 00000000..7adcae14 --- /dev/null +++ b/skills/json-render-redux/SKILL.md @@ -0,0 +1,64 @@ +--- +name: json-render-redux +description: Redux adapter for json-render's StateStore interface. Use when integrating json-render with Redux or Redux Toolkit for state management via @json-render/redux. +--- + +# @json-render/redux + +Redux adapter for json-render's `StateStore` interface. Wire a Redux store (or Redux Toolkit slice) as the state backend for json-render. + +## 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 +``` + +## Usage + +```tsx +import { configureStore, createSlice } from "@reduxjs/toolkit"; +import { reduxStateStore } from "@json-render/redux"; +import { StateProvider } from "@json-render/react"; + +// 1. Define a slice for json-render state +const uiSlice = createSlice({ + name: "ui", + initialState: { count: 0 } as Record, + reducers: { + replaceUiState: (_state, action) => action.payload, + }, +}); + +// 2. Create the Redux store +const reduxStore = configureStore({ + reducer: { ui: uiSlice.reducer }, +}); + +// 3. Create the json-render StateStore adapter +const store = reduxStateStore({ + store: reduxStore, + selector: (state) => state.ui, + dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)), +}); + +// 4. Use it + + {/* json-render reads/writes go through Redux */} + +``` + +## API + +### `reduxStateStore(options)` + +Creates a `StateStore` backed by a Redux store. + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `store` | `Store` | Yes | The Redux store instance | +| `selector` | `(state) => StateModel` | Yes | Select the json-render slice from the Redux state tree. Use `(s) => s` if the entire state is the model. | +| `dispatch` | `(nextState, store) => void` | Yes | Dispatch an action that replaces the selected slice with the next state | + +The `dispatch` callback receives the full next state model and the Redux store. diff --git a/skills/json-render-vue/SKILL.md b/skills/json-render-vue/SKILL.md new file mode 100644 index 00000000..30e9df7d --- /dev/null +++ b/skills/json-render-vue/SKILL.md @@ -0,0 +1,177 @@ +--- +name: json-render-vue +description: Vue 3 renderer for json-render. Use when building Vue UIs from JSON specs, working with @json-render/vue, defining Vue component registries, or rendering AI-generated specs in Vue. +--- + +# @json-render/vue + +Vue 3 renderer that converts JSON specs into Vue component trees with data binding, visibility, and actions. + +## Installation + +```bash +npm install @json-render/vue @json-render/core zod +``` + +Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`. + +## Quick Start + +### Create a Catalog + +```typescript +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/vue/schema"; +import { z } from "zod"; + +export const catalog = defineCatalog(schema, { + components: { + Card: { + props: z.object({ title: z.string(), description: z.string().nullable() }), + description: "A card container", + }, + Button: { + props: z.object({ label: z.string(), action: z.string() }), + description: "A clickable button", + }, + }, + actions: {}, +}); +``` + +### Define Registry with h() Render Functions + +```typescript +import { h } from "vue"; +import { defineRegistry } from "@json-render/vue"; +import { catalog } from "./catalog"; + +export const { registry } = defineRegistry(catalog, { + components: { + Card: ({ props, children }) => + h("div", { class: "card" }, [ + h("h3", null, props.title), + props.description ? h("p", null, props.description) : null, + children, + ]), + Button: ({ props, emit }) => + h("button", { onClick: () => emit("press") }, props.label), + }, +}); +``` + +### Render Specs + +```vue + + + +``` + +## Providers + +| Provider | Purpose | +|----------|---------| +| `StateProvider` | Share state across components (JSON Pointer paths). Accepts `initialState` or `store` for controlled mode. | +| `ActionProvider` | Handle actions dispatched via the event system | +| `VisibilityProvider` | Enable conditional rendering based on state | +| `ValidationProvider` | Form field validation | + +## Composables + +| Composable | Purpose | +|------------|---------| +| `useStateStore()` | Access state context (`state` as `ShallowRef`, `get`, `set`, `update`) | +| `useStateValue(path)` | Get single value from state | +| `useIsVisible(condition)` | Check if a visibility condition is met | +| `useActions()` | Access action context | +| `useAction(binding)` | Get a single action dispatch function | +| `useFieldValidation(path, config)` | Field validation state | +| `useBoundProp(propValue, bindingPath)` | Two-way binding for `$bindState`/`$bindItem` | + +Note: `useStateStore().state` returns a `ShallowRef` — use `state.value` to access. + +## External Store (StateStore) + +Pass a `StateStore` to `StateProvider` to wire json-render to Pinia, VueUse, or any state management: + +```typescript +import { createStateStore, type StateStore } from "@json-render/vue"; + +const store = createStateStore({ count: 0 }); +``` + +```vue + + + +``` + +## Dynamic Prop Expressions + +Props support `$state`, `$bindState`, `$cond`, `$template`, `$computed`. Use `{ "$bindState": "/path" }` on the natural value prop for two-way binding. + +## Visibility Conditions + +```typescript +{ "$state": "/user/isAdmin" } +{ "$state": "/status", "eq": "active" } +{ "$state": "/maintenance", "not": true } +[ cond1, cond2 ] // implicit AND +``` + +## Built-in Actions + +`setState`, `pushState`, `removeState`, and `validateForm` are built into the Vue schema and handled by `ActionProvider`: + +```json +{ + "action": "setState", + "params": { "statePath": "/activeTab", "value": "settings" } +} +``` + +## Event System + +Components use `emit(event)` to fire events, or `on(event)` for metadata (`shouldPreventDefault`, `bound`). + +## Streaming + +`useUIStream` and `useChatUI` return Vue Refs for streaming specs from an API. + +## BaseComponentProps + +For catalog-agnostic reusable components: + +```typescript +import type { BaseComponentProps } from "@json-render/vue"; + +const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => + h("div", null, [props.title, children]); +``` + +## Key Exports + +| Export | Purpose | +|--------|---------| +| `defineRegistry` | Create a type-safe component registry from a catalog | +| `Renderer` | Render a spec using a registry | +| `schema` | Element tree schema (from `@json-render/vue/schema`) | +| `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider` | Context providers | +| `useStateStore`, `useStateValue`, `useBoundProp` | State composables | +| `useActions`, `useAction` | Action composables | +| `useFieldValidation`, `useIsVisible` | Validation and visibility | +| `useUIStream`, `useChatUI` | Streaming composables | +| `createStateStore` | Create in-memory `StateStore` | +| `BaseComponentProps` | Catalog-agnostic component props type | diff --git a/skills/json-render-xstate/SKILL.md b/skills/json-render-xstate/SKILL.md new file mode 100644 index 00000000..2ea75f71 --- /dev/null +++ b/skills/json-render-xstate/SKILL.md @@ -0,0 +1,45 @@ +--- +name: json-render-xstate +description: XState Store adapter for json-render's StateStore interface. Use when integrating json-render with @xstate/store for state management via @json-render/xstate. +--- + +# @json-render/xstate + +[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend for json-render. + +## Installation + +```bash +npm install @json-render/xstate @json-render/core @json-render/react @xstate/store +``` + +Requires `@xstate/store` v3+. + +## Usage + +```tsx +import { createAtom } from "@xstate/store"; +import { xstateStoreStateStore } from "@json-render/xstate"; +import { StateProvider } from "@json-render/react"; + +// 1. Create an atom +const uiAtom = createAtom({ count: 0 }); + +// 2. Create the json-render StateStore adapter +const store = xstateStoreStateStore({ atom: uiAtom }); + +// 3. Use it + + {/* json-render reads/writes go through @xstate/store */} + +``` + +## API + +### `xstateStoreStateStore(options)` + +Creates a `StateStore` backed by an `@xstate/store` atom. + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `atom` | `Atom` | Yes | An `@xstate/store` atom (from `createAtom`) holding the json-render state model | diff --git a/skills/json-render-zustand/SKILL.md b/skills/json-render-zustand/SKILL.md new file mode 100644 index 00000000..f97633cc --- /dev/null +++ b/skills/json-render-zustand/SKILL.md @@ -0,0 +1,65 @@ +--- +name: json-render-zustand +description: Zustand adapter for json-render's StateStore interface. Use when integrating json-render with Zustand for state management via @json-render/zustand. +--- + +# @json-render/zustand + +Zustand adapter for json-render's `StateStore` interface. Wire a Zustand vanilla store as the state backend for json-render. + +## Installation + +```bash +npm install @json-render/zustand @json-render/core @json-render/react zustand +``` + +Requires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface. + +## Usage + +```tsx +import { createStore } from "zustand/vanilla"; +import { zustandStateStore } from "@json-render/zustand"; +import { StateProvider } from "@json-render/react"; + +// 1. Create a Zustand vanilla store +const bearStore = createStore(() => ({ + count: 0, + name: "Bear", +})); + +// 2. Create the json-render StateStore adapter +const store = zustandStateStore({ store: bearStore }); + +// 3. Use it + + {/* json-render reads/writes go through Zustand */} + +``` + +### With a Nested Slice + +```tsx +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 }), +}); +``` + +## API + +### `zustandStateStore(options)` + +Creates a `StateStore` backed by a Zustand store. + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `store` | `StoreApi` | Yes | Zustand vanilla store (from `createStore` in `zustand/vanilla`) | +| `selector` | `(state) => StateModel` | No | Select the json-render slice. Defaults to entire state. | +| `updater` | `(nextState, store) => void` | No | Apply next state to the store. Defaults to shallow merge. Override for nested slices, or use `(next, s) => s.setState(next, true)` for full replacement. | From 55fc9aca5aaa916f1d162fc5f9c31ebcb56c384e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:02:39 -0600 Subject: [PATCH 7/9] chore: version packages (#186) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/v0-12-release.md | 18 ----------------- apps/web/CHANGELOG.md | 9 +++++++++ apps/web/package.json | 2 +- examples/chat/CHANGELOG.md | 9 +++++++++ examples/chat/package.json | 2 +- examples/dashboard/CHANGELOG.md | 9 +++++++++ examples/dashboard/package.json | 2 +- examples/image/CHANGELOG.md | 8 ++++++++ examples/image/package.json | 2 +- examples/mcp/CHANGELOG.md | 11 ++++++++++ examples/mcp/package.json | 2 +- examples/no-ai/CHANGELOG.md | 9 +++++++++ examples/no-ai/package.json | 2 +- examples/react-email/CHANGELOG.md | 9 +++++++++ examples/react-email/package.json | 2 +- examples/react-native/CHANGELOG.md | 8 ++++++++ examples/react-native/package.json | 2 +- examples/react-pdf/CHANGELOG.md | 8 ++++++++ examples/react-pdf/package.json | 2 +- examples/remotion/CHANGELOG.md | 8 ++++++++ examples/remotion/package.json | 2 +- examples/stripe-app/drawer-app/CHANGELOG.md | 8 ++++++++ examples/stripe-app/drawer-app/package.json | 2 +- examples/stripe-app/fullpage-app/CHANGELOG.md | 8 ++++++++ examples/stripe-app/fullpage-app/package.json | 2 +- examples/svelte-chat/CHANGELOG.md | 9 +++++++++ examples/svelte-chat/package.json | 2 +- examples/svelte/CHANGELOG.md | 9 +++++++++ examples/svelte/package.json | 2 +- examples/vite-renderers/CHANGELOG.md | 10 ++++++++++ examples/vite-renderers/package.json | 2 +- examples/vue/CHANGELOG.md | 8 ++++++++ examples/vue/package.json | 2 +- packages/codegen/CHANGELOG.md | 7 +++++++ packages/codegen/package.json | 2 +- packages/core/CHANGELOG.md | 14 +++++++++++++ packages/core/package.json | 2 +- packages/image/CHANGELOG.md | 7 +++++++ packages/image/package.json | 2 +- packages/jotai/CHANGELOG.md | 7 +++++++ packages/jotai/package.json | 2 +- packages/mcp/CHANGELOG.md | 20 +++++++++++++++++++ packages/mcp/package.json | 2 +- packages/react-email/CHANGELOG.md | 20 +++++++++++++++++++ packages/react-email/package.json | 2 +- packages/react-native/CHANGELOG.md | 7 +++++++ packages/react-native/package.json | 2 +- packages/react-pdf/CHANGELOG.md | 7 +++++++ packages/react-pdf/package.json | 2 +- packages/react-state/CHANGELOG.md | 7 +++++++ packages/react-state/package.json | 2 +- packages/react/CHANGELOG.md | 7 +++++++ packages/react/package.json | 2 +- packages/redux/CHANGELOG.md | 7 +++++++ packages/redux/package.json | 2 +- packages/remotion/CHANGELOG.md | 7 +++++++ packages/remotion/package.json | 2 +- packages/shadcn/CHANGELOG.md | 8 ++++++++ packages/shadcn/package.json | 2 +- packages/svelte/CHANGELOG.md | 20 +++++++++++++++++++ packages/svelte/package.json | 2 +- packages/vue/CHANGELOG.md | 7 +++++++ packages/vue/package.json | 2 +- packages/xstate/CHANGELOG.md | 7 +++++++ packages/xstate/package.json | 2 +- packages/zustand/CHANGELOG.md | 7 +++++++ packages/zustand/package.json | 2 +- 67 files changed, 339 insertions(+), 51 deletions(-) delete mode 100644 .changeset/v0-12-release.md create mode 100644 examples/mcp/CHANGELOG.md create mode 100644 examples/react-email/CHANGELOG.md create mode 100644 examples/svelte-chat/CHANGELOG.md create mode 100644 examples/svelte/CHANGELOG.md create mode 100644 packages/mcp/CHANGELOG.md create mode 100644 packages/react-email/CHANGELOG.md create mode 100644 packages/svelte/CHANGELOG.md diff --git a/.changeset/v0-12-release.md b/.changeset/v0-12-release.md deleted file mode 100644 index e385243b..00000000 --- a/.changeset/v0-12-release.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@json-render/core": minor -"@json-render/svelte": minor -"@json-render/react-email": minor -"@json-render/mcp": minor ---- - -Add Svelte renderer, React Email renderer, and MCP Apps integration. - -### New: - -- **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers. -- **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support. -- **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility. - -### Fixed: - -- **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency. diff --git a/apps/web/CHANGELOG.md b/apps/web/CHANGELOG.md index 86e06d41..9e16c610 100644 --- a/apps/web/CHANGELOG.md +++ b/apps/web/CHANGELOG.md @@ -1,5 +1,14 @@ # web +## 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 ### Patch Changes diff --git a/apps/web/package.json b/apps/web/package.json index 2f8cf645..5af0df71 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.1.4", + "version": "0.1.5", "type": "module", "private": true, "license": "Apache-2.0", diff --git a/examples/chat/CHANGELOG.md b/examples/chat/CHANGELOG.md index f4540925..e5ac8b59 100644 --- a/examples/chat/CHANGELOG.md +++ b/examples/chat/CHANGELOG.md @@ -1,5 +1,14 @@ # example-chat +## 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 ### Patch Changes diff --git a/examples/chat/package.json b/examples/chat/package.json index 184cd6c6..ed822b0a 100644 --- a/examples/chat/package.json +++ b/examples/chat/package.json @@ -1,6 +1,6 @@ { "name": "example-chat", - "version": "0.1.4", + "version": "0.1.5", "type": "module", "private": true, "scripts": { diff --git a/examples/dashboard/CHANGELOG.md b/examples/dashboard/CHANGELOG.md index 980608eb..83ccb0d4 100644 --- a/examples/dashboard/CHANGELOG.md +++ b/examples/dashboard/CHANGELOG.md @@ -1,5 +1,14 @@ # example-dashboard +## 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 ### Patch Changes diff --git a/examples/dashboard/package.json b/examples/dashboard/package.json index 4beca85b..8ae675bd 100644 --- a/examples/dashboard/package.json +++ b/examples/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "example-dashboard", - "version": "0.1.4", + "version": "0.1.5", "type": "module", "private": true, "scripts": { diff --git a/examples/image/CHANGELOG.md b/examples/image/CHANGELOG.md index d7b290dd..c8b91334 100644 --- a/examples/image/CHANGELOG.md +++ b/examples/image/CHANGELOG.md @@ -1,5 +1,13 @@ # example-image +## 0.1.2 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/image@0.12.0 + ## 0.1.1 ### Patch Changes diff --git a/examples/image/package.json b/examples/image/package.json index cc550b28..e3eb2b39 100644 --- a/examples/image/package.json +++ b/examples/image/package.json @@ -1,6 +1,6 @@ { "name": "example-image", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "private": true, "scripts": { diff --git a/examples/mcp/CHANGELOG.md b/examples/mcp/CHANGELOG.md new file mode 100644 index 00000000..5ed68643 --- /dev/null +++ b/examples/mcp/CHANGELOG.md @@ -0,0 +1,11 @@ +# example-mcp + +## 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 diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 11961880..310100d4 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -1,6 +1,6 @@ { "name": "example-mcp", - "version": "0.1.0", + "version": "0.1.1", "type": "module", "private": true, "scripts": { diff --git a/examples/no-ai/CHANGELOG.md b/examples/no-ai/CHANGELOG.md index 06870651..1eeb67d2 100644 --- a/examples/no-ai/CHANGELOG.md +++ b/examples/no-ai/CHANGELOG.md @@ -1,5 +1,14 @@ # example-no-ai +## 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 ### Patch Changes diff --git a/examples/no-ai/package.json b/examples/no-ai/package.json index dfdb99db..c728b855 100644 --- a/examples/no-ai/package.json +++ b/examples/no-ai/package.json @@ -1,6 +1,6 @@ { "name": "example-no-ai", - "version": "0.1.4", + "version": "0.1.5", "type": "module", "private": true, "scripts": { diff --git a/examples/react-email/CHANGELOG.md b/examples/react-email/CHANGELOG.md new file mode 100644 index 00000000..d745f440 --- /dev/null +++ b/examples/react-email/CHANGELOG.md @@ -0,0 +1,9 @@ +# example-react-email + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/react-email@0.12.0 diff --git a/examples/react-email/package.json b/examples/react-email/package.json index 882fee33..16de708a 100644 --- a/examples/react-email/package.json +++ b/examples/react-email/package.json @@ -1,6 +1,6 @@ { "name": "example-react-email", - "version": "0.1.0", + "version": "0.1.1", "type": "module", "private": true, "scripts": { diff --git a/examples/react-native/CHANGELOG.md b/examples/react-native/CHANGELOG.md index 1f259c36..6fcb8be2 100644 --- a/examples/react-native/CHANGELOG.md +++ b/examples/react-native/CHANGELOG.md @@ -1,5 +1,13 @@ # example-react-native +## 0.1.5 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/react-native@0.12.0 + ## 0.1.4 ### Patch Changes diff --git a/examples/react-native/package.json b/examples/react-native/package.json index e642023f..166b93a7 100644 --- a/examples/react-native/package.json +++ b/examples/react-native/package.json @@ -1,6 +1,6 @@ { "name": "example-react-native", - "version": "0.1.4", + "version": "0.1.5", "private": true, "main": "expo-router/entry", "scripts": { diff --git a/examples/react-pdf/CHANGELOG.md b/examples/react-pdf/CHANGELOG.md index 4dda29ef..2a515e53 100644 --- a/examples/react-pdf/CHANGELOG.md +++ b/examples/react-pdf/CHANGELOG.md @@ -1,5 +1,13 @@ # example-react-pdf +## 0.1.5 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/react-pdf@0.12.0 + ## 0.1.4 ### Patch Changes diff --git a/examples/react-pdf/package.json b/examples/react-pdf/package.json index 85376b6e..ab81dfa8 100644 --- a/examples/react-pdf/package.json +++ b/examples/react-pdf/package.json @@ -1,6 +1,6 @@ { "name": "example-react-pdf", - "version": "0.1.4", + "version": "0.1.5", "type": "module", "private": true, "scripts": { diff --git a/examples/remotion/CHANGELOG.md b/examples/remotion/CHANGELOG.md index db04d717..845004eb 100644 --- a/examples/remotion/CHANGELOG.md +++ b/examples/remotion/CHANGELOG.md @@ -1,5 +1,13 @@ # example-remotion +## 0.1.5 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/remotion@0.12.0 + ## 0.1.4 ### Patch Changes diff --git a/examples/remotion/package.json b/examples/remotion/package.json index 8cf7d159..8ea82ca7 100644 --- a/examples/remotion/package.json +++ b/examples/remotion/package.json @@ -1,6 +1,6 @@ { "name": "example-remotion", - "version": "0.1.4", + "version": "0.1.5", "type": "module", "private": true, "scripts": { diff --git a/examples/stripe-app/drawer-app/CHANGELOG.md b/examples/stripe-app/drawer-app/CHANGELOG.md index e46dccf0..18a3059b 100644 --- a/examples/stripe-app/drawer-app/CHANGELOG.md +++ b/examples/stripe-app/drawer-app/CHANGELOG.md @@ -1,5 +1,13 @@ # com.example.json-render-demo +## 0.0.6 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/react@0.12.0 + ## 0.0.5 ### Patch Changes diff --git a/examples/stripe-app/drawer-app/package.json b/examples/stripe-app/drawer-app/package.json index e0e0082c..46fa88c6 100644 --- a/examples/stripe-app/drawer-app/package.json +++ b/examples/stripe-app/drawer-app/package.json @@ -1,6 +1,6 @@ { "name": "com.example.json-render-demo", - "version": "0.0.5", + "version": "0.0.6", "description": "Test", "private": true, "license": "~~proprietary~~", diff --git a/examples/stripe-app/fullpage-app/CHANGELOG.md b/examples/stripe-app/fullpage-app/CHANGELOG.md index eff82b5c..c66f28b6 100644 --- a/examples/stripe-app/fullpage-app/CHANGELOG.md +++ b/examples/stripe-app/fullpage-app/CHANGELOG.md @@ -1,5 +1,13 @@ # com.example.json-render-fullpage-demo +## 0.0.6 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/react@0.12.0 + ## 0.0.5 ### Patch Changes diff --git a/examples/stripe-app/fullpage-app/package.json b/examples/stripe-app/fullpage-app/package.json index 2304f36e..4b0e118a 100644 --- a/examples/stripe-app/fullpage-app/package.json +++ b/examples/stripe-app/fullpage-app/package.json @@ -1,6 +1,6 @@ { "name": "com.example.json-render-fullpage-demo", - "version": "0.0.5", + "version": "0.0.6", "description": "Full-page Stripe App example (alpha)", "private": true, "license": "~~proprietary~~", diff --git a/examples/svelte-chat/CHANGELOG.md b/examples/svelte-chat/CHANGELOG.md new file mode 100644 index 00000000..bc1d20b7 --- /dev/null +++ b/examples/svelte-chat/CHANGELOG.md @@ -0,0 +1,9 @@ +# svelte-chat + +## 0.0.2 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/svelte@0.12.0 diff --git a/examples/svelte-chat/package.json b/examples/svelte-chat/package.json index 10136891..7c778b31 100644 --- a/examples/svelte-chat/package.json +++ b/examples/svelte-chat/package.json @@ -1,7 +1,7 @@ { "name": "svelte-chat", "private": true, - "version": "0.0.1", + "version": "0.0.2", "type": "module", "scripts": { "dev": "vite dev", diff --git a/examples/svelte/CHANGELOG.md b/examples/svelte/CHANGELOG.md new file mode 100644 index 00000000..ed07c2f1 --- /dev/null +++ b/examples/svelte/CHANGELOG.md @@ -0,0 +1,9 @@ +# example-svelte + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/svelte@0.12.0 diff --git a/examples/svelte/package.json b/examples/svelte/package.json index 21eceb71..737bfeb8 100644 --- a/examples/svelte/package.json +++ b/examples/svelte/package.json @@ -1,6 +1,6 @@ { "name": "example-svelte", - "version": "0.1.1", + "version": "0.1.2", "private": true, "type": "module", "scripts": { diff --git a/examples/vite-renderers/CHANGELOG.md b/examples/vite-renderers/CHANGELOG.md index c75ae3f0..777eae3d 100644 --- a/examples/vite-renderers/CHANGELOG.md +++ b/examples/vite-renderers/CHANGELOG.md @@ -1,5 +1,15 @@ # vite-renderers +## 0.1.3 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/svelte@0.12.0 + - @json-render/react@0.12.0 + - @json-render/vue@0.12.0 + ## 0.1.2 ### Patch Changes diff --git a/examples/vite-renderers/package.json b/examples/vite-renderers/package.json index cc47867b..6dff0863 100644 --- a/examples/vite-renderers/package.json +++ b/examples/vite-renderers/package.json @@ -1,6 +1,6 @@ { "name": "vite-renderers", - "version": "0.1.2", + "version": "0.1.3", "private": true, "type": "module", "scripts": { diff --git a/examples/vue/CHANGELOG.md b/examples/vue/CHANGELOG.md index 48b82701..176b5e16 100644 --- a/examples/vue/CHANGELOG.md +++ b/examples/vue/CHANGELOG.md @@ -1,5 +1,13 @@ # example-vue +## 0.1.3 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/vue@0.12.0 + ## 0.1.2 ### Patch Changes diff --git a/examples/vue/package.json b/examples/vue/package.json index 6fcb8cdf..92bf96d5 100644 --- a/examples/vue/package.json +++ b/examples/vue/package.json @@ -1,6 +1,6 @@ { "name": "example-vue", - "version": "0.1.2", + "version": "0.1.3", "private": true, "scripts": { "predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)", diff --git a/packages/codegen/CHANGELOG.md b/packages/codegen/CHANGELOG.md index d45ebf9f..1e9321a2 100644 --- a/packages/codegen/CHANGELOG.md +++ b/packages/codegen/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/codegen +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/codegen/package.json b/packages/codegen/package.json index 20172772..4b1fa4ed 100644 --- a/packages/codegen/package.json +++ b/packages/codegen/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/codegen", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "Utilities for generating code from json-render UI trees", "keywords": [ diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index d5815a58..1b665bb3 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,19 @@ # @json-render/core +## 0.12.0 + +### Minor Changes + +- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration. + + ### New: + - **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers. + - **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support. + - **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility. + + ### Fixed: + - **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency. + ## 0.11.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index bd3e5d7a..75657e15 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/core", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "JSON becomes real things. Define your catalog, register your components, let AI generate.", "keywords": [ diff --git a/packages/image/CHANGELOG.md b/packages/image/CHANGELOG.md index d311525b..4626184c 100644 --- a/packages/image/CHANGELOG.md +++ b/packages/image/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/image +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Minor Changes diff --git a/packages/image/package.json b/packages/image/package.json index 2cfff18c..fd715d04 100644 --- a/packages/image/package.json +++ b/packages/image/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/image", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.", "keywords": [ diff --git a/packages/jotai/CHANGELOG.md b/packages/jotai/CHANGELOG.md index 32b95508..6e65f6b1 100644 --- a/packages/jotai/CHANGELOG.md +++ b/packages/jotai/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/jotai +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/jotai/package.json b/packages/jotai/package.json index 78413382..c39952ea 100644 --- a/packages/jotai/package.json +++ b/packages/jotai/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/jotai", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "Jotai adapter for json-render StateStore", "keywords": [ diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md new file mode 100644 index 00000000..89fad359 --- /dev/null +++ b/packages/mcp/CHANGELOG.md @@ -0,0 +1,20 @@ +# @json-render/mcp + +## 0.12.0 + +### Minor Changes + +- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration. + + ### New: + - **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers. + - **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support. + - **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility. + + ### Fixed: + - **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency. + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 07909a83..2ff9eb89 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/mcp", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.", "keywords": [ diff --git a/packages/react-email/CHANGELOG.md b/packages/react-email/CHANGELOG.md new file mode 100644 index 00000000..bcb0a487 --- /dev/null +++ b/packages/react-email/CHANGELOG.md @@ -0,0 +1,20 @@ +# @json-render/react-email + +## 0.12.0 + +### Minor Changes + +- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration. + + ### New: + - **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers. + - **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support. + - **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility. + + ### Fixed: + - **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency. + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 diff --git a/packages/react-email/package.json b/packages/react-email/package.json index 5bf0a310..08862f18 100644 --- a/packages/react-email/package.json +++ b/packages/react-email/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/react-email", - "version": "0.8.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "React Email renderer for @json-render/core. JSON becomes HTML emails.", "keywords": [ diff --git a/packages/react-native/CHANGELOG.md b/packages/react-native/CHANGELOG.md index 3eded9ea..87ec5735 100644 --- a/packages/react-native/CHANGELOG.md +++ b/packages/react-native/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/react-native +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/react-native/package.json b/packages/react-native/package.json index fe13ecbb..15cbf966 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/react-native", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "React Native renderer for @json-render/core. JSON becomes React Native components.", "keywords": [ diff --git a/packages/react-pdf/CHANGELOG.md b/packages/react-pdf/CHANGELOG.md index 8a56e9cb..c73f291c 100644 --- a/packages/react-pdf/CHANGELOG.md +++ b/packages/react-pdf/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/react-pdf +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/react-pdf/package.json b/packages/react-pdf/package.json index 0f5fdc0c..8bf9da11 100644 --- a/packages/react-pdf/package.json +++ b/packages/react-pdf/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/react-pdf", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.", "keywords": [ diff --git a/packages/react-state/CHANGELOG.md b/packages/react-state/CHANGELOG.md index 840051bf..885273e3 100644 --- a/packages/react-state/CHANGELOG.md +++ b/packages/react-state/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/react-state +## 0.8.5 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.8.4 ### Patch Changes diff --git a/packages/react-state/package.json b/packages/react-state/package.json index 94c57de9..8dda010c 100644 --- a/packages/react-state/package.json +++ b/packages/react-state/package.json @@ -1,6 +1,6 @@ { "name": "@internal/react-state", - "version": "0.8.4", + "version": "0.8.5", "private": true, "license": "Apache-2.0", "description": "Shared React state context for json-render renderer packages", diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 21378bb3..b2faeb65 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/react +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/react/package.json b/packages/react/package.json index 6e8acf2a..637e9697 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/react", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "React renderer for @json-render/core. JSON becomes React components.", "keywords": [ diff --git a/packages/redux/CHANGELOG.md b/packages/redux/CHANGELOG.md index 2199b22f..177e25f0 100644 --- a/packages/redux/CHANGELOG.md +++ b/packages/redux/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/redux +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/redux/package.json b/packages/redux/package.json index 0870c887..3f440bd7 100644 --- a/packages/redux/package.json +++ b/packages/redux/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/redux", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "Redux adapter for json-render StateStore", "keywords": [ diff --git a/packages/remotion/CHANGELOG.md b/packages/remotion/CHANGELOG.md index 2db7252c..08856e7a 100644 --- a/packages/remotion/CHANGELOG.md +++ b/packages/remotion/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/remotion +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/remotion/package.json b/packages/remotion/package.json index 682f36f7..2e1aad23 100644 --- a/packages/remotion/package.json +++ b/packages/remotion/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/remotion", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "Remotion renderer for @json-render/core. JSON becomes video compositions.", "keywords": [ diff --git a/packages/shadcn/CHANGELOG.md b/packages/shadcn/CHANGELOG.md index a2675a76..f916ffb0 100644 --- a/packages/shadcn/CHANGELOG.md +++ b/packages/shadcn/CHANGELOG.md @@ -1,5 +1,13 @@ # @json-render/shadcn +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + - @json-render/react@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json index d83fa093..6ab33a69 100644 --- a/packages/shadcn/package.json +++ b/packages/shadcn/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/shadcn", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.", "keywords": [ diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md new file mode 100644 index 00000000..b47fff4b --- /dev/null +++ b/packages/svelte/CHANGELOG.md @@ -0,0 +1,20 @@ +# @json-render/svelte + +## 0.12.0 + +### Minor Changes + +- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration. + + ### New: + - **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers. + - **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support. + - **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility. + + ### Fixed: + - **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency. + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e92b9477..6c840a88 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/svelte", - "version": "0.6.1", + "version": "0.12.0", "license": "Apache-2.0", "description": "Svelte 5 renderer for @json-render/core. JSON becomes Svelte components.", "keywords": [ diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index 260dc34b..6d25a2ac 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/vue +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/vue/package.json b/packages/vue/package.json index f6cf5e88..da9436bc 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/vue", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "Vue renderer for @json-render/core. JSON becomes Vue components.", "keywords": [ diff --git a/packages/xstate/CHANGELOG.md b/packages/xstate/CHANGELOG.md index 59c709a6..705c4df4 100644 --- a/packages/xstate/CHANGELOG.md +++ b/packages/xstate/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/xstate +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/xstate/package.json b/packages/xstate/package.json index 05bbe35c..6b8d72e8 100644 --- a/packages/xstate/package.json +++ b/packages/xstate/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/xstate", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "XState Store adapter for json-render StateStore", "keywords": [ diff --git a/packages/zustand/CHANGELOG.md b/packages/zustand/CHANGELOG.md index 05961ee6..5db3217b 100644 --- a/packages/zustand/CHANGELOG.md +++ b/packages/zustand/CHANGELOG.md @@ -1,5 +1,12 @@ # @json-render/zustand +## 0.12.0 + +### Patch Changes + +- Updated dependencies [63c339b] + - @json-render/core@0.12.0 + ## 0.11.0 ### Patch Changes diff --git a/packages/zustand/package.json b/packages/zustand/package.json index b21bf440..9a258c14 100644 --- a/packages/zustand/package.json +++ b/packages/zustand/package.json @@ -1,6 +1,6 @@ { "name": "@json-render/zustand", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "description": "Zustand adapter for json-render StateStore", "keywords": [ From b6e0b77f7710e8979a4b1ed84f1a384b099b903e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 6 Mar 2026 20:22:56 -0600 Subject: [PATCH 8/9] cleaner skills (#187) --- AGENTS.md | 2 +- skills/{json-render-codegen => codegen}/SKILL.md | 2 +- skills/{json-render-core => core}/SKILL.md | 2 +- skills/{json-render-image => image}/SKILL.md | 2 +- skills/{json-render-jotai => jotai}/SKILL.md | 2 +- skills/{json-render-mcp => mcp}/SKILL.md | 2 +- skills/{json-render-react-email => react-email}/SKILL.md | 2 +- skills/{json-render-react-native => react-native}/SKILL.md | 2 +- skills/{json-render-react-pdf => react-pdf}/SKILL.md | 2 +- skills/{json-render-react => react}/SKILL.md | 2 +- skills/{json-render-redux => redux}/SKILL.md | 2 +- skills/{json-render-remotion => remotion}/SKILL.md | 2 +- skills/{json-render-shadcn => shadcn}/SKILL.md | 2 +- skills/{json-render-svelte => svelte}/SKILL.md | 2 +- skills/{json-render-vue => vue}/SKILL.md | 2 +- skills/{json-render-xstate => xstate}/SKILL.md | 2 +- skills/{json-render-zustand => zustand}/SKILL.md | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) rename skills/{json-render-codegen => codegen}/SKILL.md (98%) rename skills/{json-render-core => core}/SKILL.md (99%) rename skills/{json-render-image => image}/SKILL.md (99%) rename skills/{json-render-jotai => jotai}/SKILL.md (98%) rename skills/{json-render-mcp => mcp}/SKILL.md (99%) rename skills/{json-render-react-email => react-email}/SKILL.md (99%) rename skills/{json-render-react-native => react-native}/SKILL.md (99%) rename skills/{json-render-react-pdf => react-pdf}/SKILL.md (99%) rename skills/{json-render-react => react}/SKILL.md (99%) rename skills/{json-render-redux => redux}/SKILL.md (98%) rename skills/{json-render-remotion => remotion}/SKILL.md (99%) rename skills/{json-render-shadcn => shadcn}/SKILL.md (99%) rename skills/{json-render-svelte => svelte}/SKILL.md (99%) rename skills/{json-render-vue => vue}/SKILL.md (99%) rename skills/{json-render-xstate => xstate}/SKILL.md (98%) rename skills/{json-render-zustand => zustand}/SKILL.md (98%) diff --git a/AGENTS.md b/AGENTS.md index 7b60bcbc..f82ab9e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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//page.mdx` - 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`) - - A skill at `skills/json-render-/SKILL.md` + - A skill at `skills//SKILL.md` - A `packages//README.md` 6. **Run `pnpm type-check`** after all changes to verify nothing is broken diff --git a/skills/json-render-codegen/SKILL.md b/skills/codegen/SKILL.md similarity index 98% rename from skills/json-render-codegen/SKILL.md rename to skills/codegen/SKILL.md index b0fd5ec3..c4ef0c96 100644 --- a/skills/json-render-codegen/SKILL.md +++ b/skills/codegen/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-codegen +name: codegen description: Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing specs, or serializing props for @json-render/codegen. --- diff --git a/skills/json-render-core/SKILL.md b/skills/core/SKILL.md similarity index 99% rename from skills/json-render-core/SKILL.md rename to skills/core/SKILL.md index 9cbd7624..d283817d 100644 --- a/skills/json-render-core/SKILL.md +++ b/skills/core/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-core +name: core description: Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation. --- diff --git a/skills/json-render-image/SKILL.md b/skills/image/SKILL.md similarity index 99% rename from skills/json-render-image/SKILL.md rename to skills/image/SKILL.md index 500f4d6b..4bcfec89 100644 --- a/skills/json-render-image/SKILL.md +++ b/skills/image/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-image +name: image description: Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs. --- diff --git a/skills/json-render-jotai/SKILL.md b/skills/jotai/SKILL.md similarity index 98% rename from skills/json-render-jotai/SKILL.md rename to skills/jotai/SKILL.md index a1c24196..7761b72a 100644 --- a/skills/json-render-jotai/SKILL.md +++ b/skills/jotai/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-jotai +name: jotai description: Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai. --- diff --git a/skills/json-render-mcp/SKILL.md b/skills/mcp/SKILL.md similarity index 99% rename from skills/json-render-mcp/SKILL.md rename to skills/mcp/SKILL.md index 680eae9c..7e795889 100644 --- a/skills/json-render-mcp/SKILL.md +++ b/skills/mcp/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-mcp +name: mcp description: MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol. --- diff --git a/skills/json-render-react-email/SKILL.md b/skills/react-email/SKILL.md similarity index 99% rename from skills/json-render-react-email/SKILL.md rename to skills/react-email/SKILL.md index 91e7773a..0fc69541 100644 --- a/skills/json-render-react-email/SKILL.md +++ b/skills/react-email/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-react-email +name: react-email description: React Email renderer for json-render that turns JSON specs into HTML or plain-text emails using @react-email/components and @react-email/render. Use when working with @json-render/react-email, building transactional or marketing emails from JSON, creating email catalogs, rendering AI-generated email specs, or when the user mentions react-email, HTML email, or transactional email. metadata: tags: react-email, email, json-render, html email, transactional email diff --git a/skills/json-render-react-native/SKILL.md b/skills/react-native/SKILL.md similarity index 99% rename from skills/json-render-react-native/SKILL.md rename to skills/react-native/SKILL.md index d60621e5..f43134b8 100644 --- a/skills/json-render-react-native/SKILL.md +++ b/skills/react-native/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-react-native +name: react-native description: React Native renderer for json-render that turns JSON specs into native mobile UIs. Use when working with @json-render/react-native, building React Native UIs from JSON, creating mobile component catalogs, or rendering AI-generated specs on mobile. --- diff --git a/skills/json-render-react-pdf/SKILL.md b/skills/react-pdf/SKILL.md similarity index 99% rename from skills/json-render-react-pdf/SKILL.md rename to skills/react-pdf/SKILL.md index 0fed196b..4aea9c45 100644 --- a/skills/json-render-react-pdf/SKILL.md +++ b/skills/react-pdf/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-react-pdf +name: react-pdf description: React PDF renderer for json-render. Use when generating PDF documents from JSON specs, working with @json-render/react-pdf, or rendering specs to PDF buffers/streams/files. --- diff --git a/skills/json-render-react/SKILL.md b/skills/react/SKILL.md similarity index 99% rename from skills/json-render-react/SKILL.md rename to skills/react/SKILL.md index 0f8242b6..1e4de762 100644 --- a/skills/json-render-react/SKILL.md +++ b/skills/react/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-react +name: react description: React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs. --- diff --git a/skills/json-render-redux/SKILL.md b/skills/redux/SKILL.md similarity index 98% rename from skills/json-render-redux/SKILL.md rename to skills/redux/SKILL.md index 7adcae14..b050d072 100644 --- a/skills/json-render-redux/SKILL.md +++ b/skills/redux/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-redux +name: redux description: Redux adapter for json-render's StateStore interface. Use when integrating json-render with Redux or Redux Toolkit for state management via @json-render/redux. --- diff --git a/skills/json-render-remotion/SKILL.md b/skills/remotion/SKILL.md similarity index 99% rename from skills/json-render-remotion/SKILL.md rename to skills/remotion/SKILL.md index 6bf8eebc..a521f5df 100644 --- a/skills/json-render-remotion/SKILL.md +++ b/skills/remotion/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-remotion +name: remotion description: Remotion renderer for json-render that turns JSON timeline specs into videos. Use when working with @json-render/remotion, building video compositions from JSON, creating video catalogs, or rendering AI-generated video timelines. --- diff --git a/skills/json-render-shadcn/SKILL.md b/skills/shadcn/SKILL.md similarity index 99% rename from skills/json-render-shadcn/SKILL.md rename to skills/shadcn/SKILL.md index e0ce21db..b26c6b82 100644 --- a/skills/json-render-shadcn/SKILL.md +++ b/skills/shadcn/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-shadcn +name: shadcn description: Pre-built shadcn/ui components for json-render. Use when working with @json-render/shadcn, adding standard UI components to a catalog, or building web UIs with Radix UI + Tailwind CSS components. --- diff --git a/skills/json-render-svelte/SKILL.md b/skills/svelte/SKILL.md similarity index 99% rename from skills/json-render-svelte/SKILL.md rename to skills/svelte/SKILL.md index a95ab874..ae76fd63 100644 --- a/skills/json-render-svelte/SKILL.md +++ b/skills/svelte/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-svelte +name: svelte description: Svelte 5 renderer for json-render that turns JSON specs into Svelte component trees. Use when working with @json-render/svelte, building Svelte UIs from JSON, creating component catalogs, or rendering AI-generated specs. --- diff --git a/skills/json-render-vue/SKILL.md b/skills/vue/SKILL.md similarity index 99% rename from skills/json-render-vue/SKILL.md rename to skills/vue/SKILL.md index 30e9df7d..019d03e3 100644 --- a/skills/json-render-vue/SKILL.md +++ b/skills/vue/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-vue +name: vue description: Vue 3 renderer for json-render. Use when building Vue UIs from JSON specs, working with @json-render/vue, defining Vue component registries, or rendering AI-generated specs in Vue. --- diff --git a/skills/json-render-xstate/SKILL.md b/skills/xstate/SKILL.md similarity index 98% rename from skills/json-render-xstate/SKILL.md rename to skills/xstate/SKILL.md index 2ea75f71..a1433c9d 100644 --- a/skills/json-render-xstate/SKILL.md +++ b/skills/xstate/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-xstate +name: xstate description: XState Store adapter for json-render's StateStore interface. Use when integrating json-render with @xstate/store for state management via @json-render/xstate. --- diff --git a/skills/json-render-zustand/SKILL.md b/skills/zustand/SKILL.md similarity index 98% rename from skills/json-render-zustand/SKILL.md rename to skills/zustand/SKILL.md index f97633cc..f2cf51b8 100644 --- a/skills/json-render-zustand/SKILL.md +++ b/skills/zustand/SKILL.md @@ -1,5 +1,5 @@ --- -name: json-render-zustand +name: zustand description: Zustand adapter for json-render's StateStore interface. Use when integrating json-render with Zustand for state management via @json-render/zustand. --- From 737531cb77d9a9eea0b90ad4638be97011f953c1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 6 Mar 2026 20:39:07 -0600 Subject: [PATCH 9/9] skills page (#188) --- apps/web/app/(main)/docs/skills/page.mdx | 117 +++++++++++++++++++++++ apps/web/app/api/docs-chat/route.ts | 1 + apps/web/lib/docs-navigation.ts | 1 + apps/web/lib/page-titles.ts | 1 + 4 files changed, 120 insertions(+) create mode 100644 apps/web/app/(main)/docs/skills/page.mdx diff --git a/apps/web/app/(main)/docs/skills/page.mdx b/apps/web/app/(main)/docs/skills/page.mdx new file mode 100644 index 00000000..6635d71f --- /dev/null +++ b/apps/web/app/(main)/docs/skills/page.mdx @@ -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. diff --git a/apps/web/app/api/docs-chat/route.ts b/apps/web/app/api/docs-chat/route.ts index 5c6b968f..2e74cd8b 100644 --- a/apps/web/app/api/docs-chat/route.ts +++ b/apps/web/app/api/docs-chat/route.ts @@ -17,6 +17,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render GitHub repository: https://github.com/vercel-labs/json-render Documentation: https://json-render.dev/docs npm packages: @json-render/core, @json-render/react, @json-render/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 ". 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. diff --git a/apps/web/lib/docs-navigation.ts b/apps/web/lib/docs-navigation.ts index ec93e6ee..c5d77395 100644 --- a/apps/web/lib/docs-navigation.ts +++ b/apps/web/lib/docs-navigation.ts @@ -16,6 +16,7 @@ export const docsNavigation: NavSection[] = [ { title: "Introduction", href: "/docs" }, { title: "Installation", href: "/docs/installation" }, { title: "Quick Start", href: "/docs/quick-start" }, + { title: "Skills", href: "/docs/skills" }, { title: "Migration Guide", href: "/docs/migration" }, { title: "Changelog", href: "/docs/changelog" }, ], diff --git a/apps/web/lib/page-titles.ts b/apps/web/lib/page-titles.ts index 2e92cdf1..f8d27dd5 100644 --- a/apps/web/lib/page-titles.ts +++ b/apps/web/lib/page-titles.ts @@ -37,6 +37,7 @@ export const PAGE_TITLES: Record = { "docs/ag-ui": "AG-UI Integration", "docs/migration": "Migration Guide", "docs/changelog": "Changelog", + "docs/skills": "Skills", // API references "docs/api/core": "@json-render/core API",