Эх сурвалжийг харах

feat: Svelte-renderer (#168)

* create svelte-renderer

* helpers

* reactive API

* turn into provider components

* fixes

* examples

* docs

* correct component props for catalog, harmonize defineRegistry API with other APis

* shuffle things around

* createRenderer

* on

* boundary/warning

* functions

* fix

* fix types
Simon H 4 сар өмнө
parent
commit
512f7fe5c5
100 өөрчлөгдсөн 3081 нэмэгдсэн , 3 устгасан
  1. 1 0
      .gitignore
  2. 2 1
      README.md
  3. 126 0
      apps/web/app/(main)/docs/api/svelte/page.mdx
  4. 8 0
      apps/web/app/(main)/docs/installation/page.mdx
  5. 7 1
      apps/web/lib/docs-navigation.ts
  6. 1 0
      apps/web/lib/page-titles.ts
  7. 1 1
      examples/react-email/package.json
  8. 23 0
      examples/svelte-chat/.gitignore
  9. 1 0
      examples/svelte-chat/.npmrc
  10. 42 0
      examples/svelte-chat/README.md
  11. 16 0
      examples/svelte-chat/components.json
  12. 39 0
      examples/svelte-chat/package.json
  13. 125 0
      examples/svelte-chat/src/app.css
  14. 13 0
      examples/svelte-chat/src/app.d.ts
  15. 11 0
      examples/svelte-chat/src/app.html
  16. 92 0
      examples/svelte-chat/src/lib/agent.ts
  17. 1 0
      examples/svelte-chat/src/lib/assets/favicon.svg
  18. 22 0
      examples/svelte-chat/src/lib/components/ui/accordion/accordion-content.svelte
  19. 17 0
      examples/svelte-chat/src/lib/components/ui/accordion/accordion-item.svelte
  20. 32 0
      examples/svelte-chat/src/lib/components/ui/accordion/accordion-trigger.svelte
  21. 16 0
      examples/svelte-chat/src/lib/components/ui/accordion/accordion.svelte
  22. 16 0
      examples/svelte-chat/src/lib/components/ui/accordion/index.ts
  23. 23 0
      examples/svelte-chat/src/lib/components/ui/alert/alert-description.svelte
  24. 20 0
      examples/svelte-chat/src/lib/components/ui/alert/alert-title.svelte
  25. 44 0
      examples/svelte-chat/src/lib/components/ui/alert/alert.svelte
  26. 14 0
      examples/svelte-chat/src/lib/components/ui/alert/index.ts
  27. 50 0
      examples/svelte-chat/src/lib/components/ui/badge/badge.svelte
  28. 2 0
      examples/svelte-chat/src/lib/components/ui/badge/index.ts
  29. 82 0
      examples/svelte-chat/src/lib/components/ui/button/button.svelte
  30. 17 0
      examples/svelte-chat/src/lib/components/ui/button/index.ts
  31. 20 0
      examples/svelte-chat/src/lib/components/ui/card/card-action.svelte
  32. 15 0
      examples/svelte-chat/src/lib/components/ui/card/card-content.svelte
  33. 20 0
      examples/svelte-chat/src/lib/components/ui/card/card-description.svelte
  34. 20 0
      examples/svelte-chat/src/lib/components/ui/card/card-footer.svelte
  35. 23 0
      examples/svelte-chat/src/lib/components/ui/card/card-header.svelte
  36. 20 0
      examples/svelte-chat/src/lib/components/ui/card/card-title.svelte
  37. 23 0
      examples/svelte-chat/src/lib/components/ui/card/card.svelte
  38. 25 0
      examples/svelte-chat/src/lib/components/ui/card/index.ts
  39. 7 0
      examples/svelte-chat/src/lib/components/ui/input/index.ts
  40. 52 0
      examples/svelte-chat/src/lib/components/ui/input/input.svelte
  41. 7 0
      examples/svelte-chat/src/lib/components/ui/label/index.ts
  42. 20 0
      examples/svelte-chat/src/lib/components/ui/label/label.svelte
  43. 7 0
      examples/svelte-chat/src/lib/components/ui/progress/index.ts
  44. 27 0
      examples/svelte-chat/src/lib/components/ui/progress/progress.svelte
  45. 10 0
      examples/svelte-chat/src/lib/components/ui/radio-group/index.ts
  46. 31 0
      examples/svelte-chat/src/lib/components/ui/radio-group/radio-group-item.svelte
  47. 19 0
      examples/svelte-chat/src/lib/components/ui/radio-group/radio-group.svelte
  48. 37 0
      examples/svelte-chat/src/lib/components/ui/select/index.ts
  49. 45 0
      examples/svelte-chat/src/lib/components/ui/select/select-content.svelte
  50. 21 0
      examples/svelte-chat/src/lib/components/ui/select/select-group-heading.svelte
  51. 7 0
      examples/svelte-chat/src/lib/components/ui/select/select-group.svelte
  52. 38 0
      examples/svelte-chat/src/lib/components/ui/select/select-item.svelte
  53. 20 0
      examples/svelte-chat/src/lib/components/ui/select/select-label.svelte
  54. 7 0
      examples/svelte-chat/src/lib/components/ui/select/select-portal.svelte
  55. 20 0
      examples/svelte-chat/src/lib/components/ui/select/select-scroll-down-button.svelte
  56. 20 0
      examples/svelte-chat/src/lib/components/ui/select/select-scroll-up-button.svelte
  57. 18 0
      examples/svelte-chat/src/lib/components/ui/select/select-separator.svelte
  58. 29 0
      examples/svelte-chat/src/lib/components/ui/select/select-trigger.svelte
  59. 11 0
      examples/svelte-chat/src/lib/components/ui/select/select.svelte
  60. 7 0
      examples/svelte-chat/src/lib/components/ui/separator/index.ts
  61. 21 0
      examples/svelte-chat/src/lib/components/ui/separator/separator.svelte
  62. 28 0
      examples/svelte-chat/src/lib/components/ui/table/index.ts
  63. 20 0
      examples/svelte-chat/src/lib/components/ui/table/table-body.svelte
  64. 20 0
      examples/svelte-chat/src/lib/components/ui/table/table-caption.svelte
  65. 23 0
      examples/svelte-chat/src/lib/components/ui/table/table-cell.svelte
  66. 20 0
      examples/svelte-chat/src/lib/components/ui/table/table-footer.svelte
  67. 23 0
      examples/svelte-chat/src/lib/components/ui/table/table-head.svelte
  68. 20 0
      examples/svelte-chat/src/lib/components/ui/table/table-header.svelte
  69. 23 0
      examples/svelte-chat/src/lib/components/ui/table/table-row.svelte
  70. 22 0
      examples/svelte-chat/src/lib/components/ui/table/table.svelte
  71. 16 0
      examples/svelte-chat/src/lib/components/ui/tabs/index.ts
  72. 17 0
      examples/svelte-chat/src/lib/components/ui/tabs/tabs-content.svelte
  73. 20 0
      examples/svelte-chat/src/lib/components/ui/tabs/tabs-list.svelte
  74. 20 0
      examples/svelte-chat/src/lib/components/ui/tabs/tabs-trigger.svelte
  75. 19 0
      examples/svelte-chat/src/lib/components/ui/tabs/tabs.svelte
  76. 4 0
      examples/svelte-chat/src/lib/index.ts
  77. 16 0
      examples/svelte-chat/src/lib/render/Renderer.svelte
  78. 363 0
      examples/svelte-chat/src/lib/render/catalog.ts
  79. 22 0
      examples/svelte-chat/src/lib/render/components/Accordion.svelte
  80. 19 0
      examples/svelte-chat/src/lib/render/components/Alert.svelte
  81. 13 0
      examples/svelte-chat/src/lib/render/components/Badge.svelte
  82. 99 0
      examples/svelte-chat/src/lib/render/components/BarChart.svelte
  83. 22 0
      examples/svelte-chat/src/lib/render/components/Button.svelte
  84. 57 0
      examples/svelte-chat/src/lib/render/components/Callout.svelte
  85. 32 0
      examples/svelte-chat/src/lib/render/components/Card.svelte
  86. 31 0
      examples/svelte-chat/src/lib/render/components/Grid.svelte
  87. 22 0
      examples/svelte-chat/src/lib/render/components/Heading.svelte
  88. 109 0
      examples/svelte-chat/src/lib/render/components/LineChart.svelte
  89. 19 0
      examples/svelte-chat/src/lib/render/components/Link.svelte
  90. 40 0
      examples/svelte-chat/src/lib/render/components/Metric.svelte
  91. 106 0
      examples/svelte-chat/src/lib/render/components/PieChart.svelte
  92. 13 0
      examples/svelte-chat/src/lib/render/components/Progress.svelte
  93. 45 0
      examples/svelte-chat/src/lib/render/components/RadioGroup.svelte
  94. 56 0
      examples/svelte-chat/src/lib/render/components/SelectInput.svelte
  95. 10 0
      examples/svelte-chat/src/lib/render/components/Separator.svelte
  96. 14 0
      examples/svelte-chat/src/lib/render/components/Skeleton.svelte
  97. 28 0
      examples/svelte-chat/src/lib/render/components/Stack.svelte
  98. 19 0
      examples/svelte-chat/src/lib/render/components/TabContent.svelte
  99. 91 0
      examples/svelte-chat/src/lib/render/components/Table.svelte
  100. 29 0
      examples/svelte-chat/src/lib/render/components/Tabs.svelte

+ 1 - 0
.gitignore

@@ -27,6 +27,7 @@ out/
 build
 dist
 *.tsbuildinfo
+.svelte-kit/
 
 
 # Debug

+ 2 - 1
README.md

@@ -462,8 +462,9 @@ pnpm dev
 - http://react-email-demo.json-render.localhost:1355 - React Email Example
 - http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
 - Chat Example: run `pnpm dev` in `examples/chat`
+- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
 - Vue Example: run `pnpm dev` in `examples/vue`
-- Vite Renderers (React + Vue): run `pnpm dev` in `examples/vite-renderers`
+- Vite Renderers (React + Vue + Svelte): run `pnpm dev` in `examples/vite-renderers`
 - React Native example: run `npx expo start` in `examples/react-native`
 
 ## How It Works

+ 126 - 0
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
+
+<PackageInstall packages="@json-render/core @json-render/svelte" />
+
+Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
+
+<PackageInstall packages="svelte zod" />
+
+## Components
+
+### Renderer
+
+```svelte
+<Renderer
+  spec={spec}       // Spec | null
+  registry={registry}
+  loading={false}
+/>
+```
+
+Renders a spec with your component registry. If `spec` is `null`, it renders nothing.
+
+### JsonUIProvider
+
+Convenience wrapper around `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`.
+
+```svelte
+<JsonUIProvider
+  initialState={{}}
+  handlers={handlers}
+  validationFunctions={validationFunctions}
+>
+  <Renderer {spec} {registry} />
+</JsonUIProvider>
+```
+
+## defineRegistry
+
+Create a typed component registry and action handlers from a catalog.
+
+```typescript
+import { defineRegistry } from "@json-render/svelte";
+
+const { registry, handlers, executeAction } = defineRegistry(catalog, {
+  components: {
+    Card,
+    Button,
+  },
+  actions: {
+    submit: async (params, setState, state) => {
+      // custom action logic
+    },
+  },
+});
+```
+
+`handlers` is designed for `JsonUIProvider`/`ActionProvider`. `executeAction` is an imperative helper.
+
+## Component Props
+
+Registry components receive `BaseComponentProps<TProps>`:
+
+```typescript
+interface BaseComponentProps<TProps> {
+  props: TProps;
+  children?: Snippet;
+  emit: (event: string) => void;
+  bindings?: Record<string, string>;
+  loading?: boolean;
+}
+```
+
+Use `emit("eventName")` to trigger handlers declared in the spec `on` bindings.
+
+## Context Helpers
+
+Use these helpers inside Svelte components:
+
+- `getStateValue(path)` - read/write state via `.current`
+- `getBoundProp(() => value, () => bindingPath)` - write back resolved `$bindState` / `$bindItem` values
+- `isVisible(condition)` - evaluate visibility via `.current`
+- `getAction(name)` - read a registered action handler via `.current`
+- `getFieldValidation(ctx, path, config)` - get field validation state + actions
+
+For advanced usage, access full contexts:
+
+- `getStateContext()`
+- `getActionContext()`
+- `getVisibilityContext()`
+- `getValidationContext()`
+- `getOptionalValidationContext()`
+
+## Streaming
+
+### createUIStream
+
+```typescript
+const stream = createUIStream({
+  api: "/api/generate-ui",
+  onComplete: (spec) => console.log(spec),
+});
+
+await stream.send("Create a login form");
+
+console.log(stream.spec);
+console.log(stream.isStreaming);
+```
+
+### createChatUI
+
+```typescript
+const chat = createChatUI({ api: "/api/chat-ui" });
+await chat.send("Build a settings panel");
+console.log(chat.messages, chat.isStreaming);
+```
+
+## Schema Export
+
+Use `schema` from `@json-render/svelte` when defining catalogs for Svelte specs.

+ 8 - 0
apps/web/app/(main)/docs/installation/page.mdx

@@ -21,6 +21,14 @@ Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.
 
 <PackageInstall packages="vue zod" />
 
+## For Svelte
+
+<PackageInstall packages="@json-render/core @json-render/svelte" />
+
+Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
+
+<PackageInstall packages="svelte zod" />
+
 ## For React UI with shadcn/ui
 
 Pre-built components for fast prototyping and production use:

+ 7 - 1
apps/web/lib/docs-navigation.ts

@@ -80,13 +80,18 @@ export const docsNavigation: NavSection[] = [
         href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
         external: true,
       },
+      {
+        title: "Svelte",
+        href: "https://github.com/vercel-labs/json-render/tree/main/examples/svelte",
+        external: true,
+      },
       {
         title: "Vue",
         href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
         external: true,
       },
       {
-        title: "Renders with Vite (Vue / React)",
+        title: "Renders with Vite (Vue / React / Svelte)",
         href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
         external: true,
       },
@@ -121,6 +126,7 @@ export const docsNavigation: NavSection[] = [
       { title: "@json-render/image", href: "/docs/api/image" },
       { title: "@json-render/remotion", href: "/docs/api/remotion" },
       { title: "@json-render/vue", href: "/docs/api/vue" },
+      { title: "@json-render/svelte", href: "/docs/api/svelte" },
       { title: "@json-render/codegen", href: "/docs/api/codegen" },
     ],
   },

+ 1 - 0
apps/web/lib/page-titles.ts

@@ -45,6 +45,7 @@ export const PAGE_TITLES: Record<string, string> = {
   "docs/api/react-pdf": "@json-render/react-pdf API",
   "docs/api/react-email": "@json-render/react-email API",
   "docs/api/react-native": "@json-render/react-native API",
+  "docs/api/svelte": "@json-render/svelte API",
   "docs/api/codegen": "@json-render/codegen API",
   "docs/api/image": "@json-render/image API",
   "docs/api/remotion": "@json-render/remotion API",

+ 1 - 1
examples/react-email/package.json

@@ -25,7 +25,7 @@
     "react-resizable-panels": "^4.4.1",
     "tailwind-merge": "^3.5.0",
     "tailwindcss": "^4.1.18",
-    "zod": "4.3.5"
+    "zod": "4.3.6"
   },
   "devDependencies": {
     "@internal/typescript-config": "workspace:*",

+ 23 - 0
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-*

+ 1 - 0
examples/svelte-chat/.npmrc

@@ -0,0 +1 @@
+engine-strict=true

+ 42 - 0
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.

+ 16 - 0
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"
+}

+ 39 - 0
examples/svelte-chat/package.json

@@ -0,0 +1,39 @@
+{
+  "name": "svelte-chat",
+  "private": true,
+  "version": "0.0.1",
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev",
+    "build": "vite build",
+    "preview": "vite preview",
+    "prepare": "svelte-kit sync || echo ''",
+    "check-types": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
+  },
+  "devDependencies": {
+    "@internationalized/date": "^3.10.0",
+    "@lucide/svelte": "^0.561.0",
+    "@sveltejs/adapter-auto": "^7.0.0",
+    "@sveltejs/kit": "^2.50.2",
+    "@sveltejs/vite-plugin-svelte": "^6.2.4",
+    "@tailwindcss/vite": "^4.0.0",
+    "bits-ui": "^2.14.4",
+    "svelte": "^5.49.2",
+    "svelte-check": "^4.3.6",
+    "tailwind-variants": "^3.2.2",
+    "tailwindcss": "^4.0.0",
+    "typescript": "^5.9.3",
+    "vite": "^7.3.1"
+  },
+  "dependencies": {
+    "@ai-sdk/gateway": "^3.0.46",
+    "@ai-sdk/svelte": "^4.0.96",
+    "@json-render/core": "workspace:*",
+    "@json-render/svelte": "workspace:*",
+    "ai": "^6.0.86",
+    "clsx": "^2.1.1",
+    "lucide-svelte": "^0.500.0",
+    "tailwind-merge": "^3.2.0",
+    "zod": "4.3.6"
+  }
+}

+ 125 - 0
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;
+}

+ 13 - 0
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 {};

+ 11 - 0
examples/svelte-chat/src/app.html

@@ -0,0 +1,11 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1" />
+		%sveltekit.head%
+	</head>
+	<body data-sveltekit-preload-data="hover">
+		<div style="display: contents">%sveltekit.body%</div>
+	</body>
+</html>

+ 92 - 0
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,
+});

+ 1 - 0
examples/svelte-chat/src/lib/assets/favicon.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

+ 22 - 0
examples/svelte-chat/src/lib/components/ui/accordion/accordion-content.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	import { Accordion as AccordionPrimitive } from "bits-ui";
+	import { cn, type WithoutChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
+</script>
+
+<AccordionPrimitive.Content
+	bind:ref
+	data-slot="accordion-content"
+	class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
+	{...restProps}
+>
+	<div class={cn("pt-0 pb-4", className)}>
+		{@render children?.()}
+	</div>
+</AccordionPrimitive.Content>

+ 17 - 0
examples/svelte-chat/src/lib/components/ui/accordion/accordion-item.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	import { Accordion as AccordionPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: AccordionPrimitive.ItemProps = $props();
+</script>
+
+<AccordionPrimitive.Item
+	bind:ref
+	data-slot="accordion-item"
+	class={cn("border-b last:border-b-0", className)}
+	{...restProps}
+/>

+ 32 - 0
examples/svelte-chat/src/lib/components/ui/accordion/accordion-trigger.svelte

@@ -0,0 +1,32 @@
+<script lang="ts">
+	import { Accordion as AccordionPrimitive } from "bits-ui";
+	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
+	import { cn, type WithoutChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		level = 3,
+		children,
+		...restProps
+	}: WithoutChild<AccordionPrimitive.TriggerProps> & {
+		level?: AccordionPrimitive.HeaderProps["level"];
+	} = $props();
+</script>
+
+<AccordionPrimitive.Header {level} class="flex">
+	<AccordionPrimitive.Trigger
+		data-slot="accordion-trigger"
+		bind:ref
+		class={cn(
+			"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
+			className
+		)}
+		{...restProps}
+	>
+		{@render children?.()}
+		<ChevronDownIcon
+			class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
+		/>
+	</AccordionPrimitive.Trigger>
+</AccordionPrimitive.Header>

+ 16 - 0
examples/svelte-chat/src/lib/components/ui/accordion/accordion.svelte

@@ -0,0 +1,16 @@
+<script lang="ts">
+	import { Accordion as AccordionPrimitive } from "bits-ui";
+
+	let {
+		ref = $bindable(null),
+		value = $bindable(),
+		...restProps
+	}: AccordionPrimitive.RootProps = $props();
+</script>
+
+<AccordionPrimitive.Root
+	bind:ref
+	bind:value={value as never}
+	data-slot="accordion"
+	{...restProps}
+/>

+ 16 - 0
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,
+};

+ 23 - 0
examples/svelte-chat/src/lib/components/ui/alert/alert-description.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="alert-description"
+	class={cn(
+		"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/alert/alert-title.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="alert-title"
+	class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 44 - 0
examples/svelte-chat/src/lib/components/ui/alert/alert.svelte

@@ -0,0 +1,44 @@
+<script lang="ts" module>
+	import { type VariantProps, tv } from "tailwind-variants";
+
+	export const alertVariants = tv({
+		base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+		variants: {
+			variant: {
+				default: "bg-card text-card-foreground",
+				destructive:
+					"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
+			},
+		},
+		defaultVariants: {
+			variant: "default",
+		},
+	});
+
+	export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
+</script>
+
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		variant = "default",
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
+		variant?: AlertVariant;
+	} = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="alert"
+	class={cn(alertVariants({ variant }), className)}
+	{...restProps}
+	role="alert"
+>
+	{@render children?.()}
+</div>

+ 14 - 0
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,
+};

+ 50 - 0
examples/svelte-chat/src/lib/components/ui/badge/badge.svelte

@@ -0,0 +1,50 @@
+<script lang="ts" module>
+	import { type VariantProps, tv } from "tailwind-variants";
+
+	export const badgeVariants = tv({
+		base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
+		variants: {
+			variant: {
+				default:
+					"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
+				secondary:
+					"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
+				destructive:
+					"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
+				outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+			},
+		},
+		defaultVariants: {
+			variant: "default",
+		},
+	});
+
+	export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
+</script>
+
+<script lang="ts">
+	import type { HTMLAnchorAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		href,
+		class: className,
+		variant = "default",
+		children,
+		...restProps
+	}: WithElementRef<HTMLAnchorAttributes> & {
+		variant?: BadgeVariant;
+	} = $props();
+</script>
+
+<svelte:element
+	this={href ? "a" : "span"}
+	bind:this={ref}
+	data-slot="badge"
+	{href}
+	class={cn(badgeVariants({ variant }), className)}
+	{...restProps}
+>
+	{@render children?.()}
+</svelte:element>

+ 2 - 0
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";

+ 82 - 0
examples/svelte-chat/src/lib/components/ui/button/button.svelte

@@ -0,0 +1,82 @@
+<script lang="ts" module>
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
+	import { type VariantProps, tv } from "tailwind-variants";
+
+	export const buttonVariants = tv({
+		base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+		variants: {
+			variant: {
+				default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
+				destructive:
+					"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
+				outline:
+					"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
+				secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
+				ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+				link: "text-primary underline-offset-4 hover:underline",
+			},
+			size: {
+				default: "h-9 px-4 py-2 has-[>svg]:px-3",
+				sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
+				lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+				icon: "size-9",
+				"icon-sm": "size-8",
+				"icon-lg": "size-10",
+			},
+		},
+		defaultVariants: {
+			variant: "default",
+			size: "default",
+		},
+	});
+
+	export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
+	export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
+
+	export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
+		WithElementRef<HTMLAnchorAttributes> & {
+			variant?: ButtonVariant;
+			size?: ButtonSize;
+		};
+</script>
+
+<script lang="ts">
+	let {
+		class: className,
+		variant = "default",
+		size = "default",
+		ref = $bindable(null),
+		href = undefined,
+		type = "button",
+		disabled,
+		children,
+		...restProps
+	}: ButtonProps = $props();
+</script>
+
+{#if href}
+	<a
+		bind:this={ref}
+		data-slot="button"
+		class={cn(buttonVariants({ variant, size }), className)}
+		href={disabled ? undefined : href}
+		aria-disabled={disabled}
+		role={disabled ? "link" : undefined}
+		tabindex={disabled ? -1 : undefined}
+		{...restProps}
+	>
+		{@render children?.()}
+	</a>
+{:else}
+	<button
+		bind:this={ref}
+		data-slot="button"
+		class={cn(buttonVariants({ variant, size }), className)}
+		{type}
+		{disabled}
+		{...restProps}
+	>
+		{@render children?.()}
+	</button>
+{/if}

+ 17 - 0
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,
+};

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/card/card-action.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card-action"
+	class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 15 - 0
examples/svelte-chat/src/lib/components/ui/card/card-content.svelte

@@ -0,0 +1,15 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
+	{@render children?.()}
+</div>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/card/card-description.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
+</script>
+
+<p
+	bind:this={ref}
+	data-slot="card-description"
+	class={cn("text-muted-foreground text-sm", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</p>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/card/card-footer.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card-footer"
+	class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 23 - 0
examples/svelte-chat/src/lib/components/ui/card/card-header.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card-header"
+	class={cn(
+		"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/card/card-title.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card-title"
+	class={cn("leading-none font-semibold", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 23 - 0
examples/svelte-chat/src/lib/components/ui/card/card.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card"
+	class={cn(
+		"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 25 - 0
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,
+};

+ 7 - 0
examples/svelte-chat/src/lib/components/ui/input/index.ts

@@ -0,0 +1,7 @@
+import Root from "./input.svelte";
+
+export {
+  Root,
+  //
+  Root as Input,
+};

+ 52 - 0
examples/svelte-chat/src/lib/components/ui/input/input.svelte

@@ -0,0 +1,52 @@
+<script lang="ts">
+	import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	type InputType = Exclude<HTMLInputTypeAttribute, "file">;
+
+	type Props = WithElementRef<
+		Omit<HTMLInputAttributes, "type"> &
+			({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
+	>;
+
+	let {
+		ref = $bindable(null),
+		value = $bindable(),
+		type,
+		files = $bindable(),
+		class: className,
+		"data-slot": dataSlot = "input",
+		...restProps
+	}: Props = $props();
+</script>
+
+{#if type === "file"}
+	<input
+		bind:this={ref}
+		data-slot={dataSlot}
+		class={cn(
+			"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50",
+			"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+			"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+			className
+		)}
+		type="file"
+		bind:files
+		bind:value
+		{...restProps}
+	/>
+{:else}
+	<input
+		bind:this={ref}
+		data-slot={dataSlot}
+		class={cn(
+			"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+			"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+			"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+			className
+		)}
+		{type}
+		bind:value
+		{...restProps}
+	/>
+{/if}

+ 7 - 0
examples/svelte-chat/src/lib/components/ui/label/index.ts

@@ -0,0 +1,7 @@
+import Root from "./label.svelte";
+
+export {
+  Root,
+  //
+  Root as Label,
+};

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/label/label.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { Label as LabelPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: LabelPrimitive.RootProps = $props();
+</script>
+
+<LabelPrimitive.Root
+	bind:ref
+	data-slot="label"
+	class={cn(
+		"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
+		className
+	)}
+	{...restProps}
+/>

+ 7 - 0
examples/svelte-chat/src/lib/components/ui/progress/index.ts

@@ -0,0 +1,7 @@
+import Root from "./progress.svelte";
+
+export {
+  Root,
+  //
+  Root as Progress,
+};

+ 27 - 0
examples/svelte-chat/src/lib/components/ui/progress/progress.svelte

@@ -0,0 +1,27 @@
+<script lang="ts">
+	import { Progress as ProgressPrimitive } from "bits-ui";
+	import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		max = 100,
+		value,
+		...restProps
+	}: WithoutChildrenOrChild<ProgressPrimitive.RootProps> = $props();
+</script>
+
+<ProgressPrimitive.Root
+	bind:ref
+	data-slot="progress"
+	class={cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className)}
+	{value}
+	{max}
+	{...restProps}
+>
+	<div
+		data-slot="progress-indicator"
+		class="bg-primary h-full w-full flex-1 transition-all"
+		style="transform: translateX(-{100 - (100 * (value ?? 0)) / (max ?? 1)}%)"
+	></div>
+</ProgressPrimitive.Root>

+ 10 - 0
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,
+};

+ 31 - 0
examples/svelte-chat/src/lib/components/ui/radio-group/radio-group-item.svelte

@@ -0,0 +1,31 @@
+<script lang="ts">
+	import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
+	import CircleIcon from "@lucide/svelte/icons/circle";
+	import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
+</script>
+
+<RadioGroupPrimitive.Item
+	bind:ref
+	data-slot="radio-group-item"
+	class={cn(
+		"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
+		className
+	)}
+	{...restProps}
+>
+	{#snippet children({ checked })}
+		<div data-slot="radio-group-indicator" class="relative flex items-center justify-center">
+			{#if checked}
+				<CircleIcon
+					class="fill-primary absolute start-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
+				/>
+			{/if}
+		</div>
+	{/snippet}
+</RadioGroupPrimitive.Item>

+ 19 - 0
examples/svelte-chat/src/lib/components/ui/radio-group/radio-group.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		value = $bindable(""),
+		...restProps
+	}: RadioGroupPrimitive.RootProps = $props();
+</script>
+
+<RadioGroupPrimitive.Root
+	bind:ref
+	bind:value
+	data-slot="radio-group"
+	class={cn("grid gap-3", className)}
+	{...restProps}
+/>

+ 37 - 0
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,
+};

+ 45 - 0
examples/svelte-chat/src/lib/components/ui/select/select-content.svelte

@@ -0,0 +1,45 @@
+<script lang="ts">
+	import { Select as SelectPrimitive } from "bits-ui";
+	import SelectPortal from "./select-portal.svelte";
+	import SelectScrollUpButton from "./select-scroll-up-button.svelte";
+	import SelectScrollDownButton from "./select-scroll-down-button.svelte";
+	import { cn, type WithoutChild } from "$lib/utils.js";
+	import type { ComponentProps } from "svelte";
+	import type { WithoutChildrenOrChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		sideOffset = 4,
+		portalProps,
+		children,
+		preventScroll = true,
+		...restProps
+	}: WithoutChild<SelectPrimitive.ContentProps> & {
+		portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
+	} = $props();
+</script>
+
+<SelectPortal {...portalProps}>
+	<SelectPrimitive.Content
+		bind:ref
+		{sideOffset}
+		{preventScroll}
+		data-slot="select-content"
+		class={cn(
+			"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+			className
+		)}
+		{...restProps}
+	>
+		<SelectScrollUpButton />
+		<SelectPrimitive.Viewport
+			class={cn(
+				"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1"
+			)}
+		>
+			{@render children?.()}
+		</SelectPrimitive.Viewport>
+		<SelectScrollDownButton />
+	</SelectPrimitive.Content>
+</SelectPortal>

+ 21 - 0
examples/svelte-chat/src/lib/components/ui/select/select-group-heading.svelte

@@ -0,0 +1,21 @@
+<script lang="ts">
+	import { Select as SelectPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+	import type { ComponentProps } from "svelte";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
+</script>
+
+<SelectPrimitive.GroupHeading
+	bind:ref
+	data-slot="select-group-heading"
+	class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</SelectPrimitive.GroupHeading>

+ 7 - 0
examples/svelte-chat/src/lib/components/ui/select/select-group.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Select as SelectPrimitive } from "bits-ui";
+
+	let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
+</script>
+
+<SelectPrimitive.Group bind:ref data-slot="select-group" {...restProps} />

+ 38 - 0
examples/svelte-chat/src/lib/components/ui/select/select-item.svelte

@@ -0,0 +1,38 @@
+<script lang="ts">
+	import CheckIcon from "@lucide/svelte/icons/check";
+	import { Select as SelectPrimitive } from "bits-ui";
+	import { cn, type WithoutChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		value,
+		label,
+		children: childrenProp,
+		...restProps
+	}: WithoutChild<SelectPrimitive.ItemProps> = $props();
+</script>
+
+<SelectPrimitive.Item
+	bind:ref
+	{value}
+	data-slot="select-item"
+	class={cn(
+		"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+		className
+	)}
+	{...restProps}
+>
+	{#snippet children({ selected, highlighted })}
+		<span class="absolute end-2 flex size-3.5 items-center justify-center">
+			{#if selected}
+				<CheckIcon class="size-4" />
+			{/if}
+		</span>
+		{#if childrenProp}
+			{@render childrenProp({ selected, highlighted })}
+		{:else}
+			{label || value}
+		{/if}
+	{/snippet}
+</SelectPrimitive.Item>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/select/select-label.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="select-label"
+	class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 7 - 0
examples/svelte-chat/src/lib/components/ui/select/select-portal.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Select as SelectPrimitive } from "bits-ui";
+
+	let { ...restProps }: SelectPrimitive.PortalProps = $props();
+</script>
+
+<SelectPrimitive.Portal {...restProps} />

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/select/select-scroll-down-button.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
+	import { Select as SelectPrimitive } from "bits-ui";
+	import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
+</script>
+
+<SelectPrimitive.ScrollDownButton
+	bind:ref
+	data-slot="select-scroll-down-button"
+	class={cn("flex cursor-default items-center justify-center py-1", className)}
+	{...restProps}
+>
+	<ChevronDownIcon class="size-4" />
+</SelectPrimitive.ScrollDownButton>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/select/select-scroll-up-button.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
+	import { Select as SelectPrimitive } from "bits-ui";
+	import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
+</script>
+
+<SelectPrimitive.ScrollUpButton
+	bind:ref
+	data-slot="select-scroll-up-button"
+	class={cn("flex cursor-default items-center justify-center py-1", className)}
+	{...restProps}
+>
+	<ChevronUpIcon class="size-4" />
+</SelectPrimitive.ScrollUpButton>

+ 18 - 0
examples/svelte-chat/src/lib/components/ui/select/select-separator.svelte

@@ -0,0 +1,18 @@
+<script lang="ts">
+	import type { Separator as SeparatorPrimitive } from "bits-ui";
+	import { Separator } from "$lib/components/ui/separator/index.js";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: SeparatorPrimitive.RootProps = $props();
+</script>
+
+<Separator
+	bind:ref
+	data-slot="select-separator"
+	class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
+	{...restProps}
+/>

+ 29 - 0
examples/svelte-chat/src/lib/components/ui/select/select-trigger.svelte

@@ -0,0 +1,29 @@
+<script lang="ts">
+	import { Select as SelectPrimitive } from "bits-ui";
+	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
+	import { cn, type WithoutChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		size = "default",
+		...restProps
+	}: WithoutChild<SelectPrimitive.TriggerProps> & {
+		size?: "sm" | "default";
+	} = $props();
+</script>
+
+<SelectPrimitive.Trigger
+	bind:ref
+	data-slot="select-trigger"
+	data-size={size}
+	class={cn(
+		"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+	<ChevronDownIcon class="size-4 opacity-50" />
+</SelectPrimitive.Trigger>

+ 11 - 0
examples/svelte-chat/src/lib/components/ui/select/select.svelte

@@ -0,0 +1,11 @@
+<script lang="ts">
+	import { Select as SelectPrimitive } from "bits-ui";
+
+	let {
+		open = $bindable(false),
+		value = $bindable(),
+		...restProps
+	}: SelectPrimitive.RootProps = $props();
+</script>
+
+<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />

+ 7 - 0
examples/svelte-chat/src/lib/components/ui/separator/index.ts

@@ -0,0 +1,7 @@
+import Root from "./separator.svelte";
+
+export {
+  Root,
+  //
+  Root as Separator,
+};

+ 21 - 0
examples/svelte-chat/src/lib/components/ui/separator/separator.svelte

@@ -0,0 +1,21 @@
+<script lang="ts">
+	import { Separator as SeparatorPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		"data-slot": dataSlot = "separator",
+		...restProps
+	}: SeparatorPrimitive.RootProps = $props();
+</script>
+
+<SeparatorPrimitive.Root
+	bind:ref
+	data-slot={dataSlot}
+	class={cn(
+		"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:min-h-full data-[orientation=vertical]:w-px",
+		className
+	)}
+	{...restProps}
+/>

+ 28 - 0
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,
+};

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/table/table-body.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
+</script>
+
+<tbody
+	bind:this={ref}
+	data-slot="table-body"
+	class={cn("[&_tr:last-child]:border-0", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</tbody>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/table/table-caption.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
+</script>
+
+<caption
+	bind:this={ref}
+	data-slot="table-caption"
+	class={cn("text-muted-foreground mt-4 text-sm", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</caption>

+ 23 - 0
examples/svelte-chat/src/lib/components/ui/table/table-cell.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLTdAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLTdAttributes> = $props();
+</script>
+
+<td
+	bind:this={ref}
+	data-slot="table-cell"
+	class={cn(
+		"bg-clip-padding p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</td>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/table/table-footer.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
+</script>
+
+<tfoot
+	bind:this={ref}
+	data-slot="table-footer"
+	class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</tfoot>

+ 23 - 0
examples/svelte-chat/src/lib/components/ui/table/table-head.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLThAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLThAttributes> = $props();
+</script>
+
+<th
+	bind:this={ref}
+	data-slot="table-head"
+	class={cn(
+		"text-foreground h-10 bg-clip-padding px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</th>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/table/table-header.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
+</script>
+
+<thead
+	bind:this={ref}
+	data-slot="table-header"
+	class={cn("[&_tr]:border-b", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</thead>

+ 23 - 0
examples/svelte-chat/src/lib/components/ui/table/table-row.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "$lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
+</script>
+
+<tr
+	bind:this={ref}
+	data-slot="table-row"
+	class={cn(
+		"hover:[&,&>svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</tr>

+ 22 - 0
examples/svelte-chat/src/lib/components/ui/table/table.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	import type { HTMLTableAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLTableAttributes> = $props();
+</script>
+
+<div data-slot="table-container" class="relative w-full overflow-x-auto">
+	<table
+		bind:this={ref}
+		data-slot="table"
+		class={cn("w-full caption-bottom text-sm", className)}
+		{...restProps}
+	>
+		{@render children?.()}
+	</table>
+</div>

+ 16 - 0
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,
+};

+ 17 - 0
examples/svelte-chat/src/lib/components/ui/tabs/tabs-content.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	import { Tabs as TabsPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: TabsPrimitive.ContentProps = $props();
+</script>
+
+<TabsPrimitive.Content
+	bind:ref
+	data-slot="tabs-content"
+	class={cn("flex-1 outline-none", className)}
+	{...restProps}
+/>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/tabs/tabs-list.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { Tabs as TabsPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: TabsPrimitive.ListProps = $props();
+</script>
+
+<TabsPrimitive.List
+	bind:ref
+	data-slot="tabs-list"
+	class={cn(
+		"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
+		className
+	)}
+	{...restProps}
+/>

+ 20 - 0
examples/svelte-chat/src/lib/components/ui/tabs/tabs-trigger.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { Tabs as TabsPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: TabsPrimitive.TriggerProps = $props();
+</script>
+
+<TabsPrimitive.Trigger
+	bind:ref
+	data-slot="tabs-trigger"
+	class={cn(
+		"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+		className
+	)}
+	{...restProps}
+/>

+ 19 - 0
examples/svelte-chat/src/lib/components/ui/tabs/tabs.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	import { Tabs as TabsPrimitive } from "bits-ui";
+	import { cn } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		value = $bindable(""),
+		class: className,
+		...restProps
+	}: TabsPrimitive.RootProps = $props();
+</script>
+
+<TabsPrimitive.Root
+	bind:ref
+	bind:value
+	data-slot="tabs"
+	class={cn("flex flex-col gap-2", className)}
+	{...restProps}
+/>

+ 4 - 0
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";

+ 16 - 0
examples/svelte-chat/src/lib/render/Renderer.svelte

@@ -0,0 +1,16 @@
+<script lang="ts">
+  import { Renderer, JsonUIProvider } from "@json-render/svelte";
+  import type { Spec } from "@json-render/svelte";
+  import { registry } from "./registry";
+
+  interface Props {
+    spec: Spec;
+    loading?: boolean;
+  }
+
+  let { spec, loading = false }: Props = $props();
+</script>
+
+<JsonUIProvider initialState={spec.state}>
+  <Renderer {spec} {registry} {loading} />
+</JsonUIProvider>

+ 363 - 0
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: {},
+});

+ 22 - 0
examples/svelte-chat/src/lib/render/components/Accordion.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import * as Accordion from "$lib/components/ui/accordion";
+
+  interface Props extends BaseComponentProps<{
+    items: Array<{ title: string; content: string }>;
+    type?: "single" | "multiple" | null;
+  }> {}
+
+  let { props }: Props = $props();
+</script>
+
+<Accordion.Root type={props.type === "single" ? "single" : "multiple"} class="w-full">
+  {#each props.items ?? [] as item, i}
+    <Accordion.Item value="item-{i}">
+      <Accordion.Trigger>{item.title}</Accordion.Trigger>
+      <Accordion.Content>
+        <p class="text-muted-foreground">{item.content}</p>
+      </Accordion.Content>
+    </Accordion.Item>
+  {/each}
+</Accordion.Root>

+ 19 - 0
examples/svelte-chat/src/lib/render/components/Alert.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import * as Alert from "$lib/components/ui/alert";
+
+  interface Props extends BaseComponentProps<{
+    variant?: "default" | "destructive" | null;
+    title: string;
+    description?: string | null;
+  }> {}
+
+  let { props }: Props = $props();
+</script>
+
+<Alert.Root variant={props.variant ?? "default"}>
+  <Alert.Title>{props.title}</Alert.Title>
+  {#if props.description}
+    <Alert.Description>{props.description}</Alert.Description>
+  {/if}
+</Alert.Root>

+ 13 - 0
examples/svelte-chat/src/lib/render/components/Badge.svelte

@@ -0,0 +1,13 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import { Badge } from "$lib/components/ui/badge";
+
+  interface Props extends BaseComponentProps<{
+    text: string;
+    variant?: "default" | "secondary" | "destructive" | "outline" | null;
+  }> {}
+
+  let { props }: Props = $props();
+</script>
+
+<Badge variant={props.variant ?? "default"}>{props.text}</Badge>

+ 99 - 0
examples/svelte-chat/src/lib/render/components/BarChart.svelte

@@ -0,0 +1,99 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+
+  interface Props extends BaseComponentProps<{
+    title?: string | null;
+    data: Array<Record<string, unknown>>;
+    xKey: string;
+    yKey: string;
+    aggregate?: "sum" | "count" | "avg" | null;
+    color?: string | null;
+    height?: number | null;
+  }> {}
+
+  let { props }: Props = $props();
+
+  const rawData = $derived(props.data);
+  const rawItems = $derived<Array<Record<string, unknown>>>(
+    Array.isArray(rawData)
+      ? rawData
+      : Array.isArray((rawData as Record<string, unknown>)?.data)
+        ? ((rawData as Record<string, unknown>).data as Array<Record<string, unknown>>)
+        : []
+  );
+
+  const processedData = $derived(() => {
+    if (rawItems.length === 0) return { items: [], valueKey: props.yKey };
+    
+    const { xKey, yKey, aggregate } = props;
+    
+    if (!aggregate) {
+      const formatted = rawItems.map((item) => ({
+        label: String(item[xKey] ?? ""),
+        value: typeof item[yKey] === "number" ? item[yKey] : parseFloat(String(item[yKey])) || 0,
+      }));
+      return { items: formatted, valueKey: yKey };
+    }
+
+    const groups = new Map<string, Array<Record<string, unknown>>>();
+    for (const item of rawItems) {
+      const groupKey = String(item[xKey] ?? "unknown");
+      const group = groups.get(groupKey) ?? [];
+      group.push(item);
+      groups.set(groupKey, group);
+    }
+
+    const valueKey = aggregate === "count" ? "count" : yKey;
+    const aggregated: Array<{ label: string; value: number }> = [];
+
+    for (const [key, group] of Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
+      let value: number;
+      if (aggregate === "count") {
+        value = group.length;
+      } else if (aggregate === "sum") {
+        value = group.reduce((sum, item) => {
+          const v = item[yKey];
+          return sum + (typeof v === "number" ? v : parseFloat(String(v)) || 0);
+        }, 0);
+      } else {
+        const sum = group.reduce((s, item) => {
+          const v = item[yKey];
+          return s + (typeof v === "number" ? v : parseFloat(String(v)) || 0);
+        }, 0);
+        value = group.length > 0 ? sum / group.length : 0;
+      }
+      aggregated.push({ label: key, value });
+    }
+
+    return { items: aggregated, valueKey };
+  });
+
+  const chartData = $derived(processedData());
+  const maxValue = $derived(Math.max(...chartData.items.map(d => d.value), 1));
+  const chartColor = $derived(props.color ?? "var(--chart-1)");
+</script>
+
+<div class="w-full">
+  {#if props.title}
+    <p class="text-sm font-medium mb-2">{props.title}</p>
+  {/if}
+  
+  {#if chartData.items.length === 0}
+    <div class="text-center py-4 text-muted-foreground">No data available</div>
+  {:else}
+    <div class="space-y-2" style="height: {props.height ?? 200}px">
+      {#each chartData.items as item}
+        <div class="flex items-center gap-2">
+          <span class="text-xs text-muted-foreground w-16 truncate">{item.label}</span>
+          <div class="flex-1 h-6 bg-muted rounded overflow-hidden">
+            <div 
+              class="h-full rounded transition-all"
+              style="width: {(item.value / maxValue) * 100}%; background-color: {chartColor}"
+            ></div>
+          </div>
+          <span class="text-xs font-medium w-12 text-right">{item.value.toLocaleString()}</span>
+        </div>
+      {/each}
+    </div>
+  {/if}
+</div>

+ 22 - 0
examples/svelte-chat/src/lib/render/components/Button.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import { Button } from "$lib/components/ui/button";
+
+  interface Props extends BaseComponentProps<{
+    label: string;
+    variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | null;
+    size?: "default" | "sm" | "lg" | null;
+    disabled?: boolean | null;
+  }> {}
+
+  let { props, emit }: Props = $props();
+</script>
+
+<Button
+  variant={props.variant ?? "default"}
+  size={props.size ?? "default"}
+  disabled={props.disabled ?? false}
+  onclick={() => emit("press")}
+>
+  {props.label}
+</Button>

+ 57 - 0
examples/svelte-chat/src/lib/render/components/Callout.svelte

@@ -0,0 +1,57 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import { Info, Lightbulb, AlertTriangle, Star } from "lucide-svelte";
+
+  interface Props extends BaseComponentProps<{
+    type?: "info" | "tip" | "warning" | "important" | null;
+    title?: string | null;
+    content: string;
+  }> {}
+
+  let { props }: Props = $props();
+
+  const configs = {
+    info: {
+      border: "border-l-blue-500",
+      bg: "bg-blue-500/5",
+      iconColor: "text-blue-500",
+    },
+    tip: {
+      border: "border-l-emerald-500",
+      bg: "bg-emerald-500/5",
+      iconColor: "text-emerald-500",
+    },
+    warning: {
+      border: "border-l-amber-500",
+      bg: "bg-amber-500/5",
+      iconColor: "text-amber-500",
+    },
+    important: {
+      border: "border-l-purple-500",
+      bg: "bg-purple-500/5",
+      iconColor: "text-purple-500",
+    },
+  };
+
+  const config = $derived(configs[props.type ?? "info"] ?? configs.info);
+</script>
+
+<div class="border-l-4 {config.border} {config.bg} rounded-r-lg p-4">
+  <div class="flex items-start gap-3">
+    {#if props.type === "tip"}
+      <Lightbulb class="h-5 w-5 mt-0.5 shrink-0 {config.iconColor}" />
+    {:else if props.type === "warning"}
+      <AlertTriangle class="h-5 w-5 mt-0.5 shrink-0 {config.iconColor}" />
+    {:else if props.type === "important"}
+      <Star class="h-5 w-5 mt-0.5 shrink-0 {config.iconColor}" />
+    {:else}
+      <Info class="h-5 w-5 mt-0.5 shrink-0 {config.iconColor}" />
+    {/if}
+    <div class="flex-1 min-w-0">
+      {#if props.title}
+        <p class="font-semibold text-sm mb-1">{props.title}</p>
+      {/if}
+      <p class="text-sm text-muted-foreground">{props.content}</p>
+    </div>
+  </div>
+</div>

+ 32 - 0
examples/svelte-chat/src/lib/render/components/Card.svelte

@@ -0,0 +1,32 @@
+<script lang="ts">
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import * as Card from "$lib/components/ui/card";
+
+  interface Props extends BaseComponentProps<{
+    title?: string | null;
+    description?: string | null;
+  }> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+</script>
+
+<Card.Root>
+  {#if props.title || props.description}
+    <Card.Header>
+      {#if props.title}
+        <Card.Title>{props.title}</Card.Title>
+      {/if}
+      {#if props.description}
+        <Card.Description>{props.description}</Card.Description>
+      {/if}
+    </Card.Header>
+  {/if}
+  <Card.Content class="flex flex-col gap-4">
+    {#if children}
+      {@render children()}
+    {/if}
+  </Card.Content>
+</Card.Root>

+ 31 - 0
examples/svelte-chat/src/lib/render/components/Grid.svelte

@@ -0,0 +1,31 @@
+<script lang="ts">
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+
+  interface Props extends BaseComponentProps<{
+    columns?: "1" | "2" | "3" | "4" | null;
+    gap?: "sm" | "md" | "lg" | null;
+  }> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  const colsClass = $derived(
+    {
+      "1": "grid-cols-1",
+      "2": "grid-cols-1 md:grid-cols-2",
+      "3": "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
+      "4": "grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
+    }[props.columns ?? "3"] ?? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
+  );
+  const gapClass = $derived(
+    { sm: "gap-2", md: "gap-4", lg: "gap-6" }[props.gap ?? "md"] ?? "gap-4"
+  );
+</script>
+
+<div class="grid {colsClass} {gapClass}">
+  {#if children}
+    {@render children()}
+  {/if}
+</div>

+ 22 - 0
examples/svelte-chat/src/lib/render/components/Heading.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+
+  interface Props extends BaseComponentProps<{
+    text: string;
+    level?: "h1" | "h2" | "h3" | "h4" | null;
+  }> {}
+
+  let { props }: Props = $props();
+
+  const level = $derived(props.level ?? "h2");
+</script>
+
+{#if level === "h1"}
+  <h1 class="text-3xl font-bold tracking-tight">{props.text}</h1>
+{:else if level === "h2"}
+  <h2 class="text-2xl font-semibold tracking-tight">{props.text}</h2>
+{:else if level === "h3"}
+  <h3 class="text-xl font-semibold">{props.text}</h3>
+{:else}
+  <h4 class="text-lg font-medium">{props.text}</h4>
+{/if}

+ 109 - 0
examples/svelte-chat/src/lib/render/components/LineChart.svelte

@@ -0,0 +1,109 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+
+  interface Props extends BaseComponentProps<{
+    title?: string | null;
+    data: Array<Record<string, unknown>>;
+    xKey: string;
+    yKey: string;
+    aggregate?: "sum" | "count" | "avg" | null;
+    color?: string | null;
+    height?: number | null;
+  }> {}
+
+  let { props }: Props = $props();
+
+  const rawData = $derived(props.data);
+  const rawItems = $derived<Array<Record<string, unknown>>>(
+    Array.isArray(rawData)
+      ? rawData
+      : Array.isArray((rawData as Record<string, unknown>)?.data)
+        ? ((rawData as Record<string, unknown>).data as Array<Record<string, unknown>>)
+        : []
+  );
+
+  const processedData = $derived(() => {
+    if (rawItems.length === 0) return [];
+    
+    const { xKey, yKey, aggregate } = props;
+    
+    if (!aggregate) {
+      return rawItems.map((item) => ({
+        label: String(item[xKey] ?? ""),
+        value: typeof item[yKey] === "number" ? item[yKey] : parseFloat(String(item[yKey])) || 0,
+      }));
+    }
+
+    const groups = new Map<string, Array<Record<string, unknown>>>();
+    for (const item of rawItems) {
+      const groupKey = String(item[xKey] ?? "unknown");
+      const group = groups.get(groupKey) ?? [];
+      group.push(item);
+      groups.set(groupKey, group);
+    }
+
+    const aggregated: Array<{ label: string; value: number }> = [];
+    for (const [key, group] of Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
+      let value: number;
+      if (aggregate === "count") {
+        value = group.length;
+      } else if (aggregate === "sum") {
+        value = group.reduce((sum, item) => {
+          const v = item[yKey];
+          return sum + (typeof v === "number" ? v : parseFloat(String(v)) || 0);
+        }, 0);
+      } else {
+        const sum = group.reduce((s, item) => {
+          const v = item[yKey];
+          return s + (typeof v === "number" ? v : parseFloat(String(v)) || 0);
+        }, 0);
+        value = group.length > 0 ? sum / group.length : 0;
+      }
+      aggregated.push({ label: key, value });
+    }
+    return aggregated;
+  });
+
+  const chartData = $derived(processedData());
+  const maxValue = $derived(Math.max(...chartData.map(d => d.value), 1));
+  const minValue = $derived(Math.min(...chartData.map(d => d.value), 0));
+  const chartColor = $derived(props.color ?? "var(--chart-1)");
+  const height = $derived(props.height ?? 200);
+  
+  const points = $derived(() => {
+    if (chartData.length === 0) return "";
+    const range = maxValue - minValue || 1;
+    return chartData.map((d, i) => {
+      const x = (i / (chartData.length - 1 || 1)) * 100;
+      const y = 100 - ((d.value - minValue) / range) * 100;
+      return `${x},${y}`;
+    }).join(" ");
+  });
+</script>
+
+<div class="w-full">
+  {#if props.title}
+    <p class="text-sm font-medium mb-2">{props.title}</p>
+  {/if}
+  
+  {#if chartData.length === 0}
+    <div class="text-center py-4 text-muted-foreground">No data available</div>
+  {:else}
+    <div class="relative" style="height: {height}px">
+      <svg viewBox="0 0 100 100" preserveAspectRatio="none" class="w-full h-full">
+        <polyline
+          points={points()}
+          fill="none"
+          stroke={chartColor}
+          stroke-width="2"
+          vector-effect="non-scaling-stroke"
+        />
+      </svg>
+      <div class="flex justify-between text-xs text-muted-foreground mt-1">
+        {#each chartData.filter((_, i) => i === 0 || i === chartData.length - 1 || i === Math.floor(chartData.length / 2)) as item}
+          <span>{item.label}</span>
+        {/each}
+      </div>
+    </div>
+  {/if}
+</div>

+ 19 - 0
examples/svelte-chat/src/lib/render/components/Link.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+
+  interface Props extends BaseComponentProps<{
+    text: string;
+    href: string;
+  }> {}
+
+  let { props }: Props = $props();
+</script>
+
+<a
+  href={props.href}
+  target="_blank"
+  rel="noopener noreferrer"
+  class="text-primary underline underline-offset-4 hover:text-primary/80"
+>
+  {props.text}
+</a>

+ 40 - 0
examples/svelte-chat/src/lib/render/components/Metric.svelte

@@ -0,0 +1,40 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import { TrendingUp, TrendingDown, Minus } from "lucide-svelte";
+
+  interface Props extends BaseComponentProps<{
+    label: string;
+    value: string;
+    detail?: string | null;
+    trend?: "up" | "down" | "neutral" | null;
+  }> {}
+
+  let { props }: Props = $props();
+
+  const trendColor = $derived(
+    props.trend === "up"
+      ? "text-green-500"
+      : props.trend === "down"
+        ? "text-red-500"
+        : "text-muted-foreground"
+  );
+</script>
+
+<div class="flex flex-col gap-1">
+  <p class="text-sm text-muted-foreground">{props.label}</p>
+  <div class="flex items-center gap-2">
+    <span class="text-2xl font-bold">{props.value}</span>
+    {#if props.trend}
+      {#if props.trend === "up"}
+        <TrendingUp class="h-4 w-4 {trendColor}" />
+      {:else if props.trend === "down"}
+        <TrendingDown class="h-4 w-4 {trendColor}" />
+      {:else}
+        <Minus class="h-4 w-4 {trendColor}" />
+      {/if}
+    {/if}
+  </div>
+  {#if props.detail}
+    <p class="text-xs text-muted-foreground">{props.detail}</p>
+  {/if}
+</div>

+ 106 - 0
examples/svelte-chat/src/lib/render/components/PieChart.svelte

@@ -0,0 +1,106 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+
+  interface Props extends BaseComponentProps<{
+    title?: string | null;
+    data: Array<Record<string, unknown>>;
+    nameKey: string;
+    valueKey: string;
+    height?: number | null;
+  }> {}
+
+  let { props }: Props = $props();
+
+  const PIE_COLORS = [
+    "var(--chart-1)",
+    "var(--chart-2)",
+    "var(--chart-3)",
+    "var(--chart-4)",
+    "var(--chart-5)",
+  ];
+
+  const rawData = $derived(props.data);
+  const items = $derived<Array<Record<string, unknown>>>(
+    Array.isArray(rawData)
+      ? rawData
+      : Array.isArray((rawData as Record<string, unknown>)?.data)
+        ? ((rawData as Record<string, unknown>).data as Array<Record<string, unknown>>)
+        : []
+  );
+
+  const chartData = $derived(
+    items.map((item, i) => ({
+      name: String(item[props.nameKey] ?? `Segment ${i + 1}`),
+      value: typeof item[props.valueKey] === "number"
+        ? item[props.valueKey] as number
+        : parseFloat(String(item[props.valueKey])) || 0,
+      color: PIE_COLORS[i % PIE_COLORS.length],
+    }))
+  );
+
+  const total = $derived(chartData.reduce((sum, d) => sum + d.value, 0));
+
+  const segments = $derived(() => {
+    let startAngle = 0;
+    return chartData.map((d) => {
+      const angle = (d.value / (total || 1)) * 360;
+      const segment = {
+        ...d,
+        startAngle,
+        endAngle: startAngle + angle,
+        percentage: Math.round((d.value / (total || 1)) * 100),
+      };
+      startAngle += angle;
+      return segment;
+    });
+  });
+
+  function polarToCartesian(cx: number, cy: number, r: number, angle: number) {
+    const rad = ((angle - 90) * Math.PI) / 180;
+    return {
+      x: cx + r * Math.cos(rad),
+      y: cy + r * Math.sin(rad),
+    };
+  }
+
+  function describeArc(cx: number, cy: number, r: number, startAngle: number, endAngle: number) {
+    const start = polarToCartesian(cx, cy, r, endAngle);
+    const end = polarToCartesian(cx, cy, r, startAngle);
+    const largeArc = endAngle - startAngle <= 180 ? 0 : 1;
+    return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 0 ${end.x} ${end.y}`;
+  }
+</script>
+
+<div class="w-full">
+  {#if props.title}
+    <p class="text-sm font-medium mb-2">{props.title}</p>
+  {/if}
+  
+  {#if items.length === 0}
+    <div class="text-center py-4 text-muted-foreground">No data available</div>
+  {:else}
+    <div class="flex items-center gap-4" style="height: {props.height ?? 200}px">
+      <svg viewBox="0 0 100 100" class="h-full aspect-square">
+        {#each segments() as seg}
+          {#if seg.endAngle - seg.startAngle >= 1}
+            <path
+              d={describeArc(50, 50, 35, seg.startAngle, seg.endAngle)}
+              fill="none"
+              stroke={seg.color}
+              stroke-width="15"
+            />
+          {/if}
+        {/each}
+      </svg>
+      <div class="flex flex-col gap-1 text-sm">
+        {#each segments() as seg}
+          <div class="flex items-center gap-2">
+            <span class="w-3 h-3 rounded-full" style="background-color: {seg.color}"></span>
+            <span class="text-muted-foreground">{seg.name}</span>
+            <span class="font-medium">{seg.percentage}%</span>
+          </div>
+        {/each}
+      </div>
+    </div>
+  {/if}
+</div>

+ 13 - 0
examples/svelte-chat/src/lib/render/components/Progress.svelte

@@ -0,0 +1,13 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import { Progress } from "$lib/components/ui/progress";
+
+  interface Props extends BaseComponentProps<{
+    value: number;
+    max?: number | null;
+  }> {}
+
+  let { props }: Props = $props();
+</script>
+
+<Progress value={props.value} max={props.max ?? 100} />

+ 45 - 0
examples/svelte-chat/src/lib/render/components/RadioGroup.svelte

@@ -0,0 +1,45 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import { getBoundProp } from "@json-render/svelte";
+  import * as RadioGroup from "$lib/components/ui/radio-group";
+  import { Label } from "$lib/components/ui/label";
+
+  interface Props extends BaseComponentProps<{
+    label?: string | null;
+    value?: string | null;
+    options: Array<{ value: string; label: string }>;
+  }> {}
+
+  let { props, bindings }: Props = $props();
+
+  function valueBinding() {
+    return getBoundProp<string>(
+      () => (props.value ?? undefined) as string | undefined,
+      () => bindings?.value,
+    );
+  }
+
+  let value = $derived(
+    valueBinding().current ?? ""
+  );
+
+  function handleChange(newValue: string) {
+    valueBinding().current = newValue;
+  }
+</script>
+
+<div class="flex flex-col gap-2">
+  {#if props.label}
+    <Label class="text-sm font-medium">{props.label}</Label>
+  {/if}
+  <RadioGroup.Root value={value} onValueChange={handleChange}>
+    {#each props.options ?? [] as opt}
+      <div class="flex items-center gap-2">
+        <RadioGroup.Item value={opt.value} id="rg-{opt.value}" />
+        <Label for="rg-{opt.value}" class="font-normal cursor-pointer">
+          {opt.label}
+        </Label>
+      </div>
+    {/each}
+  </RadioGroup.Root>
+</div>

+ 56 - 0
examples/svelte-chat/src/lib/render/components/SelectInput.svelte

@@ -0,0 +1,56 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import { getBoundProp } from "@json-render/svelte";
+  import * as Select from "$lib/components/ui/select";
+  import { Label } from "$lib/components/ui/label";
+
+  interface Props extends BaseComponentProps<{
+    label?: string | null;
+    value?: string | null;
+    placeholder?: string | null;
+    options: Array<{ value: string; label: string }>;
+  }> {}
+
+  let { props, bindings }: Props = $props();
+
+  function valueBinding() {
+    return getBoundProp<string>(
+      () => (props.value ?? undefined) as string | undefined,
+      () => bindings?.value,
+    );
+  }
+
+  let value = $derived(
+    valueBinding().current ?? ""
+  );
+
+  const selectedOption = $derived(
+    props.options?.find(o => o.value === value)
+  );
+
+  function handleChange(newValue: string | undefined) {
+    if (newValue) {
+      valueBinding().current = newValue;
+    }
+  }
+</script>
+
+<div class="flex flex-col gap-2">
+  {#if props.label}
+    <Label class="text-sm font-medium">{props.label}</Label>
+  {/if}
+  <Select.Root type="single" value={value} onValueChange={handleChange}>
+    <Select.Trigger>
+      {#if selectedOption}
+        {selectedOption.label}
+      {:else}
+        <span class="text-muted-foreground">{props.placeholder ?? "Select..."}</span>
+      {/if}
+    </Select.Trigger>
+    <Select.Content>
+      {#each props.options ?? [] as opt}
+        <Select.Item value={opt.value}>{opt.label}</Select.Item>
+      {/each}
+    </Select.Content>
+  </Select.Root>
+</div>

+ 10 - 0
examples/svelte-chat/src/lib/render/components/Separator.svelte

@@ -0,0 +1,10 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import { Separator } from "$lib/components/ui/separator";
+
+  interface Props extends BaseComponentProps<Record<string, never>> {}
+
+  let {}: Props = $props();
+</script>
+
+<Separator />

+ 14 - 0
examples/svelte-chat/src/lib/render/components/Skeleton.svelte

@@ -0,0 +1,14 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+
+  interface Props extends BaseComponentProps<{
+    width?: string | null;
+    height?: string | null;
+  }> {}
+
+  let { props }: Props = $props();
+</script>
+
+<div 
+  class="animate-pulse rounded-md bg-muted {props.width ?? 'w-full'} {props.height ?? 'h-4'}"
+></div>

+ 28 - 0
examples/svelte-chat/src/lib/render/components/Stack.svelte

@@ -0,0 +1,28 @@
+<script lang="ts">
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+
+  interface Props extends BaseComponentProps<{
+    direction?: "horizontal" | "vertical" | null;
+    gap?: "sm" | "md" | "lg" | null;
+    wrap?: boolean | null;
+  }> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  const gapClass = $derived(
+    { sm: "gap-2", md: "gap-4", lg: "gap-6" }[props.gap ?? "md"] ?? "gap-4"
+  );
+  const dirClass = $derived(
+    props.direction === "horizontal" ? "flex-row" : "flex-col"
+  );
+  const wrapClass = $derived(props.wrap ? "flex-wrap" : "");
+</script>
+
+<div class="flex {dirClass} {wrapClass} {gapClass}">
+  {#if children}
+    {@render children()}
+  {/if}
+</div>

+ 19 - 0
examples/svelte-chat/src/lib/render/components/TabContent.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import * as Tabs from "$lib/components/ui/tabs";
+
+  interface Props extends BaseComponentProps<{
+    value: string;
+  }> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+</script>
+
+<Tabs.Content value={props.value}>
+  {#if children}
+    {@render children()}
+  {/if}
+</Tabs.Content>

+ 91 - 0
examples/svelte-chat/src/lib/render/components/Table.svelte

@@ -0,0 +1,91 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import * as Table from "$lib/components/ui/table";
+  import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-svelte";
+
+  interface Props extends BaseComponentProps<{
+    data: Array<Record<string, unknown>>;
+    columns: Array<{ key: string; label: string }>;
+    emptyMessage?: string | null;
+  }> {}
+
+  let { props }: Props = $props();
+
+  let sortKey = $state<string | null>(null);
+  let sortDir = $state<"asc" | "desc">("asc");
+
+  const rawData = $derived(props.data);
+  const items = $derived<Array<Record<string, unknown>>>(
+    Array.isArray(rawData)
+      ? rawData
+      : Array.isArray((rawData as Record<string, unknown>)?.data)
+        ? ((rawData as Record<string, unknown>).data as Array<Record<string, unknown>>)
+        : []
+  );
+
+  const sorted = $derived(
+    sortKey
+      ? [...items].sort((a, b) => {
+          const av = a[sortKey!];
+          const bv = b[sortKey!];
+          if (typeof av === "number" && typeof bv === "number") {
+            return sortDir === "asc" ? av - bv : bv - av;
+          }
+          const as = String(av ?? "");
+          const bs = String(bv ?? "");
+          return sortDir === "asc" ? as.localeCompare(bs) : bs.localeCompare(as);
+        })
+      : items
+  );
+
+  function handleSort(key: string) {
+    if (sortKey === key) {
+      sortDir = sortDir === "asc" ? "desc" : "asc";
+    } else {
+      sortKey = key;
+      sortDir = "asc";
+    }
+  }
+</script>
+
+{#if items.length === 0}
+  <div class="text-center py-4 text-muted-foreground">
+    {props.emptyMessage ?? "No data"}
+  </div>
+{:else}
+  <Table.Root>
+    <Table.Header>
+      <Table.Row>
+        {#each props.columns as col}
+          <Table.Head>
+            <button
+              type="button"
+              class="inline-flex items-center gap-1 hover:text-foreground transition-colors"
+              onclick={() => handleSort(col.key)}
+            >
+              {col.label}
+              {#if sortKey === col.key}
+                {#if sortDir === "asc"}
+                  <ArrowUp class="h-3 w-3 text-muted-foreground" />
+                {:else}
+                  <ArrowDown class="h-3 w-3 text-muted-foreground" />
+                {/if}
+              {:else}
+                <ArrowUpDown class="h-3 w-3 text-muted-foreground" />
+              {/if}
+            </button>
+          </Table.Head>
+        {/each}
+      </Table.Row>
+    </Table.Header>
+    <Table.Body>
+        {#each sorted as item, i}
+        <Table.Row>
+          {#each props.columns as col}
+            <Table.Cell>{String(item[col.key] ?? "")}</Table.Cell>
+          {/each}
+        </Table.Row>
+      {/each}
+    </Table.Body>
+  </Table.Root>
+{/if}

+ 29 - 0
examples/svelte-chat/src/lib/render/components/Tabs.svelte

@@ -0,0 +1,29 @@
+<script lang="ts">
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import * as Tabs from "$lib/components/ui/tabs";
+
+  interface Props extends BaseComponentProps<{
+    defaultValue?: string | null;
+    tabs: Array<{ value: string; label: string }>;
+  }> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  const defaultVal = $derived(
+    props.defaultValue ?? (props.tabs ?? [])[0]?.value ?? ""
+  );
+</script>
+
+<Tabs.Root value={defaultVal}>
+  <Tabs.List>
+    {#each props.tabs ?? [] as tab}
+      <Tabs.Trigger value={tab.value}>{tab.label}</Tabs.Trigger>
+    {/each}
+  </Tabs.List>
+  {#if children}
+    {@render children()}
+  {/if}
+</Tabs.Root>

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно