diff --git a/.changeset/shy-donuts-add.md b/.changeset/shy-donuts-add.md new file mode 100644 index 00000000..ecae7191 --- /dev/null +++ b/.changeset/shy-donuts-add.md @@ -0,0 +1,5 @@ +--- +"@json-render/shadcn-svelte": minor +--- + +Add a new `@json-render/shadcn-svelte` package with 36 pre-built shadcn-svelte components, catalog definitions, and Svelte renderer adapters for json-render. 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 b0a37562..d693dc4c 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 Svelte with pre-built shadcn-svelte components +npm install @json-render/shadcn-svelte # or for React Native npm install @json-render/core @json-render/react-native # or for video @@ -113,8 +115,10 @@ function Dashboard({ spec }) { |---------|-------------| | `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities | | `@json-render/react` | React renderer, contexts, hooks | +| `@json-render/svelte` | Svelte 5 renderer, providers, and helpers | | `@json-render/vue` | Vue 3 renderer, composables, providers | | `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) | +| `@json-render/shadcn-svelte` | 36 pre-built shadcn-svelte components (Svelte 5 + Tailwind CSS) | | `@json-render/react-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 | @@ -394,8 +398,9 @@ pnpm dev - http://dashboard-demo.json-render.localhost:1355 - Example Dashboard - http://remotion-demo.json-render.localhost:1355 - Remotion Video Example - Chat Example: run `pnpm dev` in `examples/chat` +- 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/shadcn-svelte/page.mdx b/apps/web/app/(main)/docs/api/shadcn-svelte/page.mdx new file mode 100644 index 00000000..9d186ec7 --- /dev/null +++ b/apps/web/app/(main)/docs/api/shadcn-svelte/page.mdx @@ -0,0 +1,59 @@ +import { pageMetadata } from "@/lib/page-metadata" +export const metadata = pageMetadata("docs/api/shadcn-svelte") + +# @json-render/shadcn-svelte + +Pre-built [shadcn-svelte](https://www.shadcn-svelte.com/) components for json-render. 36 components built on Svelte 5 + Tailwind CSS, ready to use with `schema.createCatalog` and `defineRegistry`. + +## Installation + +```bash +npm install @json-render/shadcn-svelte @json-render/core @json-render/svelte zod +``` + +Your app must have Tailwind CSS configured. + +## Entry Points + +| Entry Point | Exports | Use For | +|-------------|---------|---------| +| `@json-render/shadcn-svelte` | `shadcnComponents`, `shadcnComponentDefinitions` | Svelte implementations + shared definitions | +| `@json-render/shadcn-svelte/catalog` | `shadcnComponentDefinitions` | Catalog schemas (server-safe, no renderer dependency) | + +## Usage + +Pick the components you need from the standard definitions: + +```typescript +import { schema } from "@json-render/svelte/schema"; +import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog"; +import { defineRegistry } from "@json-render/svelte"; +import { shadcnComponents } from "@json-render/shadcn-svelte"; + +const catalog = schema.createCatalog({ + components: { + Card: shadcnComponentDefinitions.Card, + Stack: shadcnComponentDefinitions.Stack, + Heading: shadcnComponentDefinitions.Heading, + Button: shadcnComponentDefinitions.Button, + Input: shadcnComponentDefinitions.Input, + }, + actions: {}, +}); + +const { registry } = defineRegistry(catalog, { + components: { + Card: shadcnComponents.Card, + Stack: shadcnComponents.Stack, + Heading: shadcnComponents.Heading, + Button: shadcnComponents.Button, + Input: shadcnComponents.Input, + }, +}); +``` + +## Notes + +- Built-in state actions (`setState`, `pushState`, `removeState`, `validateForm`) come from `@json-render/svelte` +- Components rely on Tailwind utility classes and design tokens +- Form components support `checks` and `validateOn` validation timing 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 5106a4f7..50a1595b 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 e5328c1b..47c489b2 100644 --- a/apps/web/lib/docs-navigation.ts +++ b/apps/web/lib/docs-navigation.ts @@ -69,13 +69,18 @@ export const docsNavigation: NavSection[] = [ href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion", 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, }, @@ -105,9 +110,11 @@ export const docsNavigation: NavSection[] = [ { title: "@json-render/react", href: "/docs/api/react" }, { title: "@json-render/react-pdf", href: "/docs/api/react-pdf" }, { title: "@json-render/shadcn", href: "/docs/api/shadcn" }, + { title: "@json-render/shadcn-svelte", href: "/docs/api/shadcn-svelte" }, { title: "@json-render/react-native", href: "/docs/api/react-native" }, { 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 10835668..4e20d989 100644 --- a/apps/web/lib/page-titles.ts +++ b/apps/web/lib/page-titles.ts @@ -43,9 +43,11 @@ export const PAGE_TITLES: Record = { "docs/api/vue": "@json-render/vue API", "docs/api/react-pdf": "@json-render/react-pdf 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/remotion": "@json-render/remotion API", "docs/api/shadcn": "@json-render/shadcn API", + "docs/api/shadcn-svelte": "@json-render/shadcn-svelte API", }; /** 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..f6a2ba95 --- /dev/null +++ b/examples/svelte-chat/package.json @@ -0,0 +1,40 @@ +{ + "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/shadcn-svelte": "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.5" + } +} 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 @@ + + + + {@render children?.()} + 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 @@ + + + + {@render children?.()} + 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 @@ + + + + {@render children?.()} + 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?.()} +
+
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..87a4d0a5 --- /dev/null +++ b/examples/svelte-chat/src/lib/render/registry.ts @@ -0,0 +1,50 @@ +import { defineRegistry, type ComponentRegistry } from "@json-render/svelte"; +import { shadcnComponents } from "@json-render/shadcn-svelte"; +import { explorerCatalog } from "./catalog"; + +import TextComponent from "./components/Text.svelte"; +import AlertComponent from "./components/Alert.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 TabContentComponent from "./components/TabContent.svelte"; +import CalloutComponent from "./components/Callout.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"; + +const components: ComponentRegistry = { + Stack: shadcnComponents.Stack, + Card: shadcnComponents.Card, + Grid: shadcnComponents.Grid, + Heading: shadcnComponents.Heading, + Text: TextComponent, + Badge: shadcnComponents.Badge, + Alert: AlertComponent, + Separator: shadcnComponents.Separator, + Metric: MetricComponent, + Table: TableComponent, + Link: LinkComponent, + BarChart: BarChartComponent, + LineChart: LineChartComponent, + Tabs: shadcnComponents.Tabs, + TabContent: TabContentComponent, + Progress: shadcnComponents.Progress, + Skeleton: shadcnComponents.Skeleton, + Callout: CalloutComponent, + Accordion: shadcnComponents.Accordion, + Timeline: TimelineComponent, + PieChart: PieChartComponent, + RadioGroup: RadioGroupComponent, + SelectInput: SelectInputComponent, + TextInput: TextInputComponent, + Button: shadcnComponents.Button, +}; + +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..f8b64872 --- /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.5" + }, + "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..80127420 --- /dev/null +++ b/examples/svelte/src/lib/registry.ts @@ -0,0 +1,22 @@ +import { defineRegistry, type ComponentRegistry } 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: ComponentRegistry = { + 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 d88c2997..89b92a6a 100644 --- a/examples/vite-renderers/package.json +++ b/examples/vite-renderers/package.json @@ -2,6 +2,7 @@ "name": "vite-renderers", "version": "0.1.1", "private": true, + "type": "module", "scripts": { "dev": "portless vite-renderers.json-render vite", "build": "vite build", @@ -10,13 +11,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.5" }, "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 aa864378..0fb2035f 100644 --- a/examples/vue/package.json +++ b/examples/vue/package.json @@ -11,7 +11,7 @@ "@json-render/core": "workspace:*", "@json-render/vue": "workspace:*", "vue": "^3.5.0", - "zod": "^4.3.6" + "zod": "4.3.5" }, "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 d71255ea..615840ca 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,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", @@ -37,6 +39,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/shadcn-svelte/README.md b/packages/shadcn-svelte/README.md new file mode 100644 index 00000000..705db697 --- /dev/null +++ b/packages/shadcn-svelte/README.md @@ -0,0 +1,70 @@ +# @json-render/shadcn-svelte + +Pre-built [shadcn-svelte](https://www.shadcn-svelte.com/) components for json-render. Drop-in catalog definitions and Svelte implementations for 36 components built on Svelte 5 + Tailwind CSS. + +## Installation + +```bash +npm install @json-render/shadcn-svelte @json-render/core @json-render/svelte zod +``` + +## Quick Start + +### 1. Create a Catalog + +```ts +import { schema } from "@json-render/svelte/schema"; +import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog"; + +const catalog = schema.createCatalog({ + components: { + Card: shadcnComponentDefinitions.Card, + Stack: shadcnComponentDefinitions.Stack, + Heading: shadcnComponentDefinitions.Heading, + Button: shadcnComponentDefinitions.Button, + Input: shadcnComponentDefinitions.Input, + }, + actions: {}, +}); +``` + +### 2. Create a Registry + +```ts +import { defineRegistry } from "@json-render/svelte"; +import { shadcnComponents } from "@json-render/shadcn-svelte"; + +const { registry } = defineRegistry(catalog, { + components: { + Card: shadcnComponents.Card, + Stack: shadcnComponents.Stack, + Heading: shadcnComponents.Heading, + Button: shadcnComponents.Button, + Input: shadcnComponents.Input, + }, +}); +``` + +### 3. Render + +```svelte + + + + + +``` + +## Exports + +| Entry Point | Exports | +|-------------|---------| +| `@json-render/shadcn-svelte` | `shadcnComponents`, `shadcnComponentDefinitions` | +| `@json-render/shadcn-svelte/catalog` | `shadcnComponentDefinitions` | + +The `/catalog` entrypoint contains only Zod schemas (no renderer dependency), so it can be used in server-side code for prompt generation. diff --git a/packages/shadcn-svelte/package.json b/packages/shadcn-svelte/package.json new file mode 100644 index 00000000..9a34360f --- /dev/null +++ b/packages/shadcn-svelte/package.json @@ -0,0 +1,82 @@ +{ + "name": "@json-render/shadcn-svelte", + "version": "0.1.0", + "license": "Apache-2.0", + "description": "shadcn-svelte component library for @json-render/svelte. JSON becomes beautiful Tailwind-styled Svelte components.", + "keywords": [ + "json", + "ui", + "svelte", + "svelte5", + "shadcn-svelte", + "tailwind", + "bits-ui", + "ai", + "generative-ui", + "llm", + "renderer", + "streaming", + "components" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/json-render.git", + "directory": "packages/shadcn-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" + }, + "./catalog": { + "types": "./dist/catalog.d.ts", + "svelte": "./dist/catalog.js", + "default": "./dist/catalog.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "svelte-package -i src -o dist", + "dev": "svelte-package -i src -o dist --watch", + "check-types": "svelte-check --tsconfig ./tsconfig.json", + "typecheck": "svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@json-render/core": "workspace:*", + "@json-render/svelte": "workspace:*", + "@lucide/svelte": "^0.561.0", + "bits-ui": "^2.16.2", + "clsx": "^2.1.1", + "tailwind-merge": "^3.4.1", + "tailwind-variants": "^3.2.2", + "zod": "^4.0.0" + }, + "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", + "tailwindcss": "^4.0.0", + "zod": "^4.0.0" + } +} diff --git a/packages/shadcn-svelte/src/catalog.ts b/packages/shadcn-svelte/src/catalog.ts new file mode 100644 index 00000000..51196083 --- /dev/null +++ b/packages/shadcn-svelte/src/catalog.ts @@ -0,0 +1,506 @@ +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(); + +// ============================================================================= +// shadcn/ui Component Definitions +// ============================================================================= + +/** + * shadcn/ui component definitions for json-render catalogs. + * + * These can be used directly or extended with custom components. + * All components are built using Radix UI primitives + Tailwind CSS. + */ +export const shadcnComponentDefinitions = { + // ========================================================================== + // Layout Components + // ========================================================================== + + Card: { + props: z.object({ + title: z.string().nullable(), + description: z.string().nullable(), + maxWidth: z.enum(["sm", "md", "lg", "full"]).nullable(), + centered: z.boolean().nullable(), + }), + slots: ["default"], + description: + "Container card for content sections. Use for forms/content boxes, NOT for page headers.", + 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(), + }), + 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" }, + }, + + Separator: { + props: z.object({ + orientation: z.enum(["horizontal", "vertical"]).nullable(), + }), + description: "Visual separator line", + }, + + Tabs: { + props: z.object({ + tabs: z.array( + z.object({ + label: z.string(), + value: z.string(), + }), + ), + defaultValue: z.string().nullable(), + value: z.string().nullable(), + }), + slots: ["default"], + events: ["change"], + description: + "Tab navigation. Use { $bindState } on value for active tab binding.", + }, + + Accordion: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + content: z.string(), + }), + ), + type: z.enum(["single", "multiple"]).nullable(), + }), + description: + "Collapsible sections. Items as [{title, content}]. Type 'single' (default) or 'multiple'.", + }, + + Collapsible: { + props: z.object({ + title: z.string(), + defaultOpen: z.boolean().nullable(), + }), + slots: ["default"], + description: "Collapsible section with trigger. Children render inside.", + }, + + Dialog: { + props: z.object({ + title: z.string(), + description: z.string().nullable(), + openPath: z.string(), + }), + slots: ["default"], + 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(), + }), + slots: ["default"], + description: + "Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.", + }, + + Carousel: { + props: z.object({ + items: z.array( + z.object({ + title: z.string().nullable(), + description: z.string().nullable(), + }), + ), + }), + description: "Horizontally scrollable carousel of cards.", + }, + + // ========================================================================== + // Data Display Components + // ========================================================================== + + Table: { + props: z.object({ + columns: z.array(z.string()), + rows: z.array(z.array(z.string())), + caption: z.string().nullable(), + }), + description: + 'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [["Alice","admin"],["Bob","user"]].', + example: { + columns: ["Name", "Role"], + rows: [ + ["Alice", "Admin"], + ["Bob", "User"], + ], + }, + }, + + Heading: { + props: z.object({ + text: z.string(), + level: z.enum(["h1", "h2", "h3", "h4"]).nullable(), + }), + description: "Heading text (h1-h4)", + example: { text: "Welcome", level: "h1" }, + }, + + Text: { + props: z.object({ + text: z.string(), + variant: z.enum(["body", "caption", "muted", "lead", "code"]).nullable(), + }), + description: "Paragraph text", + example: { text: "Hello, world!" }, + }, + + Image: { + props: z.object({ + src: z.string().nullable(), + alt: z.string(), + width: z.number().nullable(), + height: z.number().nullable(), + }), + description: + "Image component. Renders an img tag when src is provided, otherwise a placeholder.", + }, + + Avatar: { + props: z.object({ + src: z.string().nullable(), + name: z.string(), + size: z.enum(["sm", "md", "lg"]).nullable(), + }), + description: "User avatar with fallback initials", + example: { name: "Jane Doe", size: "md" }, + }, + + Badge: { + props: z.object({ + text: z.string(), + variant: z + .enum(["default", "secondary", "destructive", "outline"]) + .nullable(), + }), + description: "Status badge", + example: { text: "Active", variant: "default" }, + }, + + Alert: { + props: z.object({ + title: z.string(), + message: z.string().nullable(), + type: z.enum(["info", "success", "warning", "error"]).nullable(), + }), + 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(), + }), + description: "Progress bar (value 0-100)", + example: { value: 65, max: 100, label: "Upload progress" }, + }, + + Skeleton: { + props: z.object({ + width: z.string().nullable(), + height: z.string().nullable(), + rounded: z.boolean().nullable(), + }), + description: "Loading placeholder skeleton", + }, + + Spinner: { + props: z.object({ + size: z.enum(["sm", "md", "lg"]).nullable(), + label: z.string().nullable(), + }), + description: "Loading spinner indicator", + }, + + Tooltip: { + props: z.object({ + content: z.string(), + text: z.string(), + }), + description: "Hover tooltip. Shows content on hover over text.", + }, + + Popover: { + props: z.object({ + trigger: z.string(), + content: z.string(), + }), + description: "Popover that appears on click of trigger.", + }, + + // ========================================================================== + // 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(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["submit", "focus", "blur"], + description: + "Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength). validateOn controls timing (default: blur).", + 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(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + description: + "Multi-line text input. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: blur).", + }, + + Select: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array(z.string()), + placeholder: z.string().nullable(), + value: z.string().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: + "Dropdown select input. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: change).", + }, + + Checkbox: { + props: z.object({ + label: z.string(), + name: z.string(), + checked: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: + "Checkbox input. Use { $bindState } on checked for binding. Use checks for validation. validateOn controls timing (default: change).", + }, + + Radio: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array(z.string()), + value: z.string().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: + "Radio button group. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: change).", + }, + + Switch: { + props: z.object({ + label: z.string(), + name: z.string(), + checked: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: + "Toggle switch. Use { $bindState } on checked for binding. Use checks for validation. validateOn controls timing (default: change).", + }, + + Slider: { + props: z.object({ + label: z.string().nullable(), + min: z.number().nullable(), + max: z.number().nullable(), + step: z.number().nullable(), + value: z.number().nullable(), + }), + events: ["change"], + description: "Range slider input. Use { $bindState } on value for binding.", + }, + + // ========================================================================== + // Action Components + // ========================================================================== + + Button: { + props: z.object({ + label: z.string(), + variant: z.enum(["primary", "secondary", "danger"]).nullable(), + disabled: z.boolean().nullable(), + }), + events: ["press"], + description: "Clickable button. Bind on.press for handler.", + example: { label: "Submit", variant: "primary" }, + }, + + Link: { + props: z.object({ + label: z.string(), + href: z.string(), + }), + events: ["press"], + description: "Anchor link. Bind on.press for click handler.", + }, + + DropdownMenu: { + props: z.object({ + label: z.string(), + items: z.array( + z.object({ + label: z.string(), + value: z.string(), + }), + ), + value: z.string().nullable(), + }), + events: ["select"], + description: + "Dropdown menu with trigger button and selectable items. Use { $bindState } on value for selected item binding.", + }, + + Toggle: { + props: z.object({ + label: z.string(), + pressed: z.boolean().nullable(), + variant: z.enum(["default", "outline"]).nullable(), + }), + events: ["change"], + description: + "Toggle button. Use { $bindState } on pressed for state binding.", + }, + + ToggleGroup: { + props: z.object({ + items: z.array( + z.object({ + label: z.string(), + value: z.string(), + }), + ), + type: z.enum(["single", "multiple"]).nullable(), + value: z.string().nullable(), + }), + events: ["change"], + description: + "Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.", + }, + + ButtonGroup: { + props: z.object({ + buttons: z.array( + z.object({ + label: z.string(), + value: z.string(), + }), + ), + selected: z.string().nullable(), + }), + events: ["change"], + description: + "Segmented button group. Use { $bindState } on selected for selected value.", + }, + + Pagination: { + props: z.object({ + totalPages: z.number(), + page: z.number().nullable(), + }), + events: ["change"], + description: + "Page navigation. Use { $bindState } on page for current page number.", + }, +}; + +// ============================================================================= +// 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 a shadcn 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 = ShadcnProps<"Card">; + * // { title: string | null; description: string | null; ... } + * ``` + */ +export type ShadcnProps = + z.output<(typeof shadcnComponentDefinitions)[K]["props"]>; diff --git a/packages/shadcn-svelte/src/components.ts b/packages/shadcn-svelte/src/components.ts new file mode 100644 index 00000000..f70f6214 --- /dev/null +++ b/packages/shadcn-svelte/src/components.ts @@ -0,0 +1,75 @@ +import Card from "./render/Card.svelte"; +import Stack from "./render/Stack.svelte"; +import Grid from "./render/Grid.svelte"; +import Separator from "./render/Separator.svelte"; +import Tabs from "./render/Tabs.svelte"; +import Accordion from "./render/Accordion.svelte"; +import Collapsible from "./render/Collapsible.svelte"; +import Dialog from "./render/Dialog.svelte"; +import Drawer from "./render/Drawer.svelte"; +import Carousel from "./render/Carousel.svelte"; +import Table from "./render/Table.svelte"; +import Heading from "./render/Heading.svelte"; +import Text from "./render/Text.svelte"; +import Image from "./render/Image.svelte"; +import Avatar from "./render/Avatar.svelte"; +import Badge from "./render/Badge.svelte"; +import Alert from "./render/Alert.svelte"; +import Progress from "./render/Progress.svelte"; +import Skeleton from "./render/Skeleton.svelte"; +import Spinner from "./render/Spinner.svelte"; +import Tooltip from "./render/Tooltip.svelte"; +import Popover from "./render/Popover.svelte"; +import Input from "./render/Input.svelte"; +import Textarea from "./render/Textarea.svelte"; +import Select from "./render/Select.svelte"; +import Checkbox from "./render/Checkbox.svelte"; +import Radio from "./render/Radio.svelte"; +import Switch from "./render/Switch.svelte"; +import Slider from "./render/Slider.svelte"; +import Button from "./render/Button.svelte"; +import Link from "./render/Link.svelte"; +import DropdownMenu from "./render/DropdownMenu.svelte"; +import Toggle from "./render/Toggle.svelte"; +import ToggleGroup from "./render/ToggleGroup.svelte"; +import ButtonGroup from "./render/ButtonGroup.svelte"; +import Pagination from "./render/Pagination.svelte"; + +export const shadcnComponents = { + Card, + Stack, + Grid, + Separator, + Tabs, + Accordion, + Collapsible, + Dialog, + Drawer, + Carousel, + Table, + Heading, + Text, + Image, + Avatar, + Badge, + Alert, + Progress, + Skeleton, + Spinner, + Tooltip, + Popover, + Input, + Textarea, + Select, + Checkbox, + Radio, + Switch, + Slider, + Button, + Link, + DropdownMenu, + Toggle, + ToggleGroup, + ButtonGroup, + Pagination, +}; diff --git a/packages/shadcn-svelte/src/index.ts b/packages/shadcn-svelte/src/index.ts new file mode 100644 index 00000000..21f636ce --- /dev/null +++ b/packages/shadcn-svelte/src/index.ts @@ -0,0 +1,7 @@ +export { shadcnComponents } from "./components.js"; + +export { + shadcnComponentDefinitions, + type ComponentDefinition, + type ShadcnProps, +} from "./catalog.js"; diff --git a/packages/shadcn-svelte/src/lib/utils.ts b/packages/shadcn-svelte/src/lib/utils.ts new file mode 100644 index 00000000..a091ef10 --- /dev/null +++ b/packages/shadcn-svelte/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/packages/shadcn-svelte/src/render/Accordion.svelte b/packages/shadcn-svelte/src/render/Accordion.svelte new file mode 100644 index 00000000..8990fe4b --- /dev/null +++ b/packages/shadcn-svelte/src/render/Accordion.svelte @@ -0,0 +1,20 @@ + + + + {#each items as item, i} + + {item.title} + {item.content} + + {/each} + diff --git a/packages/shadcn-svelte/src/render/Alert.svelte b/packages/shadcn-svelte/src/render/Alert.svelte new file mode 100644 index 00000000..d3e460dd --- /dev/null +++ b/packages/shadcn-svelte/src/render/Alert.svelte @@ -0,0 +1,18 @@ + + + + {props.title} + {#if props.message} + {props.message} + {/if} + diff --git a/packages/shadcn-svelte/src/render/Avatar.svelte b/packages/shadcn-svelte/src/render/Avatar.svelte new file mode 100644 index 00000000..f96ba11a --- /dev/null +++ b/packages/shadcn-svelte/src/render/Avatar.svelte @@ -0,0 +1,29 @@ + + +
+ {#if props.src} + {props.name} + {:else} + {initials} + {/if} +
diff --git a/packages/shadcn-svelte/src/render/Badge.svelte b/packages/shadcn-svelte/src/render/Badge.svelte new file mode 100644 index 00000000..36ee8644 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Badge.svelte @@ -0,0 +1,11 @@ + + +{props.text} diff --git a/packages/shadcn-svelte/src/render/Button.svelte b/packages/shadcn-svelte/src/render/Button.svelte new file mode 100644 index 00000000..c4e0970f --- /dev/null +++ b/packages/shadcn-svelte/src/render/Button.svelte @@ -0,0 +1,22 @@ + + + diff --git a/packages/shadcn-svelte/src/render/ButtonGroup.svelte b/packages/shadcn-svelte/src/render/ButtonGroup.svelte new file mode 100644 index 00000000..c2d2dd18 --- /dev/null +++ b/packages/shadcn-svelte/src/render/ButtonGroup.svelte @@ -0,0 +1,38 @@ + + +
+ {#each props.buttons ?? [] as btn, i} + + {/each} +
diff --git a/packages/shadcn-svelte/src/render/Card.svelte b/packages/shadcn-svelte/src/render/Card.svelte new file mode 100644 index 00000000..36215dd6 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Card.svelte @@ -0,0 +1,39 @@ + + + + {#if props.title || props.description} + + {#if props.title} + {props.title} + {/if} + {#if props.description} + {props.description} + {/if} + + {/if} + + {@render children?.()} + + diff --git a/packages/shadcn-svelte/src/render/Carousel.svelte b/packages/shadcn-svelte/src/render/Carousel.svelte new file mode 100644 index 00000000..a42d9cb3 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Carousel.svelte @@ -0,0 +1,25 @@ + + +
+
+ {#each items as item} +
+ {#if item.title} +

{item.title}

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

{item.description}

+ {/if} +
+ {/each} +
+
diff --git a/packages/shadcn-svelte/src/render/Checkbox.svelte b/packages/shadcn-svelte/src/render/Checkbox.svelte new file mode 100644 index 00000000..95b95699 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Checkbox.svelte @@ -0,0 +1,48 @@ + + +
+ + {#if errors.length > 0} +

{errors[0]}

+ {/if} +
diff --git a/packages/shadcn-svelte/src/render/Collapsible.svelte b/packages/shadcn-svelte/src/render/Collapsible.svelte new file mode 100644 index 00000000..3b4c9930 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Collapsible.svelte @@ -0,0 +1,27 @@ + + +
+ + {#if open} +
{@render children?.()}
+ {/if} +
diff --git a/packages/shadcn-svelte/src/render/Dialog.svelte b/packages/shadcn-svelte/src/render/Dialog.svelte new file mode 100644 index 00000000..960cacca --- /dev/null +++ b/packages/shadcn-svelte/src/render/Dialog.svelte @@ -0,0 +1,36 @@ + + +{#if open} +
+
e.stopPropagation()}> +
+

{props.title}

+ {#if props.description} +

{props.description}

+ {/if} +
+ {@render children?.()} +
+
+{/if} diff --git a/packages/shadcn-svelte/src/render/Drawer.svelte b/packages/shadcn-svelte/src/render/Drawer.svelte new file mode 100644 index 00000000..39f75f09 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Drawer.svelte @@ -0,0 +1,36 @@ + + +{#if open} +
+
e.stopPropagation()}> +
+

{props.title}

+ {#if props.description} +

{props.description}

+ {/if} +
+ {@render children?.()} +
+
+{/if} diff --git a/packages/shadcn-svelte/src/render/DropdownMenu.svelte b/packages/shadcn-svelte/src/render/DropdownMenu.svelte new file mode 100644 index 00000000..ce2f6aa4 --- /dev/null +++ b/packages/shadcn-svelte/src/render/DropdownMenu.svelte @@ -0,0 +1,36 @@ + + +
+ {props.label} +
+ {#each props.items ?? [] as item} + + {/each} +
+
diff --git a/packages/shadcn-svelte/src/render/Grid.svelte b/packages/shadcn-svelte/src/render/Grid.svelte new file mode 100644 index 00000000..c0bdcd8d --- /dev/null +++ b/packages/shadcn-svelte/src/render/Grid.svelte @@ -0,0 +1,32 @@ + + +
+ {@render children?.()} +
diff --git a/packages/shadcn-svelte/src/render/Heading.svelte b/packages/shadcn-svelte/src/render/Heading.svelte new file mode 100644 index 00000000..e8c3ad68 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Heading.svelte @@ -0,0 +1,21 @@ + + +{props.text} diff --git a/packages/shadcn-svelte/src/render/Image.svelte b/packages/shadcn-svelte/src/render/Image.svelte new file mode 100644 index 00000000..ecb82cf4 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Image.svelte @@ -0,0 +1,25 @@ + + +{#if props.src} + {props.alt} +{:else} +
+ {props.alt} +
+{/if} diff --git a/packages/shadcn-svelte/src/render/Input.svelte b/packages/shadcn-svelte/src/render/Input.svelte new file mode 100644 index 00000000..626eab65 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Input.svelte @@ -0,0 +1,63 @@ + + +
+ + emit("focus")} + onblur={handleBlur} + onkeydown={(e) => { + if ((e as KeyboardEvent).key === "Enter") emit("submit"); + }} + /> + {#if errors.length > 0} +

{errors[0]}

+ {/if} +
diff --git a/packages/shadcn-svelte/src/render/Link.svelte b/packages/shadcn-svelte/src/render/Link.svelte new file mode 100644 index 00000000..6953b44a --- /dev/null +++ b/packages/shadcn-svelte/src/render/Link.svelte @@ -0,0 +1,22 @@ + + + + {props.label} + diff --git a/packages/shadcn-svelte/src/render/Pagination.svelte b/packages/shadcn-svelte/src/render/Pagination.svelte new file mode 100644 index 00000000..2f5a6ca1 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Pagination.svelte @@ -0,0 +1,47 @@ + + +
+ + {#each pages as p} + {#if p === "ellipsis"} + + {:else} + + {/if} + {/each} + +
diff --git a/packages/shadcn-svelte/src/render/Popover.svelte b/packages/shadcn-svelte/src/render/Popover.svelte new file mode 100644 index 00000000..9564e565 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Popover.svelte @@ -0,0 +1,15 @@ + + +
+ {props.trigger} +
+ {props.content} +
+
diff --git a/packages/shadcn-svelte/src/render/Progress.svelte b/packages/shadcn-svelte/src/render/Progress.svelte new file mode 100644 index 00000000..fb552f5a --- /dev/null +++ b/packages/shadcn-svelte/src/render/Progress.svelte @@ -0,0 +1,19 @@ + + +
+ {#if props.label} +
{props.label}
+ {/if} + +
diff --git a/packages/shadcn-svelte/src/render/Radio.svelte b/packages/shadcn-svelte/src/render/Radio.svelte new file mode 100644 index 00000000..d03ca7a6 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Radio.svelte @@ -0,0 +1,56 @@ + + +
+ + {#each props.options ?? [] as option, i} + + {/each} + {#if errors.length > 0} +

{errors[0]}

+ {/if} +
diff --git a/packages/shadcn-svelte/src/render/Select.svelte b/packages/shadcn-svelte/src/render/Select.svelte new file mode 100644 index 00000000..fb604b27 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Select.svelte @@ -0,0 +1,56 @@ + + +
+ + + + {value || props.placeholder || "Select..."} + + + {#each props.options ?? [] as option, i} + {option} + {/each} + + + {#if errors.length > 0} +

{errors[0]}

+ {/if} +
diff --git a/packages/shadcn-svelte/src/render/Separator.svelte b/packages/shadcn-svelte/src/render/Separator.svelte new file mode 100644 index 00000000..35fd43a1 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Separator.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/shadcn-svelte/src/render/Skeleton.svelte b/packages/shadcn-svelte/src/render/Skeleton.svelte new file mode 100644 index 00000000..70d4de9c --- /dev/null +++ b/packages/shadcn-svelte/src/render/Skeleton.svelte @@ -0,0 +1,13 @@ + + +
diff --git a/packages/shadcn-svelte/src/render/Slider.svelte b/packages/shadcn-svelte/src/render/Slider.svelte new file mode 100644 index 00000000..8f9f0942 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Slider.svelte @@ -0,0 +1,45 @@ + + +
+ {#if props.label} +
+ {props.label} + {value} +
+ {/if} + +
diff --git a/packages/shadcn-svelte/src/render/Spinner.svelte b/packages/shadcn-svelte/src/render/Spinner.svelte new file mode 100644 index 00000000..e5b2dd6c --- /dev/null +++ b/packages/shadcn-svelte/src/render/Spinner.svelte @@ -0,0 +1,19 @@ + + +
+
+ {#if props.label} + {props.label} + {/if} +
diff --git a/packages/shadcn-svelte/src/render/Stack.svelte b/packages/shadcn-svelte/src/render/Stack.svelte new file mode 100644 index 00000000..8fdcd51b --- /dev/null +++ b/packages/shadcn-svelte/src/render/Stack.svelte @@ -0,0 +1,39 @@ + + +
+ {@render children?.()} +
diff --git a/packages/shadcn-svelte/src/render/Switch.svelte b/packages/shadcn-svelte/src/render/Switch.svelte new file mode 100644 index 00000000..1f3d5a8d --- /dev/null +++ b/packages/shadcn-svelte/src/render/Switch.svelte @@ -0,0 +1,53 @@ + + +
+ + {#if errors.length > 0} +

{errors[0]}

+ {/if} +
diff --git a/packages/shadcn-svelte/src/render/Table.svelte b/packages/shadcn-svelte/src/render/Table.svelte new file mode 100644 index 00000000..089a9da2 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Table.svelte @@ -0,0 +1,36 @@ + + +
+ + {#if props.caption} + {props.caption} + {/if} + + + {#each columns as col} + {col} + {/each} + + + + {#each rows as row} + + {#each row as cell} + {cell} + {/each} + + {/each} + + +
diff --git a/packages/shadcn-svelte/src/render/Tabs.svelte b/packages/shadcn-svelte/src/render/Tabs.svelte new file mode 100644 index 00000000..c9f5c6f3 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Tabs.svelte @@ -0,0 +1,50 @@ + + + + + {#each tabs as tab} + {tab.label} + {/each} + + {@render children?.()} + diff --git a/packages/shadcn-svelte/src/render/Text.svelte b/packages/shadcn-svelte/src/render/Text.svelte new file mode 100644 index 00000000..d0a2de85 --- /dev/null +++ b/packages/shadcn-svelte/src/render/Text.svelte @@ -0,0 +1,22 @@ + + +

{props.text}

diff --git a/packages/shadcn-svelte/src/render/Textarea.svelte b/packages/shadcn-svelte/src/render/Textarea.svelte new file mode 100644 index 00000000..02e0304b --- /dev/null +++ b/packages/shadcn-svelte/src/render/Textarea.svelte @@ -0,0 +1,58 @@ + + +
+ +