mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 12:28:17 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
512f7fe5c5 |
@@ -27,6 +27,7 @@ out/
|
|||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
|
|||||||
@@ -462,8 +462,9 @@ pnpm dev
|
|||||||
- http://react-email-demo.json-render.localhost:1355 - React Email Example
|
- http://react-email-demo.json-render.localhost:1355 - React Email Example
|
||||||
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
|
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
|
||||||
- Chat Example: run `pnpm dev` in `examples/chat`
|
- Chat Example: run `pnpm dev` in `examples/chat`
|
||||||
|
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
|
||||||
- Vue Example: run `pnpm dev` in `examples/vue`
|
- Vue Example: run `pnpm dev` in `examples/vue`
|
||||||
- Vite Renderers (React + Vue): run `pnpm dev` in `examples/vite-renderers`
|
- Vite Renderers (React + Vue + Svelte): run `pnpm dev` in `examples/vite-renderers`
|
||||||
- React Native example: run `npx expo start` in `examples/react-native`
|
- React Native example: run `npx expo start` in `examples/react-native`
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -21,6 +21,14 @@ Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.
|
|||||||
|
|
||||||
<PackageInstall packages="vue zod" />
|
<PackageInstall packages="vue zod" />
|
||||||
|
|
||||||
|
## For Svelte
|
||||||
|
|
||||||
|
<PackageInstall packages="@json-render/core @json-render/svelte" />
|
||||||
|
|
||||||
|
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
|
||||||
|
|
||||||
|
<PackageInstall packages="svelte zod" />
|
||||||
|
|
||||||
## For React UI with shadcn/ui
|
## For React UI with shadcn/ui
|
||||||
|
|
||||||
Pre-built components for fast prototyping and production use:
|
Pre-built components for fast prototyping and production use:
|
||||||
|
|||||||
@@ -80,13 +80,18 @@ export const docsNavigation: NavSection[] = [
|
|||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Svelte",
|
||||||
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/svelte",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Vue",
|
title: "Vue",
|
||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Renders with Vite (Vue / React)",
|
title: "Renders with Vite (Vue / React / Svelte)",
|
||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
@@ -121,6 +126,7 @@ export const docsNavigation: NavSection[] = [
|
|||||||
{ title: "@json-render/image", href: "/docs/api/image" },
|
{ title: "@json-render/image", href: "/docs/api/image" },
|
||||||
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
||||||
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
||||||
|
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
|
||||||
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
|
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/api/react-pdf": "@json-render/react-pdf API",
|
"docs/api/react-pdf": "@json-render/react-pdf API",
|
||||||
"docs/api/react-email": "@json-render/react-email API",
|
"docs/api/react-email": "@json-render/react-email API",
|
||||||
"docs/api/react-native": "@json-render/react-native API",
|
"docs/api/react-native": "@json-render/react-native API",
|
||||||
|
"docs/api/svelte": "@json-render/svelte API",
|
||||||
"docs/api/codegen": "@json-render/codegen API",
|
"docs/api/codegen": "@json-render/codegen API",
|
||||||
"docs/api/image": "@json-render/image API",
|
"docs/api/image": "@json-render/image API",
|
||||||
"docs/api/remotion": "@json-render/remotion API",
|
"docs/api/remotion": "@json-render/remotion API",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"react-resizable-panels": "^4.4.1",
|
"react-resizable-panels": "^4.4.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "4.3.5"
|
"zod": "4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@internal/typescript-config": "workspace:*",
|
"@internal/typescript-config": "workspace:*",
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# create a new project
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
To recreate this project with the same configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# recreate this project
|
||||||
|
npx sv create --template minimal --types ts --no-install svelte-chat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "zinc"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "svelte-chat",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
--radius-3xl: calc(var(--radius) + 12px);
|
||||||
|
--radius-4xl: calc(var(--radius) + 16px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
currentColor 25%,
|
||||||
|
hsl(0 0% 64%) 50%,
|
||||||
|
currentColor 75%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: shimmer 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { ToolLoopAgent, stepCountIs } from "ai";
|
||||||
|
import { createGateway } from "@ai-sdk/gateway";
|
||||||
|
import { explorerCatalog } from "./render/catalog";
|
||||||
|
import { getWeather } from "./tools/weather";
|
||||||
|
import { getGitHubRepo, getGitHubPullRequests } from "./tools/github";
|
||||||
|
import { getCryptoPrice, getCryptoPriceHistory } from "./tools/crypto";
|
||||||
|
import { getHackerNewsTop } from "./tools/hackernews";
|
||||||
|
import { webSearch } from "./tools/search";
|
||||||
|
import { env } from "$env/dynamic/private";
|
||||||
|
|
||||||
|
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
|
||||||
|
|
||||||
|
const AGENT_INSTRUCTIONS = `You are a knowledgeable assistant that helps users explore data and learn about any topic. You look up real-time information, build visual dashboards, and create rich educational content.
|
||||||
|
|
||||||
|
WORKFLOW:
|
||||||
|
1. Call the appropriate tools to gather relevant data. Use webSearch for general topics not covered by specialized tools.
|
||||||
|
2. Respond with a brief, conversational summary of what you found.
|
||||||
|
3. Then output the JSONL UI spec wrapped in a \`\`\`spec fence to render a rich visual experience.
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Always call tools FIRST to get real data. Never make up data.
|
||||||
|
- Embed the fetched data directly in /state paths so components can reference it.
|
||||||
|
- Use Card components to group related information.
|
||||||
|
- NEVER nest a Card inside another Card. If you need sub-sections inside a Card, use Stack, Separator, Heading, or Accordion instead.
|
||||||
|
- Use Grid for multi-column layouts.
|
||||||
|
- Use Metric for key numeric values (temperature, stars, price, etc.).
|
||||||
|
- Use Table for lists of items (stories, forecasts, languages, etc.).
|
||||||
|
- Use BarChart or LineChart for numeric trends and time-series data.
|
||||||
|
- Use PieChart for compositional/proportional data (market share, breakdowns, distributions).
|
||||||
|
- Use Tabs when showing multiple categories of data side by side.
|
||||||
|
- Use Badge for status indicators.
|
||||||
|
- Use Callout for key facts, tips, warnings, or important takeaways.
|
||||||
|
- Use Accordion to organize detailed sections the user can expand for deeper reading.
|
||||||
|
- Use Timeline for historical events, processes, step-by-step explanations, or milestones.
|
||||||
|
- When teaching about a topic, combine multiple component types to create a rich, engaging experience.
|
||||||
|
|
||||||
|
DATA BINDING:
|
||||||
|
- The state model is the single source of truth. Put fetched data in /state, then reference it with { "$state": "/json/pointer" } in any prop.
|
||||||
|
- $state works on ANY prop at ANY nesting level. The renderer resolves expressions before components receive props.
|
||||||
|
- Scalar binding: "title": { "$state": "/quiz/title" }
|
||||||
|
- Array binding: "items": { "$state": "/quiz/questions" } (for Accordion, Timeline, etc.)
|
||||||
|
- For Table, BarChart, LineChart, and PieChart, use { "$state": "/path" } on the data prop to bind read-only data from state.
|
||||||
|
- Always emit /state patches BEFORE the elements that reference them, so data is available when the UI renders.
|
||||||
|
- Always use the { "$state": "/foo" } object syntax for data binding.
|
||||||
|
|
||||||
|
INTERACTIVITY:
|
||||||
|
- You can use visible, repeat, on.press, and $cond/$then/$else freely.
|
||||||
|
- visible: Conditionally show/hide elements based on state. e.g. "visible": { "$state": "/q1/answer", "eq": "a" }
|
||||||
|
- repeat: Iterate over state arrays. e.g. "repeat": { "statePath": "/items" }
|
||||||
|
- on.press: Trigger actions on button clicks. e.g. "on": { "press": { "action": "setState", "params": { "statePath": "/submitted", "value": true } } }
|
||||||
|
- $cond/$then/$else: Conditional prop values. e.g. { "$cond": { "$state": "/correct" }, "$then": "Correct!", "$else": "Try again" }
|
||||||
|
|
||||||
|
BUILT-IN ACTIONS (use with on.press):
|
||||||
|
- setState: Set a value at a state path. params: { statePath: "/foo", value: "bar" }
|
||||||
|
- pushState: Append to an array. params: { statePath: "/items", value: { ... } }
|
||||||
|
- removeState: Remove by index. params: { statePath: "/items", index: 0 }
|
||||||
|
|
||||||
|
INPUT COMPONENTS:
|
||||||
|
- RadioGroup: Renders radio buttons. Writes selected value to statePath automatically.
|
||||||
|
- SelectInput: Dropdown select. Writes selected value to statePath automatically.
|
||||||
|
- TextInput: Text input field. Writes entered value to statePath automatically.
|
||||||
|
- Button: Clickable button. Use on.press to trigger actions.
|
||||||
|
|
||||||
|
${explorerCatalog.prompt({
|
||||||
|
mode: "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,
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="accordion-content"
|
||||||
|
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div class={cn("pt-0 pb-4", className)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AccordionPrimitive.ItemProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="accordion-item"
|
||||||
|
class={cn("border-b last:border-b-0", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
level = 3,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
|
||||||
|
level?: AccordionPrimitive.HeaderProps["level"];
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Header {level} class="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: AccordionPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:value={value as never}
|
||||||
|
data-slot="accordion"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Root from "./accordion.svelte";
|
||||||
|
import Content from "./accordion-content.svelte";
|
||||||
|
import Item from "./accordion-item.svelte";
|
||||||
|
import Trigger from "./accordion-trigger.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Item,
|
||||||
|
Trigger,
|
||||||
|
//
|
||||||
|
Root as Accordion,
|
||||||
|
Content as AccordionContent,
|
||||||
|
Item as AccordionItem,
|
||||||
|
Trigger as AccordionTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-description"
|
||||||
|
class={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-title"
|
||||||
|
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const alertVariants = tv({
|
||||||
|
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
variant?: AlertVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert"
|
||||||
|
class={cn(alertVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Root from "./alert.svelte";
|
||||||
|
import Description from "./alert-description.svelte";
|
||||||
|
import Title from "./alert-title.svelte";
|
||||||
|
export { alertVariants, type AlertVariant } from "./alert.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Description,
|
||||||
|
Title,
|
||||||
|
//
|
||||||
|
Root as Alert,
|
||||||
|
Description as AlertDescription,
|
||||||
|
Title as AlertTitle,
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||||
|
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
|
||||||
|
outline:
|
||||||
|
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = "button",
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? "link" : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from "./button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-action"
|
||||||
|
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-footer"
|
||||||
|
class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-header"
|
||||||
|
class={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-title"
|
||||||
|
class={cn("leading-none font-semibold", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card"
|
||||||
|
class={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Root from "./card.svelte";
|
||||||
|
import Content from "./card-content.svelte";
|
||||||
|
import Description from "./card-description.svelte";
|
||||||
|
import Footer from "./card-footer.svelte";
|
||||||
|
import Header from "./card-header.svelte";
|
||||||
|
import Title from "./card-title.svelte";
|
||||||
|
import Action from "./card-action.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
//
|
||||||
|
Root as Card,
|
||||||
|
Content as CardContent,
|
||||||
|
Description as CardDescription,
|
||||||
|
Footer as CardFooter,
|
||||||
|
Header as CardHeader,
|
||||||
|
Title as CardTitle,
|
||||||
|
Action as CardAction,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||||
|
|
||||||
|
type Props = WithElementRef<
|
||||||
|
Omit<HTMLInputAttributes, "type"> &
|
||||||
|
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||||
|
>;
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
type,
|
||||||
|
files = $bindable(),
|
||||||
|
class: className,
|
||||||
|
"data-slot": dataSlot = "input",
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if type === "file"}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
type="file"
|
||||||
|
bind:files
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{type}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./label.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Label as LabelPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: LabelPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="label"
|
||||||
|
class={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./progress.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Progress,
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Progress as ProgressPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
max = 100,
|
||||||
|
value,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<ProgressPrimitive.RootProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="progress"
|
||||||
|
class={cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className)}
|
||||||
|
{value}
|
||||||
|
{max}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
class="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style="transform: translateX(-{100 - (100 * (value ?? 0)) / (max ?? 1)}%)"
|
||||||
|
></div>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Root from "./radio-group.svelte";
|
||||||
|
import Item from "./radio-group-item.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Item,
|
||||||
|
//
|
||||||
|
Root as RadioGroup,
|
||||||
|
Item as RadioGroupItem,
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||||
|
import CircleIcon from "@lucide/svelte/icons/circle";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
class={cn(
|
||||||
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<div data-slot="radio-group-indicator" class="relative flex items-center justify-center">
|
||||||
|
{#if checked}
|
||||||
|
<CircleIcon
|
||||||
|
class="fill-primary absolute start-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value = $bindable(""),
|
||||||
|
...restProps
|
||||||
|
}: RadioGroupPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="radio-group"
|
||||||
|
class={cn("grid gap-3", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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} />
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Portal {...restProps} />
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
@@ -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} />
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./separator.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Separator,
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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";
|
||||||
@@ -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>
|
||||||
@@ -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: {},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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} />
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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 />
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
@@ -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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user