UNPKG

oneie

Version:

Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.

1,311 lines (1,138 loc) 32.1 kB
--- title: Demo Components dimension: things category: demo-components.md tags: ai, architecture related_dimensions: connections, events, groups scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the things dimension in the demo-components.md category. Location: one/things/demo-components.md Purpose: Documents one platform demo components Related dimensions: connections, events, groups For AI agents: Read this to understand demo components. --- # ONE Platform Demo Components ## Version 1.0.0 - Component Definition Specification This document defines all reusable components for ONE Platform demo pages. Components follow shadcn/ui patterns with Tailwind v4 styling. --- ## Component Library Architecture ### Hierarchy ``` ┌─ Base Components (shadcn/ui) │ ├─ Button │ ├─ Card │ ├─ Input │ ├─ Select │ ├─ Textarea │ ├─ Badge │ └─ Dialog │ ├─ Demo Components (custom) │ ├─ DemoContainer │ ├─ DemoHero │ ├─ DemoPlayground │ ├─ DemoCodeBlock │ ├─ DemoForm │ ├─ DemoList │ ├─ DemoStats │ ├─ DemoGraph │ └─ DemoCTA │ └─ Feature Components ├─ RelationshipExplorer ├─ CodeExample ├─ EntityCard └─ TimelineEvent ``` --- ## 1. DemoContainer ### Purpose Wrapper component that provides consistent max-width, padding, and spacing for all section content. ### Props ```typescript interface DemoContainerProps { children: ReactNode; className?: string; variant?: "light" | "dark" | "card"; // Background variant noPadding?: boolean; tight?: boolean; // Reduces padding } ``` ### Implementation ```tsx // src/components/demo/DemoContainer.tsx import { cn } from "@/lib/utils"; export function DemoContainer({ children, className, variant = "light", noPadding = false, tight = false, }: DemoContainerProps) { const variants = { light: "bg-background", dark: "bg-foreground/5", card: "bg-card border border-border", }; return ( <div className={cn( "relative", variants[variant], !noPadding && "px-4 py-8 md:px-6 md:py-12 lg:px-8 lg:py-16", tight && "px-4 py-4 md:px-6 md:py-6", className, )} > <div className="max-w-7xl mx-auto">{children}</div> </div> ); } ``` ### Usage ```astro <DemoContainer variant="light"> <h2 class="text-3xl font-bold mb-4">Section Title</h2> <p>Content...</p> </DemoContainer> ``` --- ## 2. DemoHero ### Purpose Hero section component with headline, subheading, and CTAs. ### Props ```typescript interface DemoHeroProps { icon?: ReactNode; badge?: string; title: string; subtitle: string; primaryCta?: { label: string; onClick?: () => void; href?: string; }; secondaryCta?: { label: string; onClick?: () => void; href?: string; }; showScroll?: boolean; // Show scroll indicator } ``` ### Implementation ```tsx // src/components/demo/DemoHero.tsx import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export function DemoHero({ icon, badge, title, subtitle, primaryCta, secondaryCta, showScroll = true, }: DemoHeroProps) { return ( <section className=" relative overflow-hidden px-4 py-20 sm:px-6 sm:py-24 md:px-8 md:py-32 bg-gradient-to-br from-primary/5 via-background to-background " > {/* Background blur */} <div className="absolute inset-0 -z-10"> <div className="absolute left-1/4 top-0 h-96 w-96 rounded-full bg-primary/10 blur-[120px]" ></div> <div className="absolute right-1/3 bottom-0 h-80 w-80 rounded-full bg-accent/10 blur-[120px]" ></div> </div> {/* Content */} <div className="max-w-4xl mx-auto text-center"> {/* Badge */} {badge && ( <div className="inline-flex items-center gap-2 mb-6 px-3 py-1.5 rounded-full bg-primary/10 text-primary" > <span className="w-2 h-2 rounded-full bg-primary"></span> <span className="text-sm font-medium">{badge}</span> </div> )} {/* Icon */} {icon && <div className="mb-6 flex justify-center">{icon}</div>} {/* Title */} <h1 className="text-4xl md:text-5xl lg:text-6xl font-bold leading-tight tracking-tight mb-6 text-foreground" > {title} </h1> {/* Subtitle */} <p className="text-xl md:text-2xl text-muted-foreground mb-12 max-w-2xl mx-auto leading-relaxed" > {subtitle} </p> {/* CTAs */} <div className="flex flex-col sm:flex-row gap-4 justify-center"> {primaryCta && ( <Button variant="default" size="lg" asChild={!!primaryCta.href}> {primaryCta.href ? ( <a href={primaryCta.href}> {primaryCta.label} <span className="ml-2"></span> </a> ) : ( <span onClick={primaryCta.onClick}> {primaryCta.label} <span className="ml-2"></span> </span> )} </Button> )} {secondaryCta && ( <Button variant="outline" size="lg" asChild={!!secondaryCta.href}> {secondaryCta.href ? ( <a href={secondaryCta.href}>{secondaryCta.label}</a> ) : ( <span onClick={secondaryCta.onClick}>{secondaryCta.label}</span> )} </Button> )} </div> </div> {/* Scroll indicator */} {showScroll && ( <div className="flex justify-center mt-12 text-muted-foreground animate-bounce"> <svg className="w-6 h-6" fill="none" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" ></path> </svg> </div> )} </section> ); } ``` ### Usage ```astro <DemoHero badge="The Ontology" title="The Groups Dimension" subtitle="Hierarchical containers for collaboration" primaryCta={{ label: "Start Playground", href: "#playground" }} secondaryCta={{ label: "Read Docs", href: "/docs/groups" }} showScroll /> ``` --- ## 3. DemoPlayground ### Purpose Interactive form + live data display section for hands-on experimentation. ### Props ```typescript interface DemoPlaygroundProps { title: string; description?: string; fields: FormField[]; onSubmit: (data: Record<string, any>) => void; liveData?: Record<string, any>; isLoading?: boolean; stats?: Array<{ label: string; value: string | number }>; tip?: string; } interface FormField { name: string; label: string; type: "text" | "select" | "textarea" | "number" | "email"; placeholder?: string; options?: Array<{ value: string; label: string }>; required?: boolean; } ``` ### Implementation ```tsx // src/components/demo/DemoPlayground.tsx "use client"; import { useState } from "react"; 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; export function DemoPlayground({ title, description, fields, onSubmit, liveData, isLoading, stats, tip, }: DemoPlaygroundProps) { const [formData, setFormData] = useState<Record<string, any>>({}); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(formData); }; return ( <div className="space-y-6"> {/* Header */} <div> <h2 className="text-3xl font-bold mb-2">{title}</h2> {description && <p className="text-muted-foreground">{description}</p>} </div> {/* Main content */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> {/* Form */} <div className="bg-background rounded-lg p-6 border border-border"> <h3 className="text-lg font-semibold mb-4">Create New Entity</h3> <form onSubmit={handleSubmit} className="space-y-4"> {fields.map((field) => ( <div key={field.name} className="flex flex-col"> <Label htmlFor={field.name} className="mb-2"> {field.label} </Label> {field.type === "text" || field.type === "email" || field.type === "number" ? ( <Input id={field.name} type={field.type} placeholder={field.placeholder} value={formData[field.name] || ""} onChange={(e) => setFormData({ ...formData, [field.name]: e.target.value }) } required={field.required} /> ) : field.type === "select" ? ( <Select value={formData[field.name] || ""} onValueChange={(value) => setFormData({ ...formData, [field.name]: value }) } > <SelectTrigger id={field.name}> <SelectValue placeholder={field.placeholder} /> </SelectTrigger> <SelectContent> {field.options?.map((opt) => ( <SelectItem key={opt.value} value={opt.value}> {opt.label} </SelectItem> ))} </SelectContent> </Select> ) : field.type === "textarea" ? ( <Textarea id={field.name} placeholder={field.placeholder} value={formData[field.name] || ""} onChange={(e) => setFormData({ ...formData, [field.name]: e.target.value }) } required={field.required} rows={4} /> ) : null} </div> ))} <Button type="submit" className="w-full" disabled={isLoading}> {isLoading ? "Creating..." : "Create Entity"} → </Button> </form> </div> {/* Live data */} <div className="space-y-4"> <div className="bg-background rounded-lg p-6 border border-border"> <h3 className="text-lg font-semibold mb-4">Live Data (JSON)</h3> <div className=" bg-foreground/5 rounded-md p-4 border border-border overflow-x-auto max-h-80 overflow-y-auto font-mono text-sm text-foreground/80 " > <pre>{JSON.stringify(liveData || {}, null, 2)}</pre> </div> <p className="mt-4 text-xs text-muted-foreground flex items-center gap-2"> <span className="inline-block w-2 h-2 rounded-full bg-green-500"></span> Data updates in real-time </p> </div> {/* Stats */} {stats && stats.length > 0 && ( <div className="grid grid-cols-2 gap-4"> {stats.map((stat) => ( <div key={stat.label} className=" bg-background rounded-lg p-4 border border-border text-center " > <p className="text-2xl font-bold text-primary"> {stat.value} </p> <p className="text-xs text-muted-foreground mt-1"> {stat.label} </p> </div> ))} </div> )} </div> </div> {/* Tip */} {tip && ( <div className="p-4 rounded-lg bg-accent/5 border border-accent/20"> <p className="text-sm text-foreground"> <strong>Pro Tip:</strong> {tip} </p> </div> )} </div> ); } ``` ### Usage ```astro <DemoPlayground title="Try It Yourself" description="Create and explore Things entities in real time" fields={[ { name: 'name', label: 'Entity Name', type: 'text', placeholder: 'Enter entity name', required: true, }, { name: 'type', label: 'Entity Type', type: 'select', options: [ { value: 'course', label: 'Course' }, { value: 'product', label: 'Product' }, { value: 'agent', label: 'Agent' }, ], }, { name: 'description', label: 'Description', type: 'textarea', placeholder: 'Describe this entity...', }, ]} onSubmit={(data) => console.log(data)} liveData={{ id: 'thing_123', name: data.name, type: data.type, createdAt: new Date().toISOString(), }} stats={[ { label: 'Entities Created', value: 42 }, { label: 'Relationships', value: 18 }, ]} tip="Click 'Create Entity' to see real-time updates in the JSON viewer" client:load /> ``` --- ## 4. DemoCodeBlock ### Purpose Syntax-highlighted code example with copy button and metadata. ### Props ```typescript interface DemoCodeBlockProps { code: string; language?: "typescript" | "javascript" | "sql" | "json" | "bash"; title?: string; filename?: string; showLineNumbers?: boolean; height?: "sm" | "md" | "lg" | "auto"; keyPoints?: string[]; relatedDocs?: Array<{ label: string; href: string }>; } ``` ### Implementation ```tsx // src/components/demo/DemoCodeBlock.tsx "use client"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Check, Copy } from "lucide-react"; export function DemoCodeBlock({ code, language = "typescript", title, filename, showLineNumbers = false, height = "md", keyPoints, relatedDocs, }: DemoCodeBlockProps) { const [copied, setCopied] = useState(false); const handleCopy = () => { navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const heightClasses = { sm: "max-h-64", md: "max-h-96", lg: "max-h-screen", auto: "", }; return ( <div className="rounded-lg border border-border overflow-hidden"> {/* Header */} <div className="bg-muted px-6 py-4 border-b border-border flex items-center justify-between" > <div> {title && <h3 className="font-semibold text-foreground">{title}</h3>} {filename && ( <p className="text-xs text-muted-foreground font-mono mt-1"> {filename} </p> )} </div> <span className="text-xs font-mono text-muted-foreground"> {language.toUpperCase()} </span> </div> {/* Code */} <div className={` relative overflow-hidden ${heightClasses[height]} overflow-y-auto `} > <pre className="p-6 bg-foreground/5 border-t border-border overflow-x-auto text-sm font-mono text-foreground/80 whitespace-pre-wrap break-words" > <code>{code}</code> </pre> {/* Copy button */} <button onClick={handleCopy} className="absolute top-4 right-4 px-3 py-1.5 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:bg-primary/90 transition-colors duration-200 focus-visible:outline-2 focus-visible:outline-ring flex items-center gap-2" aria-label="Copy code" > {copied ? ( <> <Check className="w-4 h-4" /> Copied </> ) : ( <> <Copy className="w-4 h-4" /> Copy </> )} </button> </div> {/* Footer */} <div className="px-6 py-4 bg-background border-t border-border space-y-4 text-sm" > {keyPoints && keyPoints.length > 0 && ( <div> <p className="font-semibold text-foreground mb-2">Key Points:</p> <ul className="ml-4 space-y-1 text-muted-foreground list-disc"> {keyPoints.map((point, i) => ( <li key={i}>{point}</li> ))} </ul> </div> )} {relatedDocs && relatedDocs.length > 0 && ( <div> <p className="font-semibold text-foreground mb-2">Related:</p> <div className="flex flex-wrap gap-2"> {relatedDocs.map((doc) => ( <a key={doc.href} href={doc.href} className="px-2.5 py-1.5 rounded-md bg-primary/10 text-primary text-xs font-medium hover:bg-primary/20 transition-colors" > {doc.label} </a> ))} </div> </div> )} </div> </div> ); } ``` ### Usage ```astro <DemoCodeBlock code={`export const create = mutation({ args: { name: v.string(), type: v.string() }, handler: async (ctx, args) => { return await ctx.db.insert('things', { type: args.type, name: args.name, status: 'draft', createdAt: Date.now(), }); }, });`} language="typescript" title="Create Thing Example" filename="convex/mutations/things.ts" height="md" keyPoints={[ 'All entities stored in things table', 'Type system enforces correctness', 'Mutations return entity ID', ]} relatedDocs={[ { label: 'Mutations Guide', href: '/docs/mutations' }, { label: 'Things Spec', href: '/docs/things' }, ]} /> ``` --- ## 5. DemoStats ### Purpose Display statistics and metrics in card format. ### Props ```typescript interface DemoStatsProps { stats: Array<{ label: string; value: string | number; description?: string; icon?: ReactNode; trend?: "up" | "down" | "neutral"; trendValue?: string; }>; columns?: 1 | 2 | 3 | 4; } ``` ### Implementation ```tsx // src/components/demo/DemoStats.tsx import { cn } from "@/lib/utils"; import { TrendingUp, TrendingDown } from "lucide-react"; export function DemoStats({ stats, columns = 3 }: DemoStatsProps) { const gridClasses = { 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-2 lg:grid-cols-4", }; return ( <div className={cn("grid gap-4", gridClasses[columns])}> {stats.map((stat, i) => ( <div key={i} className=" rounded-lg bg-background border border-border p-6 transition-all duration-300 hover:shadow-md hover:-translate-y-1 " > {/* Icon */} {stat.icon && <div className="mb-4 text-primary">{stat.icon}</div>} {/* Value */} <p className="text-3xl font-bold text-foreground">{stat.value}</p> {/* Label */} <p className="text-sm text-muted-foreground mt-1">{stat.label}</p> {/* Trend */} {stat.trendValue && ( <div className="mt-4 flex items-center gap-2"> {stat.trend === "up" && ( <TrendingUp className="w-4 h-4 text-green-500" /> )} {stat.trend === "down" && ( <TrendingDown className="w-4 h-4 text-red-500" /> )} <span className={cn( "text-xs font-medium", stat.trend === "up" ? "text-green-600" : stat.trend === "down" ? "text-red-600" : "text-muted-foreground", )} > {stat.trendValue} </span> </div> )} {/* Description */} {stat.description && ( <p className="text-xs text-muted-foreground mt-3"> {stat.description} </p> )} </div> ))} </div> ); } ``` ### Usage ```astro <DemoStats columns={3} stats={[ { label: 'Total Entities', value: '1,247', trend: 'up', trendValue: '+12.5%', icon: <Database className="w-5 h-5" /> }, { label: 'Active Connections', value: '843', trend: 'up', trendValue: '+8.2%', icon: <Network className="w-5 h-5" /> }, { label: 'Events Logged', value: '5,340', trend: 'up', trendValue: '+23.1%', icon: <Activity className="w-5 h-5" /> }, ]} /> ``` --- ## 6. DemoForm ### Purpose Reusable form component with validation and error handling. ### Props ```typescript interface DemoFormProps { fields: FormField[]; onSubmit: (data: Record<string, any>) => Promise<void>; isLoading?: boolean; submitLabel?: string; successMessage?: string; } ``` ### Implementation ```tsx // src/components/demo/DemoForm.tsx "use client"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { AlertCircle, CheckCircle } from "lucide-react"; export function DemoForm({ fields, onSubmit, isLoading = false, submitLabel = "Submit", successMessage = "Form submitted successfully!", }: DemoFormProps) { const [formData, setFormData] = useState<Record<string, any>>({}); const [errors, setErrors] = useState<Record<string, string>>({}); const [success, setSuccess] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrors({}); try { await onSubmit(formData); setSuccess(true); setTimeout(() => setSuccess(false), 3000); setFormData({}); } catch (error: any) { setErrors({ submit: error.message || "An error occurred", }); } }; return ( <form onSubmit={handleSubmit} className="space-y-6"> {/* Form fields */} {fields.map((field) => ( <div key={field.name} className="flex flex-col"> <Label htmlFor={field.name} className="mb-2 font-medium"> {field.label} {field.required && <span className="text-destructive ml-1">*</span>} </Label> {field.type === "textarea" ? ( <textarea id={field.name} placeholder={field.placeholder} value={formData[field.name] || ""} onChange={(e) => setFormData({ ...formData, [field.name]: e.target.value }) } required={field.required} className=" px-4 py-2.5 rounded-md border border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring font-sans " rows={4} /> ) : ( <Input id={field.name} type={field.type} placeholder={field.placeholder} value={formData[field.name] || ""} onChange={(e) => setFormData({ ...formData, [field.name]: e.target.value }) } required={field.required} /> )} {/* Field error */} {errors[field.name] && ( <p className="mt-2 text-sm text-destructive flex items-center gap-1"> <AlertCircle className="w-4 h-4" /> {errors[field.name]} </p> )} </div> ))} {/* Submit error */} {errors.submit && ( <div className="p-4 rounded-lg bg-destructive/10 border border-destructive/20 text-destructive text-sm flex items-start gap-3" > <AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5" /> <p>{errors.submit}</p> </div> )} {/* Success message */} {success && ( <div className="p-4 rounded-lg bg-green-500/10 border border-green-500/20 text-green-700 text-sm flex items-start gap-3" > <CheckCircle className="w-5 h-5 flex-shrink-0 mt-0.5" /> <p>{successMessage}</p> </div> )} {/* Submit button */} <Button type="submit" disabled={isLoading} className="w-full"> {isLoading ? "Submitting..." : submitLabel} </Button> </form> ); } ``` --- ## 7. DemoList ### Purpose Display list of items with optional filtering and sorting. ### Props ```typescript interface DemoListProps<T> { items: T[]; renderItem: (item: T, index: number) => ReactNode; emptyMessage?: string; columns?: 1 | 2 | 3; gap?: "sm" | "md" | "lg"; } ``` ### Implementation ```tsx // src/components/demo/DemoList.tsx import { cn } from "@/lib/utils"; export function DemoList<T>({ items, renderItem, emptyMessage = "No items found", columns = 1, gap = "md", }: DemoListProps<T>) { const columnClasses = { 1: "grid-cols-1", 2: "grid-cols-1 md:grid-cols-2", 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3", }; const gapClasses = { sm: "gap-3", md: "gap-4", lg: "gap-6", }; if (items.length === 0) { return ( <div className="py-12 text-center"> <p className="text-muted-foreground">{emptyMessage}</p> </div> ); } return ( <div className={cn("grid", columnClasses[columns], gapClasses[gap])}> {items.map((item, index) => renderItem(item, index))} </div> ); } ``` --- ## 8. DemoGraph ### Purpose Interactive relationship graph visualization component. ### Props ```typescript interface DemoGraphProps { data: GraphData; onNodeClick?: (nodeId: string) => void; height?: string; enableZoom?: boolean; enablePan?: boolean; theme?: "light" | "dark"; } interface GraphData { nodes: Array<{ id: string; label: string; type: string; color?: string; }>; edges: Array<{ from: string; to: string; label?: string; }>; } ``` ### Implementation ```tsx // src/components/demo/DemoGraph.tsx "use client"; import { useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; export function DemoGraph({ data, onNodeClick, height = "500px", enableZoom = true, enablePan = true, theme = "light", }: DemoGraphProps) { const containerRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!containerRef.current) return; // Initialize graph visualization library // (Vis.js, D3.js, or Three.js implementation) // This is a placeholder for the actual implementation console.log("Graph data:", data); }, [data, onNodeClick, enableZoom, enablePan, theme]); return ( <div ref={containerRef} className={cn( "rounded-lg border border-border bg-background", "transition-all duration-300", "overflow-hidden", )} style={{ height }} > {/* Graph rendered here via vis-network or similar */} <canvas className="w-full h-full" /> </div> ); } ``` --- ## 9. DemoCTA ### Purpose Call-to-action section with headline, description, and buttons. ### Props ```typescript interface DemoCTAProps { headline: string; description?: string; icon?: ReactNode; primaryCta: { label: string; href?: string; onClick?: () => void; }; secondaryCta?: { label: string; href?: string; onClick?: () => void; }; stats?: Array<{ label: string; value: string }>; bgGradient?: "primary" | "accent" | "secondary"; } ``` ### Implementation ```tsx // src/components/demo/DemoCTA.tsx import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export function DemoCTA({ headline, description, icon, primaryCta, secondaryCta, stats, bgGradient = "primary", }: DemoCTAProps) { const bgClasses = { primary: "bg-gradient-to-br from-primary to-primary/80 text-primary-foreground", accent: "bg-gradient-to-br from-accent to-accent/80 text-accent-foreground", secondary: "bg-gradient-to-br from-secondary to-secondary/80 text-secondary-foreground", }; return ( <section className={cn( "relative overflow-hidden", "px-4 py-20 md:px-6 md:py-28", bgClasses[bgGradient], )} > {/* Content */} <div className="max-w-4xl mx-auto text-center"> {icon && ( <div className="mb-6 flex justify-center opacity-75">{icon}</div> )} <h2 className="text-4xl md:text-5xl font-bold leading-tight mb-6"> {headline} </h2> {description && ( <p className="text-xl opacity-90 mb-8 max-w-2xl mx-auto"> {description} </p> )} {/* CTAs */} <div className="flex flex-col sm:flex-row gap-4 justify-center mb-12"> <Button variant="secondary" size="lg" asChild={!!primaryCta.href}> {primaryCta.href ? ( <a href={primaryCta.href}>{primaryCta.label} →</a> ) : ( <span onClick={primaryCta.onClick}>{primaryCta.label} →</span> )} </Button> {secondaryCta && ( <Button variant="outline" size="lg" asChild={!!secondaryCta.href}> {secondaryCta.href ? ( <a href={secondaryCta.href}>{secondaryCta.label}</a> ) : ( <span onClick={secondaryCta.onClick}>{secondaryCta.label}</span> )} </Button> )} </div> {/* Stats */} {stats && stats.length > 0 && ( <div className="grid grid-cols-2 md:grid-cols-3 gap-6 pt-8 border-t border-opacity-20"> {stats.map((stat) => ( <div key={stat.label}> <p className="text-3xl font-bold">{stat.value}</p> <p className="text-sm opacity-75 mt-2">{stat.label}</p> </div> ))} </div> )} </div> </section> ); } ``` --- ## Component Usage Guidelines ### Import Pattern ```tsx import { DemoHero } from "@/components/demo/DemoHero"; import { DemoPlayground } from "@/components/demo/DemoPlayground"; import { DemoCodeBlock } from "@/components/demo/DemoCodeBlock"; import { DemoStats } from "@/components/demo/DemoStats"; import { DemoCTA } from "@/components/demo/DemoCTA"; ``` ### Composition Pattern ```astro <DemoContainer> <DemoHero ... /> </DemoContainer> <DemoContainer variant="card"> <DemoPlayground client:load ... /> </DemoContainer> <DemoContainer> <DemoCodeBlock ... /> <DemoCodeBlock ... /> </DemoContainer> <DemoCTA ... /> ``` ### Client vs Server - **Server (Astro):** DemoContainer, DemoHero, DemoCodeBlock, DemoCTA, DemoStats - **Client (React):** DemoPlayground, DemoForm, DemoList, DemoGraph (add `client:load`) --- ## Version History | Version | Date | Changes | | ------- | -------- | -------------------------------- | | 1.0.0 | Oct 2024 | Initial component specifications | --- **Components Maintained By:** Design Agent **Last Updated:** October 25, 2024 **Status:** Production Ready