"use client"; import { useState } from "react"; import { defineRegistry, useBoundProp, useStateBinding, useFieldValidation, } from "@json-render/react"; import { toast } from "sonner"; import { playgroundCatalog } from "./catalog"; // shadcn components import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { Switch } from "@/components/ui/switch"; import { Progress } from "@/components/ui/progress"; import { Separator } from "@/components/ui/separator"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; import { Dialog as DialogPrimitive, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Accordion as AccordionPrimitive, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Badge } from "@/components/ui/badge"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Carousel as CarouselPrimitive, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, } from "@/components/ui/carousel"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Table as TablePrimitive, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Drawer as DrawerPrimitive, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; import { DropdownMenu as DropdownMenuPrimitive, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Pagination as PaginationPrimitive, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; import { Popover as PopoverPrimitive, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Skeleton } from "@/components/ui/skeleton"; import { Slider } from "@/components/ui/slider"; import { Tabs as TabsPrimitive, TabsList, TabsTrigger, } from "@/components/ui/tabs"; import { Toggle } from "@/components/ui/toggle"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Tooltip as TooltipPrimitive, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { icons as lucideIcons } from "lucide-react"; // ============================================================================= // Registry — components + actions, types inferred from catalog // ============================================================================= export const { registry, executeAction } = defineRegistry(playgroundCatalog, { components: { // ── Layout ──────────────────────────────────────────────────────── Card: ({ props, children }) => { const maxWidthClass = props.maxWidth === "sm" ? "max-w-xs sm:min-w-[280px]" : props.maxWidth === "md" ? "max-w-sm sm:min-w-[320px]" : props.maxWidth === "lg" ? "max-w-md sm:min-w-[360px]" : "w-full"; const centeredClass = props.centered ? "mx-auto" : ""; return (
{(props.title || props.description) && (
{props.title && (

{props.title}

)} {props.description && (

{props.description}

)}
)}
{children}
); }, Stack: ({ props, children }) => { const isHorizontal = props.direction === "horizontal"; const gapClass = props.gap === "lg" ? "gap-4" : props.gap === "md" ? "gap-3" : props.gap === "sm" ? "gap-2" : props.gap === "none" ? "gap-0" : "gap-3"; let alignClass: string; if (isHorizontal) { alignClass = props.align === "center" ? "items-center" : props.align === "end" ? "items-end" : props.align === "stretch" ? "items-stretch" : "items-start"; } else { // Vertical: items-center/end lets inline elements (Avatar, Badge, Button) // center/align naturally. Block containers (Grid, Accordion, Table) add // their own w-full to stretch regardless. alignClass = props.align === "center" ? "items-center" : props.align === "end" ? "items-end" : props.align === "start" ? "items-start" : "items-stretch"; } const justifyClass = props.justify === "center" ? "justify-center" : props.justify === "end" ? "justify-end" : props.justify === "between" ? "justify-between" : props.justify === "around" ? "justify-around" : ""; return (
{children}
); }, Grid: ({ props, children }) => { const childCount = Array.isArray(children) ? children.length : children ? 1 : 0; const n = Math.min(props.columns ?? 1, childCount || 1); const cols = n >= 6 ? "grid-cols-6" : n >= 5 ? "grid-cols-5" : n >= 4 ? "grid-cols-4" : n >= 3 ? "grid-cols-3" : n >= 2 ? "grid-cols-2" : "grid-cols-1"; const gridGap = props.gap === "lg" ? "gap-4" : props.gap === "sm" ? "gap-2" : "gap-3"; return
{children}
; }, Separator: ({ props }) => ( ), Tabs: ({ props, bindings, emit }) => { const tabs = props.tabs ?? []; const [boundValue, setBoundValue] = useBoundProp( props.value as string | undefined, bindings?.value, ); const [localValue, setLocalValue] = useState( props.defaultValue ?? tabs[0]?.value ?? "", ); const isBound = !!bindings?.value; const value = isBound ? (boundValue ?? tabs[0]?.value ?? "") : localValue; const setValue = isBound ? setBoundValue : setLocalValue; return ( { setValue(v); emit("change"); }} > {tabs.map((tab) => ( {tab.label} ))} ); }, Accordion: ({ props }) => { const items = props.items ?? []; const accordionType = props.type ?? "single"; if (accordionType === "multiple") { return ( {items.map((item, i) => ( {item.title} {item.content} ))} ); } return ( {items.map((item, i) => ( {item.title} {item.content} ))} ); }, Collapsible: ({ props, children }) => { const [open, setOpen] = useState(props.defaultOpen ?? false); return ( {children} ); }, Dialog: ({ props, children }) => { const [open, setOpen] = useStateBinding(props.openPath); return ( setOpen(v)}> {props.title} {props.description && ( {props.description} )} {children} ); }, Drawer: ({ props, children }) => { const [open, setOpen] = useStateBinding(props.openPath); return ( setOpen(v)}> {props.title} {props.description && ( {props.description} )}
{children}
); }, Carousel: ({ props }) => { const items = props.items ?? []; return ( {items.map((item, i) => (
{item.title && (

{item.title}

)} {item.description && (

{item.description}

)}
))}
); }, // ── Data Display ────────────────────────────────────────────────── Table: ({ props }) => { const columns = props.columns ?? []; const rawRows: unknown[] = Array.isArray(props.rows) ? props.rows : []; const rows = rawRows.map((row) => { if (Array.isArray(row)) return row.map(String); if (row && typeof row === "object") { const obj = row as Record; return columns.map((col) => String(obj[col] ?? obj[col.toLowerCase()] ?? ""), ); } return columns.map(() => ""); }); return (
{props.caption && {props.caption}} {columns.map((col) => ( {col} ))} {rows.map((row, i) => ( {row.map((cell, j) => ( {cell} ))} ))}
); }, Heading: ({ props }) => { const level = props.level ?? "h2"; const headingClass = level === "h1" ? "text-2xl font-bold tracking-tight" : level === "h3" ? "text-base font-semibold tracking-tight" : level === "h4" ? "text-sm font-medium uppercase tracking-wider text-muted-foreground" : "text-xl font-semibold tracking-tight"; if (level === "h1") return

{props.text}

; if (level === "h3") return

{props.text}

; if (level === "h4") return

{props.text}

; return

{props.text}

; }, Text: ({ props }) => { const textClass = props.variant === "caption" ? "text-xs" : props.variant === "muted" ? "text-sm text-muted-foreground" : props.variant === "lead" ? "text-xl text-muted-foreground" : props.variant === "code" ? "font-mono text-sm bg-muted px-1.5 py-0.5 rounded" : "text-sm"; if (props.variant === "code") { return {props.text}; } return

{props.text}

; }, Image: ({ props }) => (
{props.alt && {props.alt}}
), Icon: ({ props }) => { const IconComponent = lucideIcons[props.name as keyof typeof lucideIcons]; if (!IconComponent) return null; const sizeMap = { sm: 16, md: 20, lg: 24 } as const; const px = sizeMap[props.size ?? "md"] ?? 20; const colorClass = props.color === "muted" ? "text-muted-foreground" : props.color === "primary" ? "text-primary" : props.color === "success" ? "text-green-600 dark:text-green-400" : props.color === "warning" ? "text-yellow-600 dark:text-yellow-400" : props.color === "danger" ? "text-red-600 dark:text-red-400" : ""; return ; }, Avatar: ({ props }) => { const name = props.name || "?"; const initials = name .split(" ") .map((n) => n[0]) .join("") .slice(0, 2) .toUpperCase(); const sizeStyles = props.size === "lg" ? { outer: "w-[72px] h-[72px]", text: "text-xl", ring: "ring-[3px]" } : props.size === "sm" ? { outer: "w-8 h-8", text: "text-xs", ring: "ring-2" } : { outer: "w-10 h-10", text: "text-sm", ring: "ring-2" }; return (
{initials}
); }, Badge: ({ props }) => { const variant = props.variant === "success" || props.variant === "warning" ? "secondary" : props.variant === "danger" ? "destructive" : "default"; const customClass = props.variant === "success" ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100" : props.variant === "warning" ? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100" : ""; const dotColor = props.variant === "success" ? "bg-green-500" : props.variant === "warning" ? "bg-yellow-500" : props.variant === "danger" ? "bg-red-500" : ""; return ( {dotColor && ( )} {props.text} ); }, Alert: ({ props }) => { const variant = props.type === "error" ? "destructive" : "default"; const customClass = props.type === "success" ? "border-green-200 bg-green-50 text-green-900 dark:border-green-800 dark:bg-green-950 dark:text-green-100" : props.type === "warning" ? "border-yellow-200 bg-yellow-50 text-yellow-900 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-100" : props.type === "info" ? "border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-100" : ""; const iconProps = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const, className: "shrink-0", }; const icon = props.type === "success" ? ( ) : props.type === "warning" ? ( ) : props.type === "error" ? ( ) : ( ); return ( {icon} {props.title} {props.message && ( {props.message} )} ); }, Progress: ({ props }) => { const value = Math.min(100, Math.max(0, props.value || 0)); return (
{props.label && ( )}
); }, Skeleton: ({ props }) => ( ), Spinner: ({ props }) => { const sizeClass = props.size === "lg" ? "h-8 w-8" : props.size === "sm" ? "h-4 w-4" : "h-6 w-6"; return (
{props.label && ( {props.label} )}
); }, Tooltip: ({ props }) => ( {props.text}

{props.content}

), Popover: ({ props }) => (

{props.content}

), Rating: ({ props, bindings, emit }) => { const [boundValue, setBoundValue] = useBoundProp( props.value as number | undefined, bindings?.value, ); const [localValue, setLocalValue] = useState(props.value || 0); const isBound = !!bindings?.value; const ratingValue = isBound ? (boundValue ?? 0) : localValue; const setValue = isBound ? setBoundValue : setLocalValue; const maxRating = props.max ?? 5; const interactive = props.interactive !== false; const [hoverIndex, setHoverIndex] = useState(-1); return (
{props.label && ( )}
interactive && setHoverIndex(-1)} > {Array.from({ length: maxRating }).map((_, i) => { const filled = hoverIndex >= 0 ? i <= hoverIndex : i < ratingValue; return ( ); })}
); }, Metric: ({ props }) => { const changeColor = props.changeType === "positive" ? "text-green-600 dark:text-green-400" : props.changeType === "negative" ? "text-red-600 dark:text-red-400" : "text-muted-foreground"; const changeIcon = props.changeType === "positive" ? "\u2191" : props.changeType === "negative" ? "\u2193" : ""; return (
{props.label}
{props.prefix} {props.value} {props.suffix} {props.change && ( {changeIcon} {props.change} )}
); }, // ── Charts ──────────────────────────────────────────────────────── BarGraph: ({ props }) => { const data = props.data || []; const maxValue = Math.max(...data.map((d) => d.value), 1); const barColors = [ "bg-primary", "bg-primary/80", "bg-primary/60", "bg-primary/70", "bg-primary/90", "bg-primary/50", ]; return (
{props.title && (
{props.title}
)}
{data.map((d, i) => (
{d.value}
{d.label}
))}
); }, LineGraph: ({ props }) => { const data = props.data || []; const maxValue = Math.max(...data.map((d) => d.value)); const minValue = Math.min(...data.map((d) => d.value)); const range = maxValue - minValue || 1; const width = 300; const height = 140; const padding = { top: 12, right: 12, bottom: 12, left: 12 }; const chartWidth = width - padding.left - padding.right; const chartHeight = height - padding.top - padding.bottom; const points = data.map((d, i) => { const x = padding.left + (data.length > 1 ? (i / (data.length - 1)) * chartWidth : chartWidth / 2); const y = padding.top + chartHeight - ((d.value - minValue) / range) * chartHeight; return { x, y, ...d }; }); // Build smooth cubic bezier curve through points let smoothPath = ""; let areaPath = ""; if (points.length > 1) { const first = points[0]!; const last = points[points.length - 1]!; smoothPath = `M ${first.x} ${first.y}`; for (let i = 0; i < points.length - 1; i++) { const curr = points[i]!; const next = points[i + 1]!; const cpx = (curr.x + next.x) / 2; smoothPath += ` C ${cpx} ${curr.y}, ${cpx} ${next.y}, ${next.x} ${next.y}`; } const bottomY = height - padding.bottom; areaPath = `${smoothPath} L ${last.x} ${bottomY} L ${first.x} ${bottomY} Z`; } else if (points.length === 1) { const only = points[0]!; smoothPath = `M ${only.x} ${only.y}`; } const gradientId = `line-gradient-${Math.random().toString(36).slice(2, 8)}`; return (
{props.title && (
{props.title}
)}
{[0, 0.25, 0.5, 0.75, 1].map((frac) => ( ))} {areaPath && ( )} {smoothPath && ( )} {points.map((p, i) => (
))}
{points.length > 0 && (
{points.map((p, i) => ( {data[i]?.label} ))}
)}
); }, // ── Form Inputs ─────────────────────────────────────────────────── Input: ({ props, bindings, emit }) => { const [boundValue, setBoundValue] = useBoundProp( props.value as string | undefined, bindings?.value, ); const [localValue, setLocalValue] = useState(""); const isBound = !!bindings?.value; const value = isBound ? (boundValue ?? "") : localValue; const setValue = isBound ? setBoundValue : setLocalValue; const hasValidation = !!(bindings?.value && props.checks?.length); const { errors, validate } = useFieldValidation( bindings?.value ?? "", hasValidation ? { checks: props.checks ?? [] } : undefined, ); return (
{props.label && } setValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") emit("submit"); }} onFocus={() => emit("focus")} onBlur={() => { if (hasValidation) validate(); emit("blur"); }} /> {errors.length > 0 && (

{errors[0]}

)}
); }, Textarea: ({ props, bindings }) => { const [boundValue, setBoundValue] = useBoundProp( props.value as string | undefined, bindings?.value, ); const [localValue, setLocalValue] = useState(""); const isBound = !!bindings?.value; const value = isBound ? (boundValue ?? "") : localValue; const setValue = isBound ? setBoundValue : setLocalValue; const hasValidation = !!(bindings?.value && props.checks?.length); const { errors, validate } = useFieldValidation( bindings?.value ?? "", hasValidation ? { checks: props.checks ?? [] } : undefined, ); return (
{props.label && }