"use client";
import { Children, useEffect, 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,
TabsContent,
} 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"
: "";
// Horizontal stacks wrap by default; wrap:false keeps a single scrolling
// row (Kanban boards / column layouts that must sit side by side).
const horizontalFlow =
props.wrap === false
? "flex-row flex-nowrap overflow-x-auto w-full [&>*]:shrink-0"
: "flex-row flex-wrap";
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 >= 7
? "grid-cols-7"
: 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, children }) => {
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;
// Children map positionally to tabs: children[i] is the panel for tabs[i].
const panels = Children.toArray(children);
return (
{
setValue(v);
emit("change");
}}
>
{tabs.map((tab) => (
{tab.label}
))}
{tabs.map((tab, i) => (
{panels[i] ?? null}
))}
);
},
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 }) => {
const src = typeof props.src === "string" ? props.src.trim() : "";
if (src) {
return (
// eslint-disable-next-line @next/next/no-img-element
);
}
return (
{props.alt &&
{props.alt}}
);
},
Map: ({ props }) => {
const query = String(props.query ?? "");
const zoom = typeof props.zoom === "number" ? props.zoom : 14;
const height = typeof props.height === "number" ? props.height : 320;
const src = `https://maps.google.com/maps?q=${encodeURIComponent(
query,
)}&z=${zoom}&output=embed`;
return (
);
},
Pressable: ({ children, emit }) => (
),
// Self-contained photo gallery + lightbox. Manages its own open/index state
// with React useState — does NOT depend on the json-render setState action.
Lightbox: ({ props }) => {
const images = (
Array.isArray(props.images) ? props.images : []
) as Array<{ src: string; caption?: string | null }>;
const cols =
props.columns === 2
? "grid-cols-2"
: props.columns === 4
? "grid-cols-4"
: props.columns === 5
? "grid-cols-5"
: props.columns === 6
? "grid-cols-6"
: "grid-cols-3";
const [openIndex, setOpenIndex] = useState(null);
const current = openIndex !== null ? images[openIndex] : undefined;
useEffect(() => {
if (openIndex === null) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpenIndex(null);
if (e.key === "ArrowLeft")
setOpenIndex((i) =>
i === null ? i : (i - 1 + images.length) % images.length,
);
if (e.key === "ArrowRight")
setOpenIndex((i) => (i === null ? i : (i + 1) % images.length));
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [openIndex, images.length]);
return (
<>
{images.map((img, i) => (
))}
{current && (
setOpenIndex(null)}
>
{images.length > 1 && (
)}
e.stopPropagation()}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
{current.caption && (
{current.caption}
{(openIndex ?? 0) + 1} / {images.length}
)}
{images.length > 1 && (
)}
)}
>
);
},
// Self-contained modal: renders its own trigger button and manages open
// state with React useState — no setState / openPath / Dialog needed.
Modal: ({ props, children }) => {
const [open, setOpen] = useState(false);
const variant =
props.triggerVariant === "outline"
? "outline"
: props.triggerVariant === "secondary"
? "secondary"
: props.triggerVariant === "danger"
? "destructive"
: "default";
const sizeClass =
props.size === "lg"
? "max-w-2xl"
: props.size === "sm"
? "max-w-sm"
: "max-w-lg";
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
return (
<>
{open && (
setOpen(false)}
>
e.stopPropagation()}
>
{props.title && (
{props.title}
)}
{props.description && (
{props.description}
)}
{children &&
{children}
}
)}
>
);
},
Icon: ({ props }) => {
const IconComponent = lucideIcons[props.name as keyof typeof lucideIcons];
if (!IconComponent) return null;
const sizeMap = {
sm: 16,
md: 20,
lg: 24,
xl: 32,
"2xl": 48,
"3xl": 64,
} 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 (
);
})}
);
},
Timeline: ({ props }) => (
{(props.items ?? []).map((item, i) => {
const dotColor =
item.status === "completed"
? "bg-emerald-500"
: item.status === "current"
? "bg-blue-500"
: "bg-muted-foreground/30";
return (
{item.title}
{item.date && (
{item.date}
)}
{item.description && (
{item.description}
)}
);
})}
),
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) => (
))}
);
},
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}
)}
{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 &&
}
);
},
Select: ({ 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 rawOptions = props.options ?? [];
// Coerce options to strings – AI may produce objects/numbers instead of
// plain strings which would cause duplicate `[object Object]` keys.
const options = rawOptions.map((opt) =>
typeof opt === "string" ? opt : String(opt ?? ""),
);
const hasValidation = !!(bindings?.value && props.checks?.length);
const { errors, validate } = useFieldValidation(
bindings?.value ?? "",
hasValidation ? { checks: props.checks ?? [] } : undefined,
);
return (
{errors.length > 0 && (
{errors[0]}
)}
);
},
Checkbox: ({ props, bindings, emit }) => {
const [boundChecked, setBoundChecked] = useBoundProp(
props.checked as boolean | undefined,
bindings?.checked,
);
const [localChecked, setLocalChecked] = useState(!!props.checked);
const isBound = !!bindings?.checked;
const checked = isBound ? (boundChecked ?? false) : localChecked;
const setChecked = isBound ? setBoundChecked : setLocalChecked;
return (
{
setChecked(c === true);
emit("change");
}}
/>
);
},
Radio: ({ props, bindings, emit }) => {
const rawOptions = props.options ?? [];
const options = rawOptions.map((opt) =>
typeof opt === "string" ? opt : String(opt ?? ""),
);
const [boundValue, setBoundValue] = useBoundProp(
props.value as string | undefined,
bindings?.value,
);
const [localValue, setLocalValue] = useState(options[0] ?? "");
const isBound = !!bindings?.value;
const value = isBound ? (boundValue ?? "") : localValue;
const setValue = isBound ? setBoundValue : setLocalValue;
return (
{props.label &&
}
{
setValue(v);
emit("change");
}}
>
{options.map((opt, idx) => (
))}
);
},
Switch: ({ props, bindings, emit }) => {
const [boundChecked, setBoundChecked] = useBoundProp(
props.checked as boolean | undefined,
bindings?.checked,
);
const [localChecked, setLocalChecked] = useState(!!props.checked);
const isBound = !!bindings?.checked;
const checked = isBound ? (boundChecked ?? false) : localChecked;
const setChecked = isBound ? setBoundChecked : setLocalChecked;
return (
{
setChecked(c);
emit("change");
}}
/>
);
},
Slider: ({ props, bindings, emit }) => {
const [boundValue, setBoundValue] = useBoundProp(
props.value as number | undefined,
bindings?.value,
);
const [localValue, setLocalValue] = useState(props.min ?? 0);
const isBound = !!bindings?.value;
const value = isBound ? (boundValue ?? props.min ?? 0) : localValue;
const setValue = isBound ? setBoundValue : setLocalValue;
return (
{props.label && (
{value}
)}
{
setValue(v[0] ?? 0);
emit("change");
}}
/>
);
},
// ── Actions ───────────────────────────────────────────────────────
Button: ({ props, emit }) => {
const variant =
props.variant === "danger"
? "destructive"
: props.variant === "outline"
? "outline"
: props.variant === "secondary"
? "secondary"
: "default";
return (
);
},
Link: ({ props, emit }) => (
),
DropdownMenu: ({ props, emit }) => {
const items = props.items ?? [];
return (
{items.map((item) => (
emit("select")}>
{item.label}
))}
);
},
Toggle: ({ props, bindings, emit }) => {
const [boundPressed, setBoundPressed] = useBoundProp(
props.pressed as boolean | undefined,
bindings?.pressed,
);
const [localPressed, setLocalPressed] = useState(props.pressed ?? false);
const isBound = !!bindings?.pressed;
const pressed = isBound ? (boundPressed ?? false) : localPressed;
const setPressed = isBound ? setBoundPressed : setLocalPressed;
return (
{
setPressed(v);
emit("change");
}}
>
{props.label}
);
},
ToggleGroup: ({ props, bindings, emit }) => {
const type = props.type ?? "single";
const items = props.items ?? [];
const [boundValue, setBoundValue] = useBoundProp(
props.value as string | undefined,
bindings?.value,
);
const [localValue, setLocalValue] = useState(items[0]?.value ?? "");
const isBound = !!bindings?.value;
const value = isBound ? (boundValue ?? "") : localValue;
const setValue = isBound ? setBoundValue : setLocalValue;
if (type === "multiple") {
return (
{items.map((item) => (
{item.label}
))}
);
}
return (
{
if (v) {
setValue(v);
emit("change");
}
}}
>
{items.map((item) => (
{item.label}
))}
);
},
ButtonGroup: ({ props, bindings, emit }) => {
const buttons = props.buttons ?? [];
const [boundSelected, setBoundSelected] = useBoundProp(
props.selected as string | undefined,
bindings?.selected,
);
const [localValue, setLocalValue] = useState(buttons[0]?.value ?? "");
const isBound = !!bindings?.selected;
const value = isBound ? (boundSelected ?? "") : localValue;
const setValue = isBound ? setBoundSelected : setLocalValue;
return (
{buttons.map((btn, i) => (
))}
);
},
Pagination: ({ props, bindings, emit }) => {
const [boundPage, setBoundPage] = useBoundProp(
props.page as number | undefined,
bindings?.page,
);
const currentPage = boundPage ?? 1;
const pages = Array.from({ length: props.totalPages }, (_, i) => i + 1);
return (
{
e.preventDefault();
if (currentPage > 1) {
setBoundPage(currentPage - 1);
emit("change");
}
}}
/>
{pages.map((page) => (
{
e.preventDefault();
setBoundPage(page);
emit("change");
}}
>
{page}
))}
{
e.preventDefault();
if (currentPage < props.totalPages) {
setBoundPage(currentPage + 1);
emit("change");
}
}}
/>
);
},
},
actions: {
// Built-in state actions — handled by ActionProvider, stubs needed for types
setState: async () => {},
pushState: async () => {},
removeState: async () => {},
// Demo actions — show toasts
buttonClick: async (params) => {
const message = (params?.message as string) || "Button clicked!";
toast.success(message);
},
formSubmit: async (params) => {
const formName = (params?.formName as string) || "Form";
toast.success(`${formName} submitted successfully!`);
},
linkClick: async (params) => {
const href = (params?.href as string) || "#";
toast.info(`Navigating to: ${href}`);
},
},
});
// Fallback component for unknown types
export function Fallback({ type }: { type: string }) {
return [{type}]
;
}