"use client" import { useState, useEffect, useCallback, useRef } from "react" import type React from "react" import { cn } from "@/lib/utils" import { useTheme } from "next-themes" import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" import { Copy, Check, Settings, ChevronDown } from "lucide-react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Label } from "@/components/ui/label" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" interface ComponentProperty { name: string type: "select" | "boolean" | "text" | "textarea" | "number" options?: string[] defaultValue: any label: string description?: string placeholder?: string } interface ComponentContainerProps { title?: string description?: string code?: string children: React.ReactNode className?: string defaultTab?: "preview" | "code" language?: string properties?: ComponentProperty[] renderComponent?: (props: Record) => React.ReactNode generateCode?: (props: Record) => string } interface Token { type: string content: string index: number } // Helper function to get display value from actual value const getDisplayValue = (actualValue: string): string => { const displayMap: Record = { default: "Primary", // Map default to Primary for button variant secondary: "Secondary", destructive: "Destructive", outline: "Outline", ghost: "Ghost", link: "Link", sm: "Small", lg: "Large", icon: "Icon", success: "Success", warning: "Warning", info: "Info", error: "Error", neutral: "Neutral", xl: "Extra Large", left: "Left", right: "Right", none: "None", } return displayMap[actualValue] || actualValue.charAt(0).toUpperCase() + actualValue.slice(1) } // Helper function to get actual value from display value const getActualValue = (displayValue: string): string => { const valueMap: Record = { Primary: "default", // Map Primary to default for button variant Default: "default", Secondary: "secondary", Destructive: "destructive", Outline: "outline", Ghost: "ghost", Link: "link", Small: "sm", Large: "lg", Icon: "icon", Success: "success", Warning: "warning", Info: "info", Error: "error", Neutral: "neutral", "Extra Large": "xl", Left: "left", Right: "right", None: "none", } return valueMap[displayValue] || displayValue.toLowerCase() } export function ComponentContainer({ title = "", description, code = "", children, className, defaultTab = "preview", language = "tsx", properties = [], renderComponent, generateCode, }: ComponentContainerProps) { const { theme } = useTheme() const [mounted, setMounted] = useState(false) const [copied, setCopied] = useState(false) const [showProperties, setShowProperties] = useState(false) const [componentProps, setComponentProps] = useState>({}) // Use ref to track if we've initialized props to prevent infinite loops const initializedRef = useRef(false) const propertiesRef = useRef([]) // Initialize props with default values immediately useEffect(() => { // Check if properties have actually changed const propertiesChanged = properties.length !== propertiesRef.current.length || properties.some((prop, index) => { const prevProp = propertiesRef.current[index] return ( !prevProp || prop.name !== prevProp.name || prop.defaultValue !== prevProp.defaultValue || prop.type !== prevProp.type ) }) if (!initializedRef.current || propertiesChanged) { const initialProps: Record = {} properties.forEach((prop) => { // For select properties, we need to store the display value that matches the options if (prop.type === "select" && prop.options) { const displayValue = getDisplayValue(prop.defaultValue) // Make sure the display value exists in the options if (prop.options.includes(displayValue)) { initialProps[prop.name] = displayValue } else { // Fallback to the first option if display value not found initialProps[prop.name] = prop.options[0] || prop.defaultValue } } else { initialProps[prop.name] = prop.defaultValue } }) setComponentProps(initialProps) propertiesRef.current = [...properties] initializedRef.current = true } }, [properties]) useEffect(() => { setMounted(true) }, []) const copyToClipboard = useCallback( (textToCopy?: string) => { const finalCode = textToCopy || (generateCode ? generateCode(componentProps) : code) navigator.clipboard.writeText(finalCode || "") setCopied(true) setTimeout(() => setCopied(false), 2000) }, [componentProps, generateCode, code], ) const updateProperty = useCallback((name: string, value: any) => { setComponentProps((prev) => ({ ...prev, [name]: value, })) }, []) // VSCode-style token colors const getTokenStyle = useCallback((type: string): string => { const styles: Record = { comment: "text-[#6A9955] italic", string: "text-[#CE9178]", keyword: "text-[#569CD6]", type: "text-[#4EC9B0]", number: "text-[#B5CEA8]", function: "text-[#DCDCAA]", property: "text-[#9CDCFE]", jsx: "text-[#4FC1FF]", jsxAttribute: "text-[#9CDCFE]", operator: "text-[#D4D4D4]", bracket: "text-[#D4D4D4]", variable: "text-[#9CDCFE]", default: "text-[#D4D4D4]", } return styles[type] || styles.default }, []) const tokenize = useCallback((line: string): Token[] => { const tokens: Token[] = [] const tokenized = new Array(line.length).fill(false) const patterns = [ { type: "comment", regex: /(\/\/.*$|\/\*[\s\S]*?\*\/)/g }, { type: "string", regex: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g }, { type: "jsx", regex: /(<\/?[A-Z][a-zA-Z0-9]*(?:\s[^>]*)?\/?>|<>|<\/>)/g }, { type: "jsxAttribute", regex: /(\w+)(?==)/g }, { type: "keyword", regex: /\b(const|let|var|function|return|if|else|for|while|import|export|from|default|interface|type|extends|implements|class|public|private|protected|static|async|await|try|catch|finally|throw|new|this|super|null|undefined|true|false|break|continue|switch|case|do)\b/g, }, { type: "type", regex: /\b(string|number|boolean|object|any|void|never|unknown|React\.ReactNode|React\.FC|HTMLElement|Event|Promise|Array|Map|Set)\b/g, }, { type: "number", regex: /\b\d+\.?\d*\b/g }, { type: "function", regex: /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g }, { type: "property", regex: /\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g }, { type: "variable", regex: /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g }, { type: "operator", regex: /[+\-*/%=<>!&|?:]/g }, { type: "bracket", regex: /[{}[\]()]/g }, ] patterns.forEach(({ type, regex }) => { let match const globalRegex = new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : regex.flags + "g") while ((match = globalRegex.exec(line)) !== null) { const start = match.index! const end = start + match[0].length let canTokenize = true for (let i = start; i < end; i++) { if (tokenized[i]) { canTokenize = false break } } if (canTokenize) { tokens.push({ type, content: match[0], index: start, }) for (let i = start; i < end; i++) { tokenized[i] = true } } } }) let currentStart = 0 for (let i = 0; i <= line.length; i++) { if (i === line.length || tokenized[i]) { if (i > currentStart) { const content = line.slice(currentStart, i) if (content) { tokens.push({ type: "default", content, index: currentStart, }) } } while (i < line.length && tokenized[i]) { i++ } currentStart = i } } return tokens.sort((a, b) => a.index - b.index) }, []) const renderHighlightedLine = useCallback( (line: string, lineIndex: number) => { if (!line.trim()) { return   } const indentMatch = line.match(/^(\s*)/) const indentSpaces = indentMatch ? indentMatch[1] : "" const trimmedLine = line.slice(indentSpaces.length) const tokens = tokenize(trimmedLine) return ( {indentSpaces && {indentSpaces.replace(/ /g, "\u00A0")}} {tokens.map((token, tokenIndex) => ( {token.content} ))} ) }, [tokenize, getTokenStyle], ) const codeWithLineNumbers = useCallback( (codeToDisplay?: string) => { const displayCode = codeToDisplay || (generateCode ? generateCode(componentProps) : code) if (!displayCode) return [
1 // No code provided
, ] const lines = displayCode.split("\n") return lines.map((line, i) => { return (
{i + 1} {renderHighlightedLine(line, i)}
) }) }, [componentProps, generateCode, code, renderHighlightedLine], ) const renderPropertyControl = useCallback( (property: ComponentProperty) => { const value = componentProps[property.name] switch (property.type) { case "select": return (
) case "boolean": return (
updateProperty(property.name, checked)} />
) case "textarea": return (