mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 04:18:15 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c82eefd1c5 | ||
|
|
2d70fab00a | ||
|
|
320c935bfb |
@@ -6,6 +6,7 @@
|
|||||||
[
|
[
|
||||||
"@json-render/core",
|
"@json-render/core",
|
||||||
"@json-render/react",
|
"@json-render/react",
|
||||||
|
"@json-render/shadcn",
|
||||||
"@json-render/react-native",
|
"@json-render/react-native",
|
||||||
"@json-render/remotion",
|
"@json-render/remotion",
|
||||||
"@json-render/codegen"
|
"@json-render/codegen"
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
|
||||||
version: 9.0.0
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
@@ -25,12 +25,16 @@ This ensures we don't install outdated versions that may have incompatible types
|
|||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
- Do not use emojis in code or UI
|
- Do not use emojis in code or UI
|
||||||
- Do not use barrel files (index.ts that re-exports from other files)
|
|
||||||
- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`
|
- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
- Run `pnpm type-check` after each turn to ensure type safety
|
- Run `pnpm type-check` after each turn to ensure type safety
|
||||||
|
- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:
|
||||||
|
- Package `README.md` files in `packages/*/README.md`
|
||||||
|
- Root `README.md` (if packages table, install commands, or examples are affected)
|
||||||
|
- Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)
|
||||||
|
- `AGENTS.md` (if workflow or conventions change)
|
||||||
|
|
||||||
<!-- opensrc:start -->
|
<!-- opensrc:start -->
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ Generate dynamic, personalized UIs from prompts without sacrificing reliability.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @json-render/core @json-render/react
|
npm install @json-render/core @json-render/react
|
||||||
|
# pre-built shadcn/ui components
|
||||||
|
npm install @json-render/shadcn
|
||||||
# or for mobile
|
# or for mobile
|
||||||
npm install @json-render/core @json-render/react-native
|
npm install @json-render/core @json-render/react-native
|
||||||
# or for video
|
# or for video
|
||||||
@@ -20,6 +22,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur
|
|||||||
- **Predictable** - JSON output matches your schema, every time
|
- **Predictable** - JSON output matches your schema, every time
|
||||||
- **Fast** - Stream and render progressively as the model responds
|
- **Fast** - Stream and render progressively as the model responds
|
||||||
- **Cross-Platform** - React (web) and React Native (mobile) from the same catalog
|
- **Cross-Platform** - React (web) and React Native (mobile) from the same catalog
|
||||||
|
- **Batteries Included** - 30+ pre-built shadcn/ui components ready to use
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -105,6 +108,7 @@ function Dashboard({ spec }) {
|
|||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
||||||
| `@json-render/react` | React renderer, contexts, hooks |
|
| `@json-render/react` | React renderer, contexts, hooks |
|
||||||
|
| `@json-render/shadcn` | 30+ pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
||||||
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
||||||
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
||||||
|
|
||||||
@@ -138,6 +142,38 @@ const { registry } = defineRegistry(catalog, { components });
|
|||||||
<Renderer spec={spec} registry={registry} />
|
<Renderer spec={spec} registry={registry} />
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### shadcn/ui (Web)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema, defineRegistry, Renderer } from "@json-render/react";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
|
|
||||||
|
// Pick components from the 30+ standard definitions
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponentDefinitions.Card,
|
||||||
|
Stack: shadcnComponentDefinitions.Stack,
|
||||||
|
Heading: shadcnComponentDefinitions.Heading,
|
||||||
|
Button: shadcnComponentDefinitions.Button,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use matching implementations
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponents.Card,
|
||||||
|
Stack: shadcnComponents.Stack,
|
||||||
|
Heading: shadcnComponents.Heading,
|
||||||
|
Button: shadcnComponents.Button,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
<Renderer spec={spec} registry={registry} />
|
||||||
|
```
|
||||||
|
|
||||||
### React Native (Mobile)
|
### React Native (Mobile)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ const nextConfig = {
|
|||||||
|
|
||||||
const withMDX = createMDX({});
|
const withMDX = createMDX({});
|
||||||
|
|
||||||
export default withMDX(nextConfig);
|
/** @type {import('next').NextConfig} */
|
||||||
|
const config = withMDX(nextConfig);
|
||||||
|
export default config;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineCatalog } from "@json-render/core";
|
import { defineCatalog } from "@json-render/core";
|
||||||
import { schema } from "@json-render/react/schema";
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -45,48 +46,19 @@ const mesh3DProps = {
|
|||||||
*/
|
*/
|
||||||
export const explorerCatalog = defineCatalog(schema, {
|
export const explorerCatalog = defineCatalog(schema, {
|
||||||
components: {
|
components: {
|
||||||
// Layout
|
// From @json-render/shadcn (used as-is)
|
||||||
Stack: {
|
Stack: shadcnComponentDefinitions.Stack,
|
||||||
props: z.object({
|
Card: shadcnComponentDefinitions.Card,
|
||||||
direction: z.enum(["horizontal", "vertical"]).nullable(),
|
Grid: shadcnComponentDefinitions.Grid,
|
||||||
gap: z.enum(["sm", "md", "lg"]).nullable(),
|
Heading: shadcnComponentDefinitions.Heading,
|
||||||
wrap: z.boolean().nullable(),
|
Separator: shadcnComponentDefinitions.Separator,
|
||||||
}),
|
Accordion: shadcnComponentDefinitions.Accordion,
|
||||||
slots: ["default"],
|
Progress: shadcnComponentDefinitions.Progress,
|
||||||
description: "Flex layout container",
|
Skeleton: shadcnComponentDefinitions.Skeleton,
|
||||||
example: { direction: "vertical", gap: "md", wrap: null },
|
Badge: shadcnComponentDefinitions.Badge,
|
||||||
},
|
Alert: shadcnComponentDefinitions.Alert,
|
||||||
|
|
||||||
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" },
|
|
||||||
},
|
|
||||||
|
|
||||||
|
// Chat-specific components (different schemas or fully custom)
|
||||||
Text: {
|
Text: {
|
||||||
props: z.object({
|
props: z.object({
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
@@ -96,32 +68,6 @@ export const explorerCatalog = defineCatalog(schema, {
|
|||||||
example: { content: "Here is your data overview." },
|
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: {
|
Metric: {
|
||||||
props: z.object({
|
props: z.object({
|
||||||
label: z.string(),
|
label: z.string(),
|
||||||
@@ -222,22 +168,6 @@ export const explorerCatalog = defineCatalog(schema, {
|
|||||||
description: "Content for a specific tab",
|
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
|
// Educational / Rich content
|
||||||
Callout: {
|
Callout: {
|
||||||
props: z.object({
|
props: z.object({
|
||||||
@@ -254,24 +184,6 @@ export const explorerCatalog = defineCatalog(schema, {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
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: {
|
Timeline: {
|
||||||
props: z.object({
|
props: z.object({
|
||||||
items: z.array(
|
items: z.array(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useRef, type ReactNode } from "react";
|
import { useState, useRef, type ReactNode } from "react";
|
||||||
import { useBoundProp, defineRegistry } from "@json-render/react";
|
import { useBoundProp, defineRegistry } from "@json-render/react";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
BarChart as RechartsBarChart,
|
BarChart as RechartsBarChart,
|
||||||
@@ -20,16 +21,6 @@ import {
|
|||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -39,14 +30,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||||
import {
|
|
||||||
Accordion as AccordionRoot,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
AccordionContent,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -218,77 +201,25 @@ function AnimatedGroup({
|
|||||||
|
|
||||||
export const { registry, handlers } = defineRegistry(explorerCatalog, {
|
export const { registry, handlers } = defineRegistry(explorerCatalog, {
|
||||||
components: {
|
components: {
|
||||||
Stack: ({ props, children }) => {
|
// From @json-render/shadcn (used as-is)
|
||||||
const gapClass =
|
Stack: shadcnComponents.Stack,
|
||||||
{ sm: "gap-2", md: "gap-4", lg: "gap-6" }[props.gap ?? "md"] ?? "gap-4";
|
Card: shadcnComponents.Card,
|
||||||
return (
|
Grid: shadcnComponents.Grid,
|
||||||
<div
|
Heading: shadcnComponents.Heading,
|
||||||
className={`flex ${props.direction === "horizontal" ? "flex-row" : "flex-col"} ${props.wrap ? "flex-wrap" : ""} ${gapClass}`}
|
Separator: shadcnComponents.Separator,
|
||||||
>
|
Accordion: shadcnComponents.Accordion,
|
||||||
{children}
|
Progress: shadcnComponents.Progress,
|
||||||
</div>
|
Skeleton: shadcnComponents.Skeleton,
|
||||||
);
|
Badge: shadcnComponents.Badge,
|
||||||
},
|
Alert: shadcnComponents.Alert,
|
||||||
|
|
||||||
Card: ({ props, children }) => (
|
|
||||||
<Card>
|
|
||||||
{(props.title || props.description) && (
|
|
||||||
<CardHeader>
|
|
||||||
{props.title && <CardTitle>{props.title}</CardTitle>}
|
|
||||||
{props.description && (
|
|
||||||
<CardDescription>{props.description}</CardDescription>
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
|
||||||
)}
|
|
||||||
<CardContent className="flex flex-col gap-4">{children}</CardContent>
|
|
||||||
</Card>
|
|
||||||
),
|
|
||||||
|
|
||||||
Grid: ({ props, children }) => {
|
|
||||||
const colsClass =
|
|
||||||
{
|
|
||||||
"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 =
|
|
||||||
{ sm: "gap-2", md: "gap-4", lg: "gap-6" }[props.gap ?? "md"] ?? "gap-4";
|
|
||||||
return <div className={`grid ${colsClass} ${gapClass}`}>{children}</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
Heading: ({ props }) => {
|
|
||||||
const Tag = (props.level ?? "h2") as "h1" | "h2" | "h3" | "h4";
|
|
||||||
const sizeClass = {
|
|
||||||
h1: "text-3xl font-bold tracking-tight",
|
|
||||||
h2: "text-2xl font-semibold tracking-tight",
|
|
||||||
h3: "text-xl font-semibold",
|
|
||||||
h4: "text-lg font-medium",
|
|
||||||
}[props.level ?? "h2"];
|
|
||||||
return <Tag className={sizeClass}>{props.text}</Tag>;
|
|
||||||
},
|
|
||||||
|
|
||||||
|
// Chat-specific components
|
||||||
Text: ({ props }) => (
|
Text: ({ props }) => (
|
||||||
<p className={props.muted ? "text-muted-foreground" : ""}>
|
<p className={props.muted ? "text-muted-foreground" : ""}>
|
||||||
{props.content}
|
{props.content}
|
||||||
</p>
|
</p>
|
||||||
),
|
),
|
||||||
|
|
||||||
Badge: ({ props }) => (
|
|
||||||
<Badge variant={props.variant ?? "default"}>{props.text}</Badge>
|
|
||||||
),
|
|
||||||
|
|
||||||
Alert: ({ props }) => (
|
|
||||||
<Alert variant={props.variant ?? "default"}>
|
|
||||||
<AlertTitle>{props.title}</AlertTitle>
|
|
||||||
{props.description ? (
|
|
||||||
<AlertDescription>{props.description}</AlertDescription>
|
|
||||||
) : null}
|
|
||||||
</Alert>
|
|
||||||
),
|
|
||||||
|
|
||||||
Separator: () => <Separator />,
|
|
||||||
|
|
||||||
Metric: ({ props }) => {
|
Metric: ({ props }) => {
|
||||||
const TrendIcon =
|
const TrendIcon =
|
||||||
props.trend === "up"
|
props.trend === "up"
|
||||||
@@ -566,16 +497,6 @@ export const { registry, handlers } = defineRegistry(explorerCatalog, {
|
|||||||
<TabsContent value={props.value}>{children}</TabsContent>
|
<TabsContent value={props.value}>{children}</TabsContent>
|
||||||
),
|
),
|
||||||
|
|
||||||
Progress: ({ props }) => (
|
|
||||||
<Progress value={props.value} max={props.max ?? 100} />
|
|
||||||
),
|
|
||||||
|
|
||||||
Skeleton: ({ props }) => (
|
|
||||||
<Skeleton
|
|
||||||
className={`${props.width ?? "w-full"} ${props.height ?? "h-4"}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
|
|
||||||
Callout: ({ props }) => {
|
Callout: ({ props }) => {
|
||||||
const config = {
|
const config = {
|
||||||
info: {
|
info: {
|
||||||
@@ -626,23 +547,6 @@ export const { registry, handlers } = defineRegistry(explorerCatalog, {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
Accordion: ({ props }) => (
|
|
||||||
<AccordionRoot
|
|
||||||
type={props.type === "single" ? "single" : "multiple"}
|
|
||||||
collapsible={props.type === "single" ? true : undefined}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{(props.items ?? []).map((item, i) => (
|
|
||||||
<AccordionItem key={i} value={`item-${i}`}>
|
|
||||||
<AccordionTrigger>{item.title}</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<p className="text-muted-foreground">{item.content}</p>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
))}
|
|
||||||
</AccordionRoot>
|
|
||||||
),
|
|
||||||
|
|
||||||
Timeline: ({ props }) => (
|
Timeline: ({ props }) => (
|
||||||
<div className="relative pl-8">
|
<div className="relative pl-8">
|
||||||
{/* Vertical line centered on dots: dot is 12px wide starting at 0px, center = 6px */}
|
{/* Vertical line centered on dots: dot is 12px wide starting at 0px, center = 6px */}
|
||||||
@@ -987,8 +891,6 @@ export const { registry, handlers } = defineRegistry(explorerCatalog, {
|
|||||||
</DreiText>
|
</DreiText>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@ai-sdk/react": "^3.0.84",
|
"@ai-sdk/react": "^3.0.84",
|
||||||
"@json-render/core": "workspace:*",
|
"@json-render/core": "workspace:*",
|
||||||
"@json-render/react": "workspace:*",
|
"@json-render/react": "workspace:*",
|
||||||
|
"@json-render/shadcn": "workspace:*",
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.5.0",
|
"@react-three/fiber": "^9.5.0",
|
||||||
"@streamdown/code": "^1.0.2",
|
"@streamdown/code": "^1.0.2",
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@
|
|||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"vitest": "^4.0.17"
|
"vitest": "^4.0.17"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.0.0",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/codegen
|
# @json-render/codegen
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [2d70fab]
|
||||||
|
- @json-render/core@0.7.0
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/codegen",
|
"name": "@json-render/codegen",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Utilities for generating code from json-render UI trees",
|
"description": "Utilities for generating code from json-render UI trees",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,50 @@
|
|||||||
# @json-render/core
|
# @json-render/core
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 2d70fab: New `@json-render/shadcn` package, event handles, built-in actions, and stream improvements.
|
||||||
|
|
||||||
|
### New: `@json-render/shadcn` Package
|
||||||
|
|
||||||
|
Pre-built [shadcn/ui](https://ui.shadcn.com/) component library for json-render. 30+ components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
|
||||||
|
- `shadcnComponentDefinitions` — Zod-based catalog definitions for all components (server-safe, no React dependency via `@json-render/shadcn/catalog`)
|
||||||
|
- `shadcnComponents` — React implementations for all components
|
||||||
|
- Layout: Card, Stack, Grid, Separator
|
||||||
|
- Navigation: Tabs, Accordion, Collapsible, Pagination
|
||||||
|
- Overlay: Dialog, Drawer, Tooltip, Popover, DropdownMenu
|
||||||
|
- Content: Heading, Text, Image, Avatar, Badge, Alert, Carousel, Table
|
||||||
|
- Feedback: Progress, Skeleton, Spinner
|
||||||
|
- Input: Button, Link, Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Toggle, ToggleGroup, ButtonGroup
|
||||||
|
|
||||||
|
### New: Event Handles (`on()`)
|
||||||
|
|
||||||
|
Components now receive an `on(event)` function in addition to `emit(event)`. The `on()` function returns an `EventHandle` with metadata:
|
||||||
|
- `emit()` — fire the event
|
||||||
|
- `shouldPreventDefault` — whether any action binding requested `preventDefault`
|
||||||
|
- `bound` — whether any handler is bound to this event
|
||||||
|
|
||||||
|
### New: `BaseComponentProps`
|
||||||
|
|
||||||
|
Catalog-agnostic base type for component render functions. Use when building reusable component libraries (like `@json-render/shadcn`) that are not tied to a specific catalog.
|
||||||
|
|
||||||
|
### New: Built-in Actions in Schema
|
||||||
|
|
||||||
|
Schemas can now declare `builtInActions` — actions that are always available at runtime and automatically injected into prompts. The React schema declares `setState`, `pushState`, and `removeState` as built-in, so they appear in prompts without needing to be listed in catalog `actions`.
|
||||||
|
|
||||||
|
### New: `preventDefault` on `ActionBinding`
|
||||||
|
|
||||||
|
Action bindings now support a `preventDefault` boolean field, allowing the LLM to request that default browser behavior (e.g. navigation on links) be prevented.
|
||||||
|
|
||||||
|
### Improved: Stream Transform Text Block Splitting
|
||||||
|
|
||||||
|
`createJsonRenderTransform()` now properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs. This ensures the AI SDK creates separate text parts, preserving correct interleaving of prose and UI in `message.parts`.
|
||||||
|
|
||||||
|
### Improved: `defineRegistry` Actions Requirement
|
||||||
|
|
||||||
|
`defineRegistry` now conditionally requires the `actions` field only when the catalog declares actions. Catalogs with no actions (e.g. `actions: {}`) no longer need to pass an empty actions object.
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/core",
|
"name": "@json-render/core",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export interface ActionBinding {
|
|||||||
onSuccess?: ActionOnSuccess;
|
onSuccess?: ActionOnSuccess;
|
||||||
/** Handler after failed execution */
|
/** Handler after failed execution */
|
||||||
onError?: ActionOnError;
|
onError?: ActionOnError;
|
||||||
|
/** Whether to prevent default browser behavior (e.g. navigation on links) */
|
||||||
|
preventDefault?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,6 +93,7 @@ export const ActionBindingSchema = z.object({
|
|||||||
confirm: ActionConfirmSchema.optional(),
|
confirm: ActionConfirmSchema.optional(),
|
||||||
onSuccess: ActionOnSuccessSchema.optional(),
|
onSuccess: ActionOnSuccessSchema.optional(),
|
||||||
onError: ActionOnErrorSchema.optional(),
|
onError: ActionOnErrorSchema.optional(),
|
||||||
|
preventDefault: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export type {
|
|||||||
Schema,
|
Schema,
|
||||||
PromptTemplate,
|
PromptTemplate,
|
||||||
SchemaOptions,
|
SchemaOptions,
|
||||||
|
BuiltInAction,
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
|
|
||||||
export { defineSchema } from "./schema";
|
export { defineSchema } from "./schema";
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export interface Schema<TDef extends SchemaDefinition = SchemaDefinition> {
|
|||||||
readonly promptTemplate?: PromptTemplate;
|
readonly promptTemplate?: PromptTemplate;
|
||||||
/** Default rules baked into the schema (injected before customRules) */
|
/** Default rules baked into the schema (injected before customRules) */
|
||||||
readonly defaultRules?: string[];
|
readonly defaultRules?: string[];
|
||||||
|
/** Built-in actions always available at runtime (injected into prompts automatically) */
|
||||||
|
readonly builtInActions?: BuiltInAction[];
|
||||||
/** Create a catalog from this schema */
|
/** Create a catalog from this schema */
|
||||||
createCatalog<TCatalog extends InferCatalogInput<TDef["catalog"]>>(
|
createCatalog<TCatalog extends InferCatalogInput<TDef["catalog"]>>(
|
||||||
catalog: TCatalog,
|
catalog: TCatalog,
|
||||||
@@ -140,6 +142,18 @@ export type PromptTemplate<TCatalog = unknown> = (
|
|||||||
context: PromptContext<TCatalog>,
|
context: PromptContext<TCatalog>,
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A built-in action that is always available regardless of catalog configuration.
|
||||||
|
* These are handled by the runtime (e.g. ActionProvider) and injected into prompts
|
||||||
|
* automatically so the LLM knows about them.
|
||||||
|
*/
|
||||||
|
export interface BuiltInAction {
|
||||||
|
/** Action name (e.g. "setState") */
|
||||||
|
name: string;
|
||||||
|
/** Human-readable description for the LLM */
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema options
|
* Schema options
|
||||||
*/
|
*/
|
||||||
@@ -148,6 +162,13 @@ export interface SchemaOptions<TCatalog = unknown> {
|
|||||||
promptTemplate?: PromptTemplate<TCatalog>;
|
promptTemplate?: PromptTemplate<TCatalog>;
|
||||||
/** Default rules baked into the schema (injected before customRules in prompts) */
|
/** Default rules baked into the schema (injected before customRules in prompts) */
|
||||||
defaultRules?: string[];
|
defaultRules?: string[];
|
||||||
|
/**
|
||||||
|
* Built-in actions that are always available regardless of catalog configuration.
|
||||||
|
* These are injected into prompts automatically so the LLM knows about them,
|
||||||
|
* but they don't require handlers in defineRegistry because the runtime
|
||||||
|
* (e.g. ActionProvider) handles them directly.
|
||||||
|
*/
|
||||||
|
builtInActions?: BuiltInAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -347,6 +368,7 @@ export function defineSchema<TDef extends SchemaDefinition>(
|
|||||||
definition,
|
definition,
|
||||||
promptTemplate: options?.promptTemplate,
|
promptTemplate: options?.promptTemplate,
|
||||||
defaultRules: options?.defaultRules,
|
defaultRules: options?.defaultRules,
|
||||||
|
builtInActions: options?.builtInActions,
|
||||||
createCatalog<TCatalog extends InferCatalogInput<TDef["catalog"]>>(
|
createCatalog<TCatalog extends InferCatalogInput<TDef["catalog"]>>(
|
||||||
catalog: TCatalog,
|
catalog: TCatalog,
|
||||||
): Catalog<TDef, TCatalog> {
|
): Catalog<TDef, TCatalog> {
|
||||||
@@ -749,12 +771,26 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
| Record<string, { params?: z.ZodType; description?: string }>
|
| Record<string, { params?: z.ZodType; description?: string }>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (actions && catalog.actionNames.length > 0) {
|
const builtInActions = catalog.schema.builtInActions ?? [];
|
||||||
|
const hasCustomActions = actions && catalog.actionNames.length > 0;
|
||||||
|
const hasBuiltInActions = builtInActions.length > 0;
|
||||||
|
|
||||||
|
if (hasCustomActions || hasBuiltInActions) {
|
||||||
lines.push("AVAILABLE ACTIONS:");
|
lines.push("AVAILABLE ACTIONS:");
|
||||||
lines.push("");
|
lines.push("");
|
||||||
for (const [name, def] of Object.entries(actions)) {
|
|
||||||
lines.push(`- ${name}${def.description ? `: ${def.description}` : ""}`);
|
// Built-in actions (handled by runtime, always available)
|
||||||
|
for (const action of builtInActions) {
|
||||||
|
lines.push(`- ${action.name}: ${action.description} [built-in]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom actions (declared in catalog, require handlers)
|
||||||
|
if (hasCustomActions) {
|
||||||
|
for (const [name, def] of Object.entries(actions)) {
|
||||||
|
lines.push(`- ${name}${def.description ? `: ${def.description}` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1022,6 +1022,104 @@ describe("createJsonRenderTransform", () => {
|
|||||||
const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);
|
const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);
|
||||||
expect(specChunks.length).toBe(1);
|
expect(specChunks.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Text block splitting around spec data
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
it("splits text blocks around spec data (text-start/text-end pairs)", async () => {
|
||||||
|
const input = [
|
||||||
|
"Some text\n",
|
||||||
|
'{"op":"add","path":"/root","value":"r"}\n',
|
||||||
|
"More text\n",
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
const chunks = await transformText(input);
|
||||||
|
|
||||||
|
const textStarts = chunks.filter((c) => c.type === "text-start");
|
||||||
|
const textEnds = chunks.filter((c) => c.type === "text-end");
|
||||||
|
|
||||||
|
// There should be two text blocks: one before the patch and one after
|
||||||
|
expect(textStarts.length).toBe(2);
|
||||||
|
expect(textEnds.length).toBe(2);
|
||||||
|
|
||||||
|
// Spec data should appear between the two text blocks
|
||||||
|
const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);
|
||||||
|
expect(specChunks.length).toBe(1);
|
||||||
|
|
||||||
|
// Find the indices of the first text-end and the spec chunk
|
||||||
|
const firstTextEndIdx = chunks.findIndex((c) => c.type === "text-end");
|
||||||
|
const specIdx = chunks.findIndex((c) => c.type === SPEC_DATA_PART_TYPE);
|
||||||
|
const secondTextStartIdx = chunks.findIndex(
|
||||||
|
(c, i) => i > specIdx && c.type === "text-start",
|
||||||
|
);
|
||||||
|
expect(firstTextEndIdx).toBeLessThan(specIdx);
|
||||||
|
expect(specIdx).toBeLessThan(secondTextStartIdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flush closes an open text block when stream ends without text-end", async () => {
|
||||||
|
const transform = createJsonRenderTransform();
|
||||||
|
const writer = transform.writable.getWriter();
|
||||||
|
const reader = transform.readable.getReader();
|
||||||
|
|
||||||
|
const chunks: StreamChunk[] = [];
|
||||||
|
const readAll = (async () => {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
chunks.push(value);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Write text-start + text-delta, then close WITHOUT text-end
|
||||||
|
await writer.write({ type: "text-start", id: "t1" });
|
||||||
|
await writer.write({
|
||||||
|
type: "text-delta",
|
||||||
|
id: "t1",
|
||||||
|
delta: "Hello world\n",
|
||||||
|
});
|
||||||
|
await writer.close();
|
||||||
|
|
||||||
|
await readAll;
|
||||||
|
|
||||||
|
// The transform's flush should have emitted a text-end to close the block
|
||||||
|
const textEnds = chunks.filter((c) => c.type === "text-end");
|
||||||
|
expect(textEnds.length).toBe(1);
|
||||||
|
|
||||||
|
// Text content should still be present
|
||||||
|
const textChunks = chunks.filter((c) => c.type === "text-delta");
|
||||||
|
const text = textChunks.map((c) => (c as { delta: string }).delta).join("");
|
||||||
|
expect(text).toContain("Hello world");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("consecutive patches do not produce empty text blocks", async () => {
|
||||||
|
const input = [
|
||||||
|
'{"op":"add","path":"/root","value":"r"}\n',
|
||||||
|
'{"op":"add","path":"/elements/r","value":{"type":"Card","props":{},"children":[]}}\n',
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
const chunks = await transformText(input);
|
||||||
|
|
||||||
|
const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);
|
||||||
|
expect(specChunks.length).toBe(2);
|
||||||
|
|
||||||
|
// There should be no text-start/text-end pairs between the two spec chunks
|
||||||
|
// (the initial text-start from the upstream is forwarded, but no new empty ones)
|
||||||
|
const textDeltas = chunks.filter((c) => c.type === "text-delta");
|
||||||
|
const textContent = textDeltas
|
||||||
|
.map((c) => (c as { delta: string }).delta)
|
||||||
|
.join("")
|
||||||
|
.trim();
|
||||||
|
// No meaningful text content between the patches
|
||||||
|
expect(textContent).toBe("");
|
||||||
|
|
||||||
|
// Count text blocks: there should be at most 1 (the initial upstream one),
|
||||||
|
// not extra empty ones inserted between patches
|
||||||
|
const textStarts = chunks.filter((c) => c.type === "text-start");
|
||||||
|
const textEnds = chunks.filter((c) => c.type === "text-end");
|
||||||
|
expect(textStarts.length).toBeLessThanOrEqual(1);
|
||||||
|
expect(textEnds.length).toBeLessThanOrEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
+56
-38
@@ -1010,11 +1010,48 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
let buffering = false;
|
let buffering = false;
|
||||||
// Whether we are inside a ```spec fence
|
// Whether we are inside a ```spec fence
|
||||||
let inSpecFence = false;
|
let inSpecFence = false;
|
||||||
|
// Whether we are currently inside a text block (between text-start/text-end).
|
||||||
|
// Used to split text blocks around spec data so the AI SDK creates separate
|
||||||
|
// text parts, preserving interleaving of prose and UI in message.parts.
|
||||||
|
let inTextBlock = false;
|
||||||
|
let textIdCounter = 0;
|
||||||
|
|
||||||
|
/** Close the current text block if one is open. */
|
||||||
|
function closeTextBlock(
|
||||||
|
controller: TransformStreamDefaultController<StreamChunk>,
|
||||||
|
) {
|
||||||
|
if (inTextBlock) {
|
||||||
|
controller.enqueue({ type: "text-end", id: currentTextId });
|
||||||
|
inTextBlock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ensure a text block is open, starting a new one if needed. */
|
||||||
|
function ensureTextBlock(
|
||||||
|
controller: TransformStreamDefaultController<StreamChunk>,
|
||||||
|
) {
|
||||||
|
if (!inTextBlock) {
|
||||||
|
textIdCounter++;
|
||||||
|
currentTextId = String(textIdCounter);
|
||||||
|
controller.enqueue({ type: "text-start", id: currentTextId });
|
||||||
|
inTextBlock = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emit a text-delta, opening a text block first if necessary. */
|
||||||
|
function emitTextDelta(
|
||||||
|
delta: string,
|
||||||
|
controller: TransformStreamDefaultController<StreamChunk>,
|
||||||
|
) {
|
||||||
|
ensureTextBlock(controller);
|
||||||
|
controller.enqueue({ type: "text-delta", id: currentTextId, delta });
|
||||||
|
}
|
||||||
|
|
||||||
function emitPatch(
|
function emitPatch(
|
||||||
patch: SpecStreamLine,
|
patch: SpecStreamLine,
|
||||||
controller: TransformStreamDefaultController<StreamChunk>,
|
controller: TransformStreamDefaultController<StreamChunk>,
|
||||||
) {
|
) {
|
||||||
|
closeTextBlock(controller);
|
||||||
controller.enqueue({
|
controller.enqueue({
|
||||||
type: SPEC_DATA_PART_TYPE,
|
type: SPEC_DATA_PART_TYPE,
|
||||||
data: { type: "patch", patch },
|
data: { type: "patch", patch },
|
||||||
@@ -1046,19 +1083,11 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
emitPatch(patch, controller);
|
emitPatch(patch, controller);
|
||||||
} else {
|
} else {
|
||||||
// Was buffered but isn't JSONL — flush as text
|
// Was buffered but isn't JSONL — flush as text
|
||||||
controller.enqueue({
|
emitTextDelta(lineBuffer, controller);
|
||||||
type: "text-delta",
|
|
||||||
id: currentTextId,
|
|
||||||
delta: lineBuffer,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Whitespace-only buffer — forward as-is (preserves blank lines)
|
// Whitespace-only buffer — forward as-is (preserves blank lines)
|
||||||
controller.enqueue({
|
emitTextDelta(lineBuffer, controller);
|
||||||
type: "text-delta",
|
|
||||||
id: currentTextId,
|
|
||||||
delta: lineBuffer,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
lineBuffer = "";
|
lineBuffer = "";
|
||||||
buffering = false;
|
buffering = false;
|
||||||
@@ -1092,11 +1121,7 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
// --- Outside fence: heuristic mode ---
|
// --- Outside fence: heuristic mode ---
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
// Empty line — forward for markdown paragraph breaks
|
// Empty line — forward for markdown paragraph breaks
|
||||||
controller.enqueue({
|
emitTextDelta("\n", controller);
|
||||||
type: "text-delta",
|
|
||||||
id: currentTextId,
|
|
||||||
delta: "\n",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1104,11 +1129,7 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
if (patch) {
|
if (patch) {
|
||||||
emitPatch(patch, controller);
|
emitPatch(patch, controller);
|
||||||
} else {
|
} else {
|
||||||
controller.enqueue({
|
emitTextDelta(line + "\n", controller);
|
||||||
type: "text-delta",
|
|
||||||
id: currentTextId,
|
|
||||||
delta: line + "\n",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,14 +1137,19 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
transform(chunk, controller) {
|
transform(chunk, controller) {
|
||||||
switch (chunk.type) {
|
switch (chunk.type) {
|
||||||
case "text-start": {
|
case "text-start": {
|
||||||
currentTextId = (chunk as { id: string }).id;
|
const id = (chunk as { id: string }).id;
|
||||||
|
const idNum = parseInt(id, 10);
|
||||||
|
if (!isNaN(idNum) && idNum >= textIdCounter) {
|
||||||
|
textIdCounter = idNum;
|
||||||
|
}
|
||||||
|
currentTextId = id;
|
||||||
|
inTextBlock = true;
|
||||||
controller.enqueue(chunk);
|
controller.enqueue(chunk);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "text-delta": {
|
case "text-delta": {
|
||||||
const delta = chunk as { id: string; delta: string };
|
const delta = chunk as { id: string; delta: string };
|
||||||
currentTextId = delta.id;
|
|
||||||
const text = delta.delta;
|
const text = delta.delta;
|
||||||
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
@@ -1138,11 +1164,7 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
} else {
|
} else {
|
||||||
// Outside fence, emit newline; inside fence, swallow it
|
// Outside fence, emit newline; inside fence, swallow it
|
||||||
if (!inSpecFence) {
|
if (!inSpecFence) {
|
||||||
controller.enqueue({
|
emitTextDelta("\n", controller);
|
||||||
type: "text-delta",
|
|
||||||
id: currentTextId,
|
|
||||||
delta: "\n",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (lineBuffer.length === 0 && !buffering) {
|
} else if (lineBuffer.length === 0 && !buffering) {
|
||||||
@@ -1152,20 +1174,12 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
buffering = true;
|
buffering = true;
|
||||||
lineBuffer += ch;
|
lineBuffer += ch;
|
||||||
} else {
|
} else {
|
||||||
controller.enqueue({
|
emitTextDelta(ch, controller);
|
||||||
type: "text-delta",
|
|
||||||
id: currentTextId,
|
|
||||||
delta: ch,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (buffering) {
|
} else if (buffering) {
|
||||||
lineBuffer += ch;
|
lineBuffer += ch;
|
||||||
} else {
|
} else {
|
||||||
controller.enqueue({
|
emitTextDelta(ch, controller);
|
||||||
type: "text-delta",
|
|
||||||
id: currentTextId,
|
|
||||||
delta: ch,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1173,7 +1187,10 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
|
|
||||||
case "text-end": {
|
case "text-end": {
|
||||||
flushBuffer(controller);
|
flushBuffer(controller);
|
||||||
controller.enqueue(chunk);
|
if (inTextBlock) {
|
||||||
|
controller.enqueue({ type: "text-end", id: currentTextId });
|
||||||
|
inTextBlock = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1186,6 +1203,7 @@ export function createJsonRenderTransform(): TransformStream<
|
|||||||
|
|
||||||
flush(controller) {
|
flush(controller) {
|
||||||
flushBuffer(controller);
|
flushBuffer(controller);
|
||||||
|
closeTextBlock(controller);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/react-native
|
# @json-render/react-native
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [2d70fab]
|
||||||
|
- @json-render/core@0.7.0
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-native",
|
"name": "@json-render/react-native",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,55 @@
|
|||||||
# @json-render/react
|
# @json-render/react
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 2d70fab: New `@json-render/shadcn` package, event handles, built-in actions, and stream improvements.
|
||||||
|
|
||||||
|
### New: `@json-render/shadcn` Package
|
||||||
|
|
||||||
|
Pre-built [shadcn/ui](https://ui.shadcn.com/) component library for json-render. 30+ components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
|
||||||
|
- `shadcnComponentDefinitions` — Zod-based catalog definitions for all components (server-safe, no React dependency via `@json-render/shadcn/catalog`)
|
||||||
|
- `shadcnComponents` — React implementations for all components
|
||||||
|
- Layout: Card, Stack, Grid, Separator
|
||||||
|
- Navigation: Tabs, Accordion, Collapsible, Pagination
|
||||||
|
- Overlay: Dialog, Drawer, Tooltip, Popover, DropdownMenu
|
||||||
|
- Content: Heading, Text, Image, Avatar, Badge, Alert, Carousel, Table
|
||||||
|
- Feedback: Progress, Skeleton, Spinner
|
||||||
|
- Input: Button, Link, Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Toggle, ToggleGroup, ButtonGroup
|
||||||
|
|
||||||
|
### New: Event Handles (`on()`)
|
||||||
|
|
||||||
|
Components now receive an `on(event)` function in addition to `emit(event)`. The `on()` function returns an `EventHandle` with metadata:
|
||||||
|
- `emit()` — fire the event
|
||||||
|
- `shouldPreventDefault` — whether any action binding requested `preventDefault`
|
||||||
|
- `bound` — whether any handler is bound to this event
|
||||||
|
|
||||||
|
### New: `BaseComponentProps`
|
||||||
|
|
||||||
|
Catalog-agnostic base type for component render functions. Use when building reusable component libraries (like `@json-render/shadcn`) that are not tied to a specific catalog.
|
||||||
|
|
||||||
|
### New: Built-in Actions in Schema
|
||||||
|
|
||||||
|
Schemas can now declare `builtInActions` — actions that are always available at runtime and automatically injected into prompts. The React schema declares `setState`, `pushState`, and `removeState` as built-in, so they appear in prompts without needing to be listed in catalog `actions`.
|
||||||
|
|
||||||
|
### New: `preventDefault` on `ActionBinding`
|
||||||
|
|
||||||
|
Action bindings now support a `preventDefault` boolean field, allowing the LLM to request that default browser behavior (e.g. navigation on links) be prevented.
|
||||||
|
|
||||||
|
### Improved: Stream Transform Text Block Splitting
|
||||||
|
|
||||||
|
`createJsonRenderTransform()` now properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs. This ensures the AI SDK creates separate text parts, preserving correct interleaving of prose and UI in `message.parts`.
|
||||||
|
|
||||||
|
### Improved: `defineRegistry` Actions Requirement
|
||||||
|
|
||||||
|
`defineRegistry` now conditionally requires the `actions` field only when the catalog declares actions. Catalogs with no actions (e.g. `actions: {}`) no longer need to pass an empty actions object.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [2d70fab]
|
||||||
|
- @json-render/core@0.7.0
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react",
|
"name": "@json-render/react",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -25,6 +25,53 @@ export type SetState = (
|
|||||||
// Component Types
|
// Component Types
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle returned by the `on()` function for a specific event.
|
||||||
|
* Provides metadata about the event binding and a method to fire it.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const press = on("press");
|
||||||
|
* if (press.shouldPreventDefault) e.preventDefault();
|
||||||
|
* press.emit();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface EventHandle {
|
||||||
|
/** Fire the event (resolve action bindings) */
|
||||||
|
emit: () => void;
|
||||||
|
/** Whether any binding requested preventDefault */
|
||||||
|
shouldPreventDefault: boolean;
|
||||||
|
/** Whether any handler is bound to this event */
|
||||||
|
bound: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog-agnostic base type for component render function arguments.
|
||||||
|
* Use this when building reusable component libraries (e.g. `@json-render/shadcn`)
|
||||||
|
* that are not tied to a specific catalog.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (
|
||||||
|
* <div>{props.title}{children}</div>
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface BaseComponentProps<P = Record<string, unknown>> {
|
||||||
|
props: P;
|
||||||
|
children?: ReactNode;
|
||||||
|
/** Simple event emitter (shorthand). Fires the event and returns void. */
|
||||||
|
emit: (event: string) => void;
|
||||||
|
/** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */
|
||||||
|
on: (event: string) => EventHandle;
|
||||||
|
/**
|
||||||
|
* Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.
|
||||||
|
* Maps prop name → absolute state path for write-back.
|
||||||
|
*/
|
||||||
|
bindings?: Record<string, string>;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context passed to component render functions
|
* Context passed to component render functions
|
||||||
* @example
|
* @example
|
||||||
@@ -35,18 +82,7 @@ export type SetState = (
|
|||||||
export interface ComponentContext<
|
export interface ComponentContext<
|
||||||
C extends Catalog,
|
C extends Catalog,
|
||||||
K extends keyof InferCatalogComponents<C>,
|
K extends keyof InferCatalogComponents<C>,
|
||||||
> {
|
> extends BaseComponentProps<InferComponentProps<C, K>> {}
|
||||||
props: InferComponentProps<C, K>;
|
|
||||||
children?: ReactNode;
|
|
||||||
/** Emit a named event. The renderer resolves the event to an action binding from the element's `on` field. */
|
|
||||||
emit: (event: string) => void;
|
|
||||||
/**
|
|
||||||
* Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.
|
|
||||||
* Maps prop name → absolute state path for write-back.
|
|
||||||
*/
|
|
||||||
bindings?: Record<string, string>;
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component render function type for React
|
* Component render function type for React
|
||||||
@@ -104,3 +140,15 @@ export type ActionFn<
|
|||||||
export type Actions<C extends Catalog> = {
|
export type Actions<C extends Catalog> = {
|
||||||
[K in keyof InferCatalogActions<C>]: ActionFn<C, K>;
|
[K in keyof InferCatalogActions<C>]: ActionFn<C, K>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the catalog declares at least one action, false otherwise.
|
||||||
|
* Used by defineRegistry to conditionally require the `actions` field.
|
||||||
|
*/
|
||||||
|
export type CatalogHasActions<C extends Catalog> = [
|
||||||
|
InferCatalogActions<C>,
|
||||||
|
] extends [never]
|
||||||
|
? false
|
||||||
|
: [keyof InferCatalogActions<C>] extends [never]
|
||||||
|
? false
|
||||||
|
: true;
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export type { Spec } from "@json-render/core";
|
|||||||
|
|
||||||
// Catalog-aware types for React
|
// Catalog-aware types for React
|
||||||
export type {
|
export type {
|
||||||
|
EventHandle,
|
||||||
|
BaseComponentProps,
|
||||||
SetState,
|
SetState,
|
||||||
StateModel,
|
StateModel,
|
||||||
ComponentContext,
|
ComponentContext,
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import type {
|
|||||||
ActionFn,
|
ActionFn,
|
||||||
SetState,
|
SetState,
|
||||||
StateModel,
|
StateModel,
|
||||||
|
CatalogHasActions,
|
||||||
|
EventHandle,
|
||||||
} from "./catalog-types";
|
} from "./catalog-types";
|
||||||
import { useIsVisible, useVisibility } from "./contexts/visibility";
|
import { useIsVisible, useVisibility } from "./contexts/visibility";
|
||||||
import { useActions } from "./contexts/actions";
|
import { useActions } from "./contexts/actions";
|
||||||
@@ -50,6 +52,8 @@ export interface ComponentRenderProps<P = Record<string, unknown>> {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
/** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. Always provided by the renderer. */
|
/** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. Always provided by the renderer. */
|
||||||
emit: (event: string) => void;
|
emit: (event: string) => void;
|
||||||
|
/** Get an event handle with metadata (shouldPreventDefault, bound). Use when you need to inspect event bindings. */
|
||||||
|
on: (event: string) => EventHandle;
|
||||||
/**
|
/**
|
||||||
* Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.
|
* Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.
|
||||||
* Maps prop name → absolute state path for write-back.
|
* Maps prop name → absolute state path for write-back.
|
||||||
@@ -199,6 +203,24 @@ const ElementRenderer = React.memo(function ElementRenderer({
|
|||||||
[onBindings, execute, fullCtx],
|
[onBindings, execute, fullCtx],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create on() function that returns an EventHandle with metadata for a specific event.
|
||||||
|
const on = useCallback(
|
||||||
|
(eventName: string): EventHandle => {
|
||||||
|
const binding = onBindings?.[eventName];
|
||||||
|
if (!binding) {
|
||||||
|
return { emit: () => {}, shouldPreventDefault: false, bound: false };
|
||||||
|
}
|
||||||
|
const actionBindings = Array.isArray(binding) ? binding : [binding];
|
||||||
|
const shouldPreventDefault = actionBindings.some((b) => b.preventDefault);
|
||||||
|
return {
|
||||||
|
emit: () => emit(eventName),
|
||||||
|
shouldPreventDefault,
|
||||||
|
bound: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[onBindings, emit],
|
||||||
|
);
|
||||||
|
|
||||||
// Don't render if not visible
|
// Don't render if not visible
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
return null;
|
return null;
|
||||||
@@ -262,6 +284,7 @@ const ElementRenderer = React.memo(function ElementRenderer({
|
|||||||
<Component
|
<Component
|
||||||
element={resolvedElement}
|
element={resolvedElement}
|
||||||
emit={emit}
|
emit={emit}
|
||||||
|
on={on}
|
||||||
bindings={elementBindings}
|
bindings={elementBindings}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
@@ -466,12 +489,26 @@ export interface DefineRegistryResult {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for defineRegistry.
|
||||||
|
*
|
||||||
|
* When the catalog declares actions, the `actions` field is required.
|
||||||
|
* When the catalog has no actions (or `actions: {}`), the field is optional.
|
||||||
|
*/
|
||||||
|
type DefineRegistryOptions<C extends Catalog> = {
|
||||||
|
components?: Components<C>;
|
||||||
|
} & (CatalogHasActions<C> extends true
|
||||||
|
? { actions: Actions<C> }
|
||||||
|
: { actions?: Actions<C> });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a registry from a catalog with components and/or actions.
|
* Create a registry from a catalog with components and/or actions.
|
||||||
*
|
*
|
||||||
|
* When the catalog declares actions, the `actions` field is required.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```tsx
|
* ```tsx
|
||||||
* // Components only
|
* // Components only (catalog has no actions)
|
||||||
* const { registry } = defineRegistry(catalog, {
|
* const { registry } = defineRegistry(catalog, {
|
||||||
* components: {
|
* components: {
|
||||||
* Card: ({ props, children }) => (
|
* Card: ({ props, children }) => (
|
||||||
@@ -480,14 +517,7 @@ export interface DefineRegistryResult {
|
|||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* // Actions only
|
* // Both (catalog declares actions)
|
||||||
* const { handlers, executeAction } = defineRegistry(catalog, {
|
|
||||||
* actions: {
|
|
||||||
* viewCustomers: async (params, setState) => { ... },
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Both
|
|
||||||
* const { registry, handlers, executeAction } = defineRegistry(catalog, {
|
* const { registry, handlers, executeAction } = defineRegistry(catalog, {
|
||||||
* components: { ... },
|
* components: { ... },
|
||||||
* actions: { ... },
|
* actions: { ... },
|
||||||
@@ -496,10 +526,7 @@ export interface DefineRegistryResult {
|
|||||||
*/
|
*/
|
||||||
export function defineRegistry<C extends Catalog>(
|
export function defineRegistry<C extends Catalog>(
|
||||||
_catalog: C,
|
_catalog: C,
|
||||||
options: {
|
options: DefineRegistryOptions<C>,
|
||||||
components?: Components<C>;
|
|
||||||
actions?: Actions<C>;
|
|
||||||
},
|
|
||||||
): DefineRegistryResult {
|
): DefineRegistryResult {
|
||||||
// Build component registry
|
// Build component registry
|
||||||
const registry: ComponentRegistry = {};
|
const registry: ComponentRegistry = {};
|
||||||
@@ -509,6 +536,7 @@ export function defineRegistry<C extends Catalog>(
|
|||||||
element,
|
element,
|
||||||
children,
|
children,
|
||||||
emit,
|
emit,
|
||||||
|
on,
|
||||||
bindings,
|
bindings,
|
||||||
loading,
|
loading,
|
||||||
}: ComponentRenderProps) => {
|
}: ComponentRenderProps) => {
|
||||||
@@ -516,6 +544,7 @@ export function defineRegistry<C extends Catalog>(
|
|||||||
props: element.props,
|
props: element.props,
|
||||||
children,
|
children,
|
||||||
emit,
|
emit,
|
||||||
|
on,
|
||||||
bindings,
|
bindings,
|
||||||
loading,
|
loading,
|
||||||
});
|
});
|
||||||
@@ -572,6 +601,7 @@ type DefineRegistryComponentFn = (ctx: {
|
|||||||
props: unknown;
|
props: unknown;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
emit: (event: string) => void;
|
emit: (event: string) => void;
|
||||||
|
on: (event: string) => EventHandle;
|
||||||
bindings?: Record<string, string>;
|
bindings?: Record<string, string>;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
|
|||||||
@@ -51,6 +51,23 @@ export const schema = defineSchema(
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
builtInActions: [
|
||||||
|
{
|
||||||
|
name: "setState",
|
||||||
|
description:
|
||||||
|
"Update a value in the state model at the given statePath. Params: { statePath: string, value: any }",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pushState",
|
||||||
|
description:
|
||||||
|
'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {"$state":"/path"} refs and "$id" for auto IDs.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "removeState",
|
||||||
|
description:
|
||||||
|
"Remove an item from an array in state by index. Params: { statePath: string, index: number }",
|
||||||
|
},
|
||||||
|
],
|
||||||
defaultRules: [
|
defaultRules: [
|
||||||
// Element integrity
|
// Element integrity
|
||||||
"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.",
|
"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/remotion
|
# @json-render/remotion
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [2d70fab]
|
||||||
|
- @json-render/core@0.7.0
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/remotion",
|
"name": "@json-render/remotion",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# @json-render/shadcn
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 2d70fab: New `@json-render/shadcn` package, event handles, built-in actions, and stream improvements.
|
||||||
|
|
||||||
|
### New: `@json-render/shadcn` Package
|
||||||
|
|
||||||
|
Pre-built [shadcn/ui](https://ui.shadcn.com/) component library for json-render. 30+ components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
|
||||||
|
- `shadcnComponentDefinitions` — Zod-based catalog definitions for all components (server-safe, no React dependency via `@json-render/shadcn/catalog`)
|
||||||
|
- `shadcnComponents` — React implementations for all components
|
||||||
|
- Layout: Card, Stack, Grid, Separator
|
||||||
|
- Navigation: Tabs, Accordion, Collapsible, Pagination
|
||||||
|
- Overlay: Dialog, Drawer, Tooltip, Popover, DropdownMenu
|
||||||
|
- Content: Heading, Text, Image, Avatar, Badge, Alert, Carousel, Table
|
||||||
|
- Feedback: Progress, Skeleton, Spinner
|
||||||
|
- Input: Button, Link, Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Toggle, ToggleGroup, ButtonGroup
|
||||||
|
|
||||||
|
### New: Event Handles (`on()`)
|
||||||
|
|
||||||
|
Components now receive an `on(event)` function in addition to `emit(event)`. The `on()` function returns an `EventHandle` with metadata:
|
||||||
|
- `emit()` — fire the event
|
||||||
|
- `shouldPreventDefault` — whether any action binding requested `preventDefault`
|
||||||
|
- `bound` — whether any handler is bound to this event
|
||||||
|
|
||||||
|
### New: `BaseComponentProps`
|
||||||
|
|
||||||
|
Catalog-agnostic base type for component render functions. Use when building reusable component libraries (like `@json-render/shadcn`) that are not tied to a specific catalog.
|
||||||
|
|
||||||
|
### New: Built-in Actions in Schema
|
||||||
|
|
||||||
|
Schemas can now declare `builtInActions` — actions that are always available at runtime and automatically injected into prompts. The React schema declares `setState`, `pushState`, and `removeState` as built-in, so they appear in prompts without needing to be listed in catalog `actions`.
|
||||||
|
|
||||||
|
### New: `preventDefault` on `ActionBinding`
|
||||||
|
|
||||||
|
Action bindings now support a `preventDefault` boolean field, allowing the LLM to request that default browser behavior (e.g. navigation on links) be prevented.
|
||||||
|
|
||||||
|
### Improved: Stream Transform Text Block Splitting
|
||||||
|
|
||||||
|
`createJsonRenderTransform()` now properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs. This ensures the AI SDK creates separate text parts, preserving correct interleaving of prose and UI in `message.parts`.
|
||||||
|
|
||||||
|
### Improved: `defineRegistry` Actions Requirement
|
||||||
|
|
||||||
|
`defineRegistry` now conditionally requires the `actions` field only when the catalog declares actions. Catalogs with no actions (e.g. `actions: {}`) no longer need to pass an empty actions object.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [2d70fab]
|
||||||
|
- @json-render/core@0.7.0
|
||||||
|
- @json-render/react@0.7.0
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# @json-render/shadcn
|
||||||
|
|
||||||
|
Pre-built [shadcn/ui](https://ui.shadcn.com/) components for json-render. Drop-in catalog definitions and React implementations for 30+ components built on Radix UI + Tailwind CSS.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/shadcn @json-render/core @json-render/react zod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Create a Catalog
|
||||||
|
|
||||||
|
Import standard definitions from `@json-render/shadcn/catalog` and pass them to `defineCatalog`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
// Pick the components you need
|
||||||
|
Card: shadcnComponentDefinitions.Card,
|
||||||
|
Stack: shadcnComponentDefinitions.Stack,
|
||||||
|
Heading: shadcnComponentDefinitions.Heading,
|
||||||
|
Button: shadcnComponentDefinitions.Button,
|
||||||
|
Input: shadcnComponentDefinitions.Input,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled automatically by `ActionProvider`. You don't need to declare them in your catalog.
|
||||||
|
|
||||||
|
### 2. Create a Registry
|
||||||
|
|
||||||
|
Import standard implementations from `@json-render/shadcn` and pass them to `defineRegistry`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineRegistry } from "@json-render/react";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponents.Card,
|
||||||
|
Stack: shadcnComponents.Stack,
|
||||||
|
Heading: shadcnComponents.Heading,
|
||||||
|
Button: shadcnComponents.Button,
|
||||||
|
Input: shadcnComponents.Input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Render
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Renderer } from "@json-render/react";
|
||||||
|
|
||||||
|
function App({ spec }) {
|
||||||
|
return <Renderer spec={spec} registry={registry} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending with Custom Components
|
||||||
|
|
||||||
|
Pick standard components as a base and add your own alongside them:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Catalog
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
// Standard
|
||||||
|
Card: shadcnComponentDefinitions.Card,
|
||||||
|
Stack: shadcnComponentDefinitions.Stack,
|
||||||
|
Button: shadcnComponentDefinitions.Button,
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
Metric: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
trend: z.enum(["up", "down", "neutral"]).nullable(),
|
||||||
|
}),
|
||||||
|
description: "KPI metric display",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registry
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
// Standard
|
||||||
|
Card: shadcnComponents.Card,
|
||||||
|
Stack: shadcnComponents.Stack,
|
||||||
|
Button: shadcnComponents.Button,
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
Metric: ({ props }) => (
|
||||||
|
<div>
|
||||||
|
<span>{props.label}</span>
|
||||||
|
<span>{props.value}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standard Components
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `Card` | Container card with optional title and description |
|
||||||
|
| `Stack` | Flex container (horizontal/vertical) with gap, alignment, justify |
|
||||||
|
| `Grid` | Grid layout (1-6 columns) |
|
||||||
|
| `Separator` | Visual separator line |
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `Tabs` | Tabbed navigation |
|
||||||
|
| `Accordion` | Collapsible accordion sections |
|
||||||
|
| `Collapsible` | Single collapsible section with trigger |
|
||||||
|
| `Pagination` | Page navigation |
|
||||||
|
|
||||||
|
### Overlay
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `Dialog` | Modal dialog |
|
||||||
|
| `Drawer` | Bottom drawer |
|
||||||
|
| `Tooltip` | Hover tooltip |
|
||||||
|
| `Popover` | Click-triggered popover |
|
||||||
|
| `DropdownMenu` | Dropdown menu with selectable items |
|
||||||
|
|
||||||
|
### Content
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `Heading` | Heading text (h1-h4) |
|
||||||
|
| `Text` | Paragraph text with variants (body, caption, muted, lead, code) |
|
||||||
|
| `Image` | Placeholder image |
|
||||||
|
| `Avatar` | User avatar with fallback initials |
|
||||||
|
| `Badge` | Status badge |
|
||||||
|
| `Alert` | Alert banner |
|
||||||
|
| `Carousel` | Horizontally scrollable carousel |
|
||||||
|
| `Table` | Data table with columns and rows |
|
||||||
|
|
||||||
|
### Feedback
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `Progress` | Progress bar |
|
||||||
|
| `Skeleton` | Loading placeholder |
|
||||||
|
| `Spinner` | Loading spinner |
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `Button` | Clickable button with variants |
|
||||||
|
| `Link` | Anchor link |
|
||||||
|
| `Input` | Text input with label and validation |
|
||||||
|
| `Textarea` | Multi-line text input |
|
||||||
|
| `Select` | Dropdown select |
|
||||||
|
| `Checkbox` | Checkbox input |
|
||||||
|
| `Radio` | Radio button group |
|
||||||
|
| `Switch` | Toggle switch |
|
||||||
|
| `Slider` | Range slider |
|
||||||
|
| `Toggle` | Toggle button |
|
||||||
|
| `ToggleGroup` | Group of toggle buttons |
|
||||||
|
| `ButtonGroup` | Group of buttons with selected state |
|
||||||
|
|
||||||
|
## Built-in Actions
|
||||||
|
|
||||||
|
State actions (`setState`, `pushState`, `removeState`) are built into the `@json-render/react` schema and handled automatically by `ActionProvider`. They are included in prompts without needing to be declared in your catalog.
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `setState` | Set a value at a state path |
|
||||||
|
| `pushState` | Push a value onto an array in state |
|
||||||
|
| `removeState` | Remove an item from an array in state |
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
| Entry Point | Exports |
|
||||||
|
|-------------|---------|
|
||||||
|
| `@json-render/shadcn` | `shadcnComponents` |
|
||||||
|
| `@json-render/shadcn/catalog` | `shadcnComponentDefinitions` |
|
||||||
|
|
||||||
|
The `/catalog` entry point contains only Zod schemas (no React dependency), so it can be used in server-side code for prompt generation.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "src",
|
||||||
|
"utils": "src/lib/utils",
|
||||||
|
"ui": "src/ui",
|
||||||
|
"lib": "src/lib",
|
||||||
|
"hooks": "src/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "@json-render/shadcn",
|
||||||
|
"version": "0.7.0",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"description": "shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.",
|
||||||
|
"keywords": [
|
||||||
|
"json",
|
||||||
|
"ui",
|
||||||
|
"react",
|
||||||
|
"shadcn",
|
||||||
|
"tailwind",
|
||||||
|
"radix",
|
||||||
|
"ai",
|
||||||
|
"generative-ui",
|
||||||
|
"llm",
|
||||||
|
"renderer",
|
||||||
|
"streaming",
|
||||||
|
"components"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
|
"directory": "packages/shadcn"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./catalog": {
|
||||||
|
"types": "./dist/catalog.d.ts",
|
||||||
|
"import": "./dist/catalog.mjs",
|
||||||
|
"require": "./dist/catalog.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"check-types": "tsc --noEmit",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@json-render/react": "workspace:*",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"lucide-react": "^0.564.0",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
|
"tailwind-merge": "^3.4.1",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/typescript-config": "workspace:*",
|
||||||
|
"@types/react": "19.2.3",
|
||||||
|
"tsup": "^8.0.2",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// shadcn/ui Component Definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shadcn/ui component definitions for json-render catalogs.
|
||||||
|
*
|
||||||
|
* These can be used directly or extended with custom components.
|
||||||
|
* All components are built using Radix UI primitives + Tailwind CSS.
|
||||||
|
*/
|
||||||
|
export const shadcnComponentDefinitions = {
|
||||||
|
// ==========================================================================
|
||||||
|
// Layout Components
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
Card: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string().nullable(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
maxWidth: z.enum(["sm", "md", "lg", "full"]).nullable(),
|
||||||
|
centered: z.boolean().nullable(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description:
|
||||||
|
"Container card for content sections. Use for forms/content boxes, NOT for page headers.",
|
||||||
|
example: { title: "Overview", description: "Your account summary" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Stack: {
|
||||||
|
props: z.object({
|
||||||
|
direction: z.enum(["horizontal", "vertical"]).nullable(),
|
||||||
|
gap: z.enum(["none", "sm", "md", "lg"]).nullable(),
|
||||||
|
align: z.enum(["start", "center", "end", "stretch"]).nullable(),
|
||||||
|
justify: z
|
||||||
|
.enum(["start", "center", "end", "between", "around"])
|
||||||
|
.nullable(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description: "Flex container for layouts",
|
||||||
|
example: { direction: "vertical", gap: "md" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Grid: {
|
||||||
|
props: z.object({
|
||||||
|
columns: z.number().nullable(),
|
||||||
|
gap: z.enum(["sm", "md", "lg"]).nullable(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description: "Grid layout (1-6 columns)",
|
||||||
|
example: { columns: 3, gap: "md" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Separator: {
|
||||||
|
props: z.object({
|
||||||
|
orientation: z.enum(["horizontal", "vertical"]).nullable(),
|
||||||
|
}),
|
||||||
|
description: "Visual separator line",
|
||||||
|
},
|
||||||
|
|
||||||
|
Tabs: {
|
||||||
|
props: z.object({
|
||||||
|
tabs: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
defaultValue: z.string().nullable(),
|
||||||
|
value: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
events: ["change"],
|
||||||
|
description:
|
||||||
|
"Tab navigation. Use { $bindState } on value for active tab binding.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Accordion: {
|
||||||
|
props: z.object({
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
content: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
type: z.enum(["single", "multiple"]).nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
"Collapsible sections. Items as [{title, content}]. Type 'single' (default) or 'multiple'.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Collapsible: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
defaultOpen: z.boolean().nullable(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description: "Collapsible section with trigger. Children render inside.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Dialog: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
openPath: z.string(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description:
|
||||||
|
"Modal dialog. Set openPath to a boolean state path. Use setState to toggle.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Drawer: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
openPath: z.string(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description:
|
||||||
|
"Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Carousel: {
|
||||||
|
props: z.object({
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string().nullable(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
description: "Horizontally scrollable carousel of cards.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Data Display Components
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
Table: {
|
||||||
|
props: z.object({
|
||||||
|
columns: z.array(z.string()),
|
||||||
|
rows: z.array(z.array(z.string())),
|
||||||
|
caption: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [["Alice","admin"],["Bob","user"]].',
|
||||||
|
example: {
|
||||||
|
columns: ["Name", "Role"],
|
||||||
|
rows: [
|
||||||
|
["Alice", "Admin"],
|
||||||
|
["Bob", "User"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Heading: {
|
||||||
|
props: z.object({
|
||||||
|
text: z.string(),
|
||||||
|
level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
|
||||||
|
}),
|
||||||
|
description: "Heading text (h1-h4)",
|
||||||
|
example: { text: "Welcome", level: "h1" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Text: {
|
||||||
|
props: z.object({
|
||||||
|
text: z.string(),
|
||||||
|
variant: z.enum(["body", "caption", "muted", "lead", "code"]).nullable(),
|
||||||
|
}),
|
||||||
|
description: "Paragraph text",
|
||||||
|
example: { text: "Hello, world!" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Image: {
|
||||||
|
props: z.object({
|
||||||
|
src: z.string().nullable(),
|
||||||
|
alt: z.string(),
|
||||||
|
width: z.number().nullable(),
|
||||||
|
height: z.number().nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
"Image component. Renders an img tag when src is provided, otherwise a placeholder.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Avatar: {
|
||||||
|
props: z.object({
|
||||||
|
src: z.string().nullable(),
|
||||||
|
name: z.string(),
|
||||||
|
size: z.enum(["sm", "md", "lg"]).nullable(),
|
||||||
|
}),
|
||||||
|
description: "User avatar with fallback initials",
|
||||||
|
example: { name: "Jane Doe", size: "md" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Badge: {
|
||||||
|
props: z.object({
|
||||||
|
text: z.string(),
|
||||||
|
variant: z
|
||||||
|
.enum(["default", "secondary", "destructive", "outline"])
|
||||||
|
.nullable(),
|
||||||
|
}),
|
||||||
|
description: "Status badge",
|
||||||
|
example: { text: "Active", variant: "default" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Alert: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
message: z.string().nullable(),
|
||||||
|
type: z.enum(["info", "success", "warning", "error"]).nullable(),
|
||||||
|
}),
|
||||||
|
description: "Alert banner",
|
||||||
|
example: {
|
||||||
|
title: "Note",
|
||||||
|
message: "Your changes have been saved.",
|
||||||
|
type: "success",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Progress: {
|
||||||
|
props: z.object({
|
||||||
|
value: z.number(),
|
||||||
|
max: z.number().nullable(),
|
||||||
|
label: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
description: "Progress bar (value 0-100)",
|
||||||
|
example: { value: 65, max: 100, label: "Upload progress" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Skeleton: {
|
||||||
|
props: z.object({
|
||||||
|
width: z.string().nullable(),
|
||||||
|
height: z.string().nullable(),
|
||||||
|
rounded: z.boolean().nullable(),
|
||||||
|
}),
|
||||||
|
description: "Loading placeholder skeleton",
|
||||||
|
},
|
||||||
|
|
||||||
|
Spinner: {
|
||||||
|
props: z.object({
|
||||||
|
size: z.enum(["sm", "md", "lg"]).nullable(),
|
||||||
|
label: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
description: "Loading spinner indicator",
|
||||||
|
},
|
||||||
|
|
||||||
|
Tooltip: {
|
||||||
|
props: z.object({
|
||||||
|
content: z.string(),
|
||||||
|
text: z.string(),
|
||||||
|
}),
|
||||||
|
description: "Hover tooltip. Shows content on hover over text.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Popover: {
|
||||||
|
props: z.object({
|
||||||
|
trigger: z.string(),
|
||||||
|
content: z.string(),
|
||||||
|
}),
|
||||||
|
description: "Popover that appears on click of trigger.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Form Input Components
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
Input: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
type: z.enum(["text", "email", "password", "number"]).nullable(),
|
||||||
|
placeholder: z.string().nullable(),
|
||||||
|
value: z.string().nullable(),
|
||||||
|
checks: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
args: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.nullable(),
|
||||||
|
}),
|
||||||
|
events: ["submit", "focus", "blur"],
|
||||||
|
description:
|
||||||
|
"Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength).",
|
||||||
|
example: {
|
||||||
|
label: "Email",
|
||||||
|
name: "email",
|
||||||
|
type: "email",
|
||||||
|
placeholder: "you@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Textarea: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
placeholder: z.string().nullable(),
|
||||||
|
rows: z.number().nullable(),
|
||||||
|
value: z.string().nullable(),
|
||||||
|
checks: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
args: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.nullable(),
|
||||||
|
}),
|
||||||
|
description:
|
||||||
|
"Multi-line text input. Use { $bindState } on value for binding. Use checks for validation.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Select: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
options: z.array(z.string()),
|
||||||
|
placeholder: z.string().nullable(),
|
||||||
|
value: z.string().nullable(),
|
||||||
|
checks: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
args: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description:
|
||||||
|
"Dropdown select input. Use { $bindState } on value for binding. Use checks for validation.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Checkbox: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
checked: z.boolean().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description: "Checkbox input. Use { $bindState } on checked for binding.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Radio: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
options: z.array(z.string()),
|
||||||
|
value: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description: "Radio button group. Use { $bindState } on value for binding.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Switch: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
checked: z.boolean().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description: "Toggle switch. Use { $bindState } on checked for binding.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Slider: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string().nullable(),
|
||||||
|
min: z.number().nullable(),
|
||||||
|
max: z.number().nullable(),
|
||||||
|
step: z.number().nullable(),
|
||||||
|
value: z.number().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description: "Range slider input. Use { $bindState } on value for binding.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Action Components
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
Button: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
variant: z.enum(["primary", "secondary", "danger"]).nullable(),
|
||||||
|
disabled: z.boolean().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["press"],
|
||||||
|
description: "Clickable button. Bind on.press for handler.",
|
||||||
|
example: { label: "Submit", variant: "primary" },
|
||||||
|
},
|
||||||
|
|
||||||
|
Link: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
href: z.string(),
|
||||||
|
}),
|
||||||
|
events: ["press"],
|
||||||
|
description: "Anchor link. Bind on.press for click handler.",
|
||||||
|
},
|
||||||
|
|
||||||
|
DropdownMenu: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
value: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["select"],
|
||||||
|
description:
|
||||||
|
"Dropdown menu with trigger button and selectable items. Use { $bindState } on value for selected item binding.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Toggle: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
pressed: z.boolean().nullable(),
|
||||||
|
variant: z.enum(["default", "outline"]).nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description:
|
||||||
|
"Toggle button. Use { $bindState } on pressed for state binding.",
|
||||||
|
},
|
||||||
|
|
||||||
|
ToggleGroup: {
|
||||||
|
props: z.object({
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
type: z.enum(["single", "multiple"]).nullable(),
|
||||||
|
value: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description:
|
||||||
|
"Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.",
|
||||||
|
},
|
||||||
|
|
||||||
|
ButtonGroup: {
|
||||||
|
props: z.object({
|
||||||
|
buttons: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
selected: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description:
|
||||||
|
"Segmented button group. Use { $bindState } on selected for selected value.",
|
||||||
|
},
|
||||||
|
|
||||||
|
Pagination: {
|
||||||
|
props: z.object({
|
||||||
|
totalPages: z.number(),
|
||||||
|
page: z.number().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["change"],
|
||||||
|
description:
|
||||||
|
"Page navigation. Use { $bindState } on page for current page number.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for a component definition
|
||||||
|
*/
|
||||||
|
export type ComponentDefinition = {
|
||||||
|
props: z.ZodType;
|
||||||
|
slots?: string[];
|
||||||
|
events?: string[];
|
||||||
|
description: string;
|
||||||
|
example?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infer the props type for a shadcn component by name.
|
||||||
|
* Derives the TypeScript type directly from the Zod schema,
|
||||||
|
* so component implementations stay in sync with catalog definitions.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* type CardProps = ShadcnProps<"Card">;
|
||||||
|
* // { title: string | null; description: string | null; ... }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export type ShadcnProps<K extends keyof typeof shadcnComponentDefinitions> =
|
||||||
|
z.output<(typeof shadcnComponentDefinitions)[K]["props"]>;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
// Component implementations
|
||||||
|
export { shadcnComponents } from "./components";
|
||||||
|
|
||||||
|
// Catalog definitions (also available via @json-render/shadcn/catalog)
|
||||||
|
export {
|
||||||
|
shadcnComponentDefinitions,
|
||||||
|
type ComponentDefinition,
|
||||||
|
type ShadcnProps,
|
||||||
|
} from "./catalog";
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
|
import { Accordion as AccordionPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left 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,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Avatar as AvatarPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn("aspect-square size-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Slot } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none 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 transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot.Root : "span";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Slot } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none 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",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
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",
|
||||||
|
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import useEmblaCarousel, {
|
||||||
|
type UseEmblaCarouselType,
|
||||||
|
} from "embla-carousel-react";
|
||||||
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
|
type CarouselProps = {
|
||||||
|
opts?: CarouselOptions;
|
||||||
|
plugins?: CarouselPlugin;
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
setApi?: (api: CarouselApi) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CarouselContextProps = {
|
||||||
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
|
scrollPrev: () => void;
|
||||||
|
scrollNext: () => void;
|
||||||
|
canScrollPrev: boolean;
|
||||||
|
canScrollNext: boolean;
|
||||||
|
} & CarouselProps;
|
||||||
|
|
||||||
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCarousel must be used within a <Carousel />");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Carousel({
|
||||||
|
orientation = "horizontal",
|
||||||
|
opts,
|
||||||
|
setApi,
|
||||||
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
|
},
|
||||||
|
plugins,
|
||||||
|
);
|
||||||
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||||
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||||
|
|
||||||
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
|
if (!api) return;
|
||||||
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
|
setCanScrollNext(api.canScrollNext());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollPrev = React.useCallback(() => {
|
||||||
|
api?.scrollPrev();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const scrollNext = React.useCallback(() => {
|
||||||
|
api?.scrollNext();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollPrev();
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollNext();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollPrev, scrollNext],
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api || !setApi) return;
|
||||||
|
setApi(api);
|
||||||
|
}, [api, setApi]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api) return;
|
||||||
|
onSelect(api);
|
||||||
|
api.on("reInit", onSelect);
|
||||||
|
api.on("select", onSelect);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api?.off("select", onSelect);
|
||||||
|
};
|
||||||
|
}, [api, onSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api: api,
|
||||||
|
opts,
|
||||||
|
orientation:
|
||||||
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev,
|
||||||
|
canScrollNext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cn("relative", className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
data-slot="carousel"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={carouselRef}
|
||||||
|
className="overflow-hidden"
|
||||||
|
data-slot="carousel-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex",
|
||||||
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
data-slot="carousel-item"
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CarouselPrevious({
|
||||||
|
className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-slot="carousel-previous"
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute size-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "top-1/2 -left-12 -translate-y-1/2"
|
||||||
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeft />
|
||||||
|
<span className="sr-only">Previous slide</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CarouselNext({
|
||||||
|
className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-slot="carousel-next"
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute size-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "top-1/2 -right-12 -translate-y-1/2"
|
||||||
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
onClick={scrollNext}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRight />
|
||||||
|
<span className="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
type CarouselApi,
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselPrevious,
|
||||||
|
CarouselNext,
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { CheckIcon } from "lucide-react";
|
||||||
|
import { Checkbox as CheckboxPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-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 size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="grid place-content-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox };
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { XIcon } from "lucide-react";
|
||||||
|
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={cn(
|
||||||
|
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||||
|
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||||
|
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||||
|
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||||
|
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
};
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={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-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-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",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-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",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={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-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-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,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input };
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Label as LabelPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={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,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label };
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
import { buttonVariants, type Button } from "./button";
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
React.ComponentProps<"a">;
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className="hidden sm:block">Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:block">Next</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationPrevious,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationEllipsis,
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Popover as PopoverPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={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-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="popover-header"
|
||||||
|
className={cn("flex flex-col gap-1 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="popover-title"
|
||||||
|
className={cn("font-medium", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="popover-description"
|
||||||
|
className={cn("text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTitle,
|
||||||
|
PopoverDescription,
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Progress as ProgressPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress };
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { CircleIcon } from "lucide-react";
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function RadioGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
className={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,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
data-slot="radio-group-indicator"
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem };
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
|
import { Select as SelectPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={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 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,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "item-aligned",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={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-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"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,
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus: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 pr-8 pl-2 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,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-slot="select-item-indicator"
|
||||||
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
|
>
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Separator as SeparatorPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="skeleton"
|
||||||
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton };
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Slider as SliderPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Slider({
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
min = 0,
|
||||||
|
max = 100,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||||
|
const _values = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max],
|
||||||
|
[value, defaultValue, min, max],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
data-slot="slider"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track
|
||||||
|
data-slot="slider-track"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
key={index}
|
||||||
|
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Slider };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Switch as SwitchPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||||
|
size?: "sm" | "default";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch };
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Tabs as TabsPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Tabs({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
data-orientation={orientation}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabsListVariants = cva(
|
||||||
|
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-muted",
|
||||||
|
line: "gap-1 bg-transparent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||||
|
VariantProps<typeof tabsListVariants>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(tabsListVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative 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-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||||
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
||||||
|
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot="tabs-content"
|
||||||
|
className={cn("flex-1 outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder: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 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { type VariantProps } from "class-variance-authority";
|
||||||
|
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
import { toggleVariants } from "./toggle";
|
||||||
|
|
||||||
|
const ToggleGroupContext = React.createContext<
|
||||||
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
size: "default",
|
||||||
|
variant: "default",
|
||||||
|
spacing: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
function ToggleGroup({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
spacing = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Root
|
||||||
|
data-slot="toggle-group"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
data-spacing={spacing}
|
||||||
|
style={{ "--gap": spacing } as React.CSSProperties}
|
||||||
|
className={cn(
|
||||||
|
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToggleGroupItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
const context = React.useContext(ToggleGroupContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
data-slot="toggle-group-item"
|
||||||
|
data-variant={context.variant || variant}
|
||||||
|
data-size={context.size || size}
|
||||||
|
data-spacing={context.spacing}
|
||||||
|
className={cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
||||||
|
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem };
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Toggle as TogglePrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-2 min-w-9",
|
||||||
|
sm: "h-8 px-1.5 min-w-8",
|
||||||
|
lg: "h-10 px-2.5 min-w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Toggle({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
data-slot="toggle"
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants };
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/typescript-config/react-library.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from "tsup";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts", "src/catalog.ts"],
|
||||||
|
format: ["cjs", "esm"],
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
external: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"@json-render/core",
|
||||||
|
"@json-render/react",
|
||||||
|
"zod",
|
||||||
|
],
|
||||||
|
});
|
||||||
Generated
+243
-169
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,151 @@
|
|||||||
|
---
|
||||||
|
name: json-render-shadcn
|
||||||
|
description: Pre-built shadcn/ui components for json-render. Use when working with @json-render/shadcn, adding standard UI components to a catalog, or building web UIs with Radix UI + Tailwind CSS components.
|
||||||
|
---
|
||||||
|
|
||||||
|
# @json-render/shadcn
|
||||||
|
|
||||||
|
Pre-built shadcn/ui component definitions and implementations for json-render. Provides 30+ components built on Radix UI + Tailwind CSS.
|
||||||
|
|
||||||
|
## Two Entry Points
|
||||||
|
|
||||||
|
| Entry Point | Exports | Use For |
|
||||||
|
|-------------|---------|---------|
|
||||||
|
| `@json-render/shadcn/catalog` | `shadcnComponentDefinitions` | Catalog schemas (no React dependency, safe for server) |
|
||||||
|
| `@json-render/shadcn` | `shadcnComponents` | React implementations |
|
||||||
|
|
||||||
|
## Usage Pattern
|
||||||
|
|
||||||
|
Pick the components you need from the standard definitions. Do not spread all definitions -- explicitly select what your app uses:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
|
import { defineRegistry } from "@json-render/react";
|
||||||
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
|
|
||||||
|
// Catalog: pick definitions
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponentDefinitions.Card,
|
||||||
|
Stack: shadcnComponentDefinitions.Stack,
|
||||||
|
Heading: shadcnComponentDefinitions.Heading,
|
||||||
|
Button: shadcnComponentDefinitions.Button,
|
||||||
|
Input: shadcnComponentDefinitions.Input,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registry: pick matching implementations
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponents.Card,
|
||||||
|
Stack: shadcnComponents.Stack,
|
||||||
|
Heading: shadcnComponents.Heading,
|
||||||
|
Button: shadcnComponents.Button,
|
||||||
|
Input: shadcnComponents.Input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled by `ActionProvider` automatically. No need to declare them.
|
||||||
|
|
||||||
|
## Extending with Custom Components
|
||||||
|
|
||||||
|
Add custom components alongside standard ones:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
// Standard
|
||||||
|
Card: shadcnComponentDefinitions.Card,
|
||||||
|
Stack: shadcnComponentDefinitions.Stack,
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
Metric: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
trend: z.enum(["up", "down", "neutral"]).nullable(),
|
||||||
|
}),
|
||||||
|
description: "KPI metric display",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: shadcnComponents.Card,
|
||||||
|
Stack: shadcnComponents.Stack,
|
||||||
|
Metric: ({ props }) => <div>{props.label}: {props.value}</div>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Components
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
- **Card** - Container with optional title, description, maxWidth, centered
|
||||||
|
- **Stack** - Flex container with direction, gap, align, justify
|
||||||
|
- **Grid** - Grid layout with columns (number) and gap
|
||||||
|
- **Separator** - Visual divider with orientation
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
- **Tabs** - Tabbed navigation with tabs array, defaultValue, value
|
||||||
|
- **Accordion** - Collapsible sections with items array and type (single/multiple)
|
||||||
|
- **Collapsible** - Single collapsible section with title
|
||||||
|
- **Pagination** - Page navigation with totalPages and page
|
||||||
|
|
||||||
|
### Overlay
|
||||||
|
- **Dialog** - Modal dialog with title, description, openPath
|
||||||
|
- **Drawer** - Bottom drawer with title, description, openPath
|
||||||
|
- **Tooltip** - Hover tooltip with content and text
|
||||||
|
- **Popover** - Click-triggered popover with trigger and content
|
||||||
|
- **DropdownMenu** - Dropdown with label and items array
|
||||||
|
|
||||||
|
### Content
|
||||||
|
- **Heading** - Heading text with level (h1-h4)
|
||||||
|
- **Text** - Paragraph with variant (body, caption, muted, lead, code)
|
||||||
|
- **Image** - Image with alt, width, height
|
||||||
|
- **Avatar** - User avatar with src, name, size
|
||||||
|
- **Badge** - Status badge with text and variant (default, secondary, destructive, outline)
|
||||||
|
- **Alert** - Alert banner with title, message, type (success, warning, info, error)
|
||||||
|
- **Carousel** - Scrollable carousel with items array
|
||||||
|
- **Table** - Data table with columns (string[]) and rows (string[][])
|
||||||
|
|
||||||
|
### Feedback
|
||||||
|
- **Progress** - Progress bar with value, max, label
|
||||||
|
- **Skeleton** - Loading placeholder with width, height, rounded
|
||||||
|
- **Spinner** - Loading spinner with size and label
|
||||||
|
|
||||||
|
### Input
|
||||||
|
- **Button** - Button with label, variant (primary, secondary, danger), disabled
|
||||||
|
- **Link** - Anchor link with label and href
|
||||||
|
- **Input** - Text input with label, name, type, placeholder, value, checks
|
||||||
|
- **Textarea** - Multi-line input with label, name, placeholder, rows, value, checks
|
||||||
|
- **Select** - Dropdown select with label, name, options (string[]), value, checks
|
||||||
|
- **Checkbox** - Checkbox with label, name, checked
|
||||||
|
- **Radio** - Radio group with label, name, options (string[]), value
|
||||||
|
- **Switch** - Toggle switch with label, name, checked
|
||||||
|
- **Slider** - Range slider with label, min, max, step, value
|
||||||
|
- **Toggle** - Toggle button with label, pressed, variant
|
||||||
|
- **ToggleGroup** - Group of toggles with items, type, value
|
||||||
|
- **ButtonGroup** - Button group with buttons array and selected
|
||||||
|
|
||||||
|
## Built-in Actions (from `@json-render/react`)
|
||||||
|
|
||||||
|
These are built into the React schema and handled by `ActionProvider` automatically. They appear in prompts without needing to be declared in the catalog.
|
||||||
|
|
||||||
|
- **setState** - Set a value at a state path (`{ statePath, value }`)
|
||||||
|
- **pushState** - Push a value onto an array (`{ statePath, value, clearStatePath? }`)
|
||||||
|
- **removeState** - Remove an array item by index (`{ statePath, index }`)
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- The `/catalog` entry point has no React dependency -- use it for server-side prompt generation
|
||||||
|
- Components use Tailwind CSS classes -- your app must have Tailwind configured
|
||||||
|
- Component implementations use bundled shadcn/ui primitives (not your app's `components/ui/`)
|
||||||
|
- Form inputs support `checks` for validation (type + message pairs)
|
||||||
|
- Events: inputs emit `change`/`submit`/`focus`/`blur`; buttons emit `press`; selects emit `change`/`select`
|
||||||
Reference in New Issue
Block a user