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,303 lines (1,111 loc) 35 kB
--- title: Ontology Ui Shadcn dimension: things category: products tags: 6-dimensions, ai, ontology, things related_dimensions: events 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 products category. Location: one/things/products/ontology-ui-shadcn.md Purpose: Documents ontology ui with shadcn/ui integration Related dimensions: events For AI agents: Read this to understand ontology ui shadcn. --- # Ontology UI with shadcn/ui Integration > **⚠️ DEPRECATED:** This document has been superseded by a cleaner approach. Please use: > > - `one/things/features/ontology-ui.md` - Clean UI metadata specification > - `one/things/components/card.md` - Implementation using shadcn/ui naturally > - `one/things/plans/ontology-ui-approach.md` - Philosophy explanation > > **Why the change?** The new approach keeps ontology-ui.md as a clean specification and uses shadcn/ui as an implementation detail in card.md, resulting in 50% less documentation with no redundancy. **Ontology-driven UI using shadcn/ui components for consistent, accessible design** --- ## Overview The ONE ontology UI specification is fully integrated with [shadcn/ui](https://ui.shadcn.com/) - a collection of beautifully designed, accessible components built with Radix UI and Tailwind CSS. **Why shadcn/ui:** - ✅ **Accessible** - Built on Radix UI primitives (WAI-ARIA compliant) - ✅ **Customizable** - Full control via Tailwind CSS - ✅ **Copy-paste** - Not an npm dependency, you own the code - ✅ **TypeScript** - Fully typed components - ✅ **Consistent** - Professional design system out of the box - ✅ **Well-documented** - Comprehensive docs and examples **Integration Benefits:** - ✅ Every field component maps to shadcn/ui component - ✅ Consistent design across all 66 thing types - ✅ Accessible by default (screen readers, keyboard nav) - ✅ Theme support (light/dark mode) - ✅ Responsive and mobile-friendly --- ## Setup ### 1. Install shadcn/ui ```bash cd frontend # Initialize shadcn/ui npx shadcn-ui@latest init # Choose: # Style: Default # Base color: Slate # CSS variables: Yes ``` ### 2. Install Required Components ```bash # Core components npx shadcn-ui@latest add card npx shadcn-ui@latest add button npx shadcn-ui@latest add badge npx shadcn-ui@latest add avatar npx shadcn-ui@latest add separator # Form components npx shadcn-ui@latest add input npx shadcn-ui@latest add textarea npx shadcn-ui@latest add checkbox npx shadcn-ui@latest add switch npx shadcn-ui@latest add select # Display components npx shadcn-ui@latest add table npx shadcn-ui@latest add dialog npx shadcn-ui@latest add tooltip npx shadcn-ui@latest add dropdown-menu npx shadcn-ui@latest add popover # Data components npx shadcn-ui@latest add skeleton npx shadcn-ui@latest add progress npx shadcn-ui@latest add calendar ``` ### 3. Project Structure ``` frontend/ ├── src/ │ ├── components/ │ │ ├── ui/ # shadcn/ui components (auto-generated) │ │ │ ├── card.tsx │ │ │ ├── button.tsx │ │ │ ├── badge.tsx │ │ │ └── ... │ │ ├── generic/ # Ontology-driven components │ │ │ ├── ThingCard.tsx │ │ │ ├── Field.tsx │ │ │ └── Actions.tsx │ │ └── fields/ # Field-specific components │ │ ├── Heading.tsx │ │ ├── Text.tsx │ │ ├── Price.tsx │ │ └── ... │ ├── ontology/ │ │ ├── config/ # UI configs for all types │ │ │ ├── course.ts │ │ │ ├── product.ts │ │ │ └── ... │ │ └── hooks/ │ │ └── useThingConfig.ts │ └── lib/ │ └── utils.ts # shadcn/ui utilities ``` --- ## Field Component Mapping Every ontology field component maps to a shadcn/ui component: | Field Component | shadcn/ui Component | Usage | | --------------- | --------------------------- | --------------------------- | | `Heading` | Custom (Typography) | Headings with size variants | | `Text` | Custom (Typography) | Body text with truncation | | `Price` | `Badge` + Custom formatting | Formatted prices | | `Image` | Custom `<img>` + `Skeleton` | Lazy-loaded images | | `Badge` | `Badge` | Status badges | | `TagList` | Multiple `Badge` | Tag collections | | `Date` | Custom formatting | Formatted dates | | `Avatar` | `Avatar` | User avatars | | `Button` | `Button` | Action buttons | | `Link` | Custom `<a>` | Hyperlinks | | `Checkbox` | `Checkbox` | Checkboxes | | `Switch` | `Switch` | Toggle switches | | `Select` | `Select` | Dropdowns | | `Input` | `Input` | Text inputs | | `Textarea` | `Textarea` | Multi-line text | | `Rating` | Custom stars | Star ratings | | `Progress` | `Progress` | Progress bars | | `Table` | `Table` | Data tables | | `Dialog` | `Dialog` | Modal dialogs | | `Tooltip` | `Tooltip` | Hover tooltips | --- ## ThingCard with shadcn/ui ### Implementation ```tsx // frontend/src/components/generic/ThingCard.tsx import { type Thing } from "@one-platform/core"; import { useThingConfig } from "@/ontology/hooks/useThingConfig"; import { Card, CardContent, CardFooter, CardHeader, } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { Field } from "./Field"; import { Actions } from "./Actions"; import { ConnectionBadges } from "./ConnectionBadges"; import { cn } from "@/lib/utils"; interface ThingCardProps { thing: Thing; view?: "card" | "list" | "detail"; onClick?: (thing: Thing) => void; showActions?: boolean; showConnections?: boolean; className?: string; } export function ThingCard({ thing, view = "card", onClick, showActions = true, showConnections = true, className, }: ThingCardProps) { const config = useThingConfig(thing.type); const viewConfig = config.ui.views[view]; if (!viewConfig) return null; const fields = viewConfig.fields === "*" ? Object.keys(config.properties) : viewConfig.fields; return ( <Card className={cn("cursor-pointer transition-all hover:shadow-lg", className)} onClick={onClick} > <CardHeader className="space-y-0 pb-3"> {/* Render header fields (typically thumbnail + title) */} {fields.slice(0, 2).map((fieldName) => { const fieldConfig = config.ui.fields[fieldName]; if (fieldConfig?.hidden) return null; return ( <Field key={fieldName} name={fieldName} value={thing.properties[fieldName]} config={fieldConfig} thing={thing} /> ); })} </CardHeader> <CardContent className="space-y-2"> {/* Render body fields */} {fields.slice(2).map((fieldName) => { const fieldConfig = config.ui.fields[fieldName]; if (fieldConfig?.hidden) return null; if (fieldConfig?.showOnlyIf && !thing.properties[fieldName]) return null; return ( <Field key={fieldName} name={fieldName} value={thing.properties[fieldName]} config={fieldConfig} thing={thing} /> ); })} </CardContent> {/* Connection badges */} {showConnections && config.ui.connections && ( <> <Separator /> <CardContent className="pt-3"> <ConnectionBadges thing={thing} config={config.ui.connections} /> </CardContent> </> )} {/* Actions */} {showActions && (config.ui.actions.primary || config.ui.actions.secondary) && ( <> <Separator /> <CardFooter className="pt-3"> <Actions thing={thing} primary={config.ui.actions.primary} secondary={config.ui.actions.secondary} /> </CardFooter> </> )} </Card> ); } ``` --- ## Field Component with shadcn/ui ```tsx // frontend/src/components/generic/Field.tsx import { type FieldUI } from "@/ontology/types"; import { type Thing } from "@one-platform/core"; // shadcn/ui components import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 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 { Skeleton } from "@/components/ui/skeleton"; // Custom field components import { Heading } from "@/components/fields/Heading"; import { Text } from "@/components/fields/Text"; import { Price } from "@/components/fields/Price"; import { Image } from "@/components/fields/Image"; import { TagList } from "@/components/fields/TagList"; import { DateField } from "@/components/fields/DateField"; import { Link } from "@/components/fields/Link"; interface FieldProps { name: string; value: any; config: FieldUI; thing: Thing; } export function Field({ name, value, config, thing }: FieldProps) { if (value === null || value === undefined) return null; const formattedValue = config.format ? config.format(value, thing) : value; switch (config.component) { case "Heading": return <Heading {...config}>{formattedValue}</Heading>; case "Text": return <Text {...config}>{formattedValue}</Text>; case "Price": return <Price {...config} value={formattedValue} />; case "Image": return <Image {...config} src={formattedValue} alt={thing.name} />; case "Badge": const badgeColor = config.colors?.[value] || config.color; const badgeLabel = config.labels?.[value] || config.label || value; return ( <Badge variant={badgeColor as any} className="inline-flex items-center gap-1" > {config.icon && <span className="h-3 w-3">{/* icon */}</span>} {badgeLabel} </Badge> ); case "TagList": return <TagList {...config} tags={formattedValue} />; case "Date": return <DateField {...config} value={formattedValue} />; case "Avatar": return ( <Avatar className={config.size === "sm" ? "h-8 w-8" : "h-10 w-10"}> <AvatarImage src={formattedValue} alt={thing.name} /> <AvatarFallback> {config.fallback === "initials" ? thing.name .split(" ") .map((n) => n[0]) .join("") .toUpperCase() : config.fallback} </AvatarFallback> </Avatar> ); case "Link": return <Link {...config} href={formattedValue} />; case "Checkbox": return ( <div className="flex items-center space-x-2"> <Checkbox checked={formattedValue} onCheckedChange={(checked) => { // Update thing }} /> {config.label && ( <label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"> {config.label} </label> )} </div> ); case "Switch": return ( <div className="flex items-center space-x-2"> <Switch checked={formattedValue} onCheckedChange={(checked) => { // Update thing }} /> {config.label && ( <label className="text-sm font-medium">{config.label}</label> )} </div> ); case "Progress": return ( <div className="space-y-2"> {config.label && ( <div className="flex justify-between text-sm"> <span>{config.label}</span> <span>{formattedValue}%</span> </div> )} <Progress value={formattedValue} /> </div> ); case "Separator": return <Separator />; case "Skeleton": return <Skeleton className={config.className} />; default: return <Text size="sm">{String(formattedValue)}</Text>; } } ``` --- ## Actions Component with shadcn/ui ```tsx // frontend/src/components/generic/Actions.tsx import { type Thing } from "@one-platform/core"; import { type ActionConfig } from "@/ontology/types"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { useAction } from "@/hooks/useAction"; import { MoreVertical } from "lucide-react"; import { useState } from "react"; interface ActionsProps { thing: Thing; primary?: ActionConfig; secondary?: ActionConfig[]; context?: ActionConfig[]; } export function Actions({ thing, primary, secondary, context }: ActionsProps) { const { executeAction, loading } = useAction(); const [confirmAction, setConfirmAction] = useState<ActionConfig | null>(null); const handleAction = async (action: ActionConfig) => { // Show confirmation dialog if required if (action.confirm) { setConfirmAction(action); return; } await executeAction(action.action, thing); }; return ( <> <div className="flex items-center gap-2 w-full"> {/* Primary action */} {primary && ( <Button variant={primary.variant === "danger" ? "destructive" : "default"} className="flex-1" disabled={loading} onClick={() => handleAction(primary)} > {loading ? "Loading..." : primary.label} </Button> )} {/* Secondary actions */} {secondary && secondary.length > 0 && ( <div className="flex gap-2"> {secondary.map((action) => ( <Button key={action.action} variant={action.variant === "ghost" ? "outline" : "default"} size="icon" disabled={loading} onClick={() => handleAction(action)} > {/* Icon from lucide-react */} <span className="h-4 w-4" /> </Button> ))} </div> )} {/* Context menu (more actions) */} {context && context.length > 0 && ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon"> <MoreVertical className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> {context.map((action, index) => ( <div key={action.action}> {index > 0 && action.variant === "danger" && ( <DropdownMenuSeparator /> )} <DropdownMenuItem onClick={() => handleAction(action)} className={ action.variant === "danger" ? "text-destructive" : "" } > {action.label} </DropdownMenuItem> </div> ))} </DropdownMenuContent> </DropdownMenu> )} </div> {/* Confirmation dialog */} {confirmAction && ( <AlertDialog open={!!confirmAction} onOpenChange={() => setConfirmAction(null)} > <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Are you sure?</AlertDialogTitle> <AlertDialogDescription> {confirmAction.confirm} </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction onClick={async () => { await executeAction(confirmAction.action, thing); setConfirmAction(null); }} > Continue </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> )} </> ); } ``` --- ## Connection Badges with shadcn/ui ```tsx // frontend/src/components/generic/ConnectionBadges.tsx import { type Thing } from "@one-platform/core"; import { type ConnectionUI } from "@/ontology/types"; import { useConnections } from "@/hooks/useConnections"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Separator } from "@/components/ui/separator"; interface ConnectionBadgesProps { thing: Thing; config: Record<string, ConnectionUI>; } export function ConnectionBadges({ thing, config }: ConnectionBadgesProps) { const connections = useConnections(thing._id); const badges = Object.entries(config).map(([type, connectionConfig]) => { const items = connections.filter((c) => c.relationshipType === type); if (items.length === 0) return null; const label = connectionConfig.label .replace("{count}", items.length.toString()) .replace("{name}", items[0]?.name || ""); switch (connectionConfig.display) { case "badge": return ( <Badge key={type} variant="secondary" className="text-xs"> {label} </Badge> ); case "avatar": return ( <div key={type} className="flex items-center gap-2"> <Avatar className="h-6 w-6"> <AvatarImage src={items[0]?.properties?.avatar} /> <AvatarFallback> {items[0]?.name?.[0]?.toUpperCase()} </AvatarFallback> </Avatar> <span className="text-sm text-muted-foreground">{label}</span> </div> ); case "inline": return ( <span key={type} className="text-sm text-muted-foreground"> {label} </span> ); case "list": return ( <div key={type} className="flex flex-wrap gap-1"> {items.slice(0, connectionConfig.max || 5).map((item) => ( <Badge key={item._id} variant="outline" className="text-xs"> {item.name} </Badge> ))} </div> ); default: return null; } }); const validBadges = badges.filter(Boolean); if (validBadges.length === 0) return null; return <div className="flex flex-wrap items-center gap-2">{validBadges}</div>; } ``` --- ## Field Components with shadcn/ui ### Price Component ```tsx // frontend/src/components/fields/Price.tsx import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; interface PriceProps { value: number; currency?: string; format?: "full" | "compact"; badge?: boolean; size?: "sm" | "md" | "lg"; strikethrough?: boolean; free?: { label: string; badge: boolean }; className?: string; } export function Price({ value, currency = "USD", format = "compact", badge = false, size = "md", strikethrough = false, free, className, }: PriceProps) { // Show "Free" if value is 0 and free config provided if (value === 0 && free) { return free.badge ? ( <Badge variant="secondary">{free.label}</Badge> ) : ( <span className="text-sm font-medium text-muted-foreground"> {free.label} </span> ); } const formatted = new Intl.NumberFormat("en-US", { style: "currency", currency, minimumFractionDigits: format === "compact" ? 0 : 2, maximumFractionDigits: format === "compact" ? 0 : 2, }).format(value); const sizeClasses = { sm: "text-sm", md: "text-base", lg: "text-lg", }; const content = ( <span className={cn( "font-semibold", sizeClasses[size], strikethrough && "line-through text-muted-foreground", className, )} > {formatted} </span> ); return badge ? ( <Badge variant="default" className="font-semibold"> {formatted} </Badge> ) : ( content ); } ``` ### Image Component ```tsx // frontend/src/components/fields/Image.tsx import { useState } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; interface ImageProps { src: string; alt: string; aspect?: "square" | "video" | "wide" | "portrait"; lazy?: boolean; placeholder?: string; fallback?: string; sizes?: string; className?: string; } export function Image({ src, alt, aspect = "video", lazy = true, placeholder, fallback, sizes, className, }: ImageProps) { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const aspectClasses = { square: "aspect-square", video: "aspect-video", wide: "aspect-[21/9]", portrait: "aspect-[3/4]", }; return ( <div className={cn( "relative overflow-hidden rounded-md", aspectClasses[aspect], )} > {loading && <Skeleton className="absolute inset-0" />} <img src={error && fallback ? fallback : src} alt={alt} loading={lazy ? "lazy" : "eager"} sizes={sizes} className={cn( "h-full w-full object-cover transition-opacity", loading ? "opacity-0" : "opacity-100", className, )} onLoad={() => setLoading(false)} onError={() => { setError(true); setLoading(false); }} /> </div> ); } ``` ### TagList Component ```tsx // frontend/src/components/fields/TagList.tsx import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; interface TagListProps { tags: string[]; max?: number; color?: string; moreLabel?: string; } export function TagList({ tags, max = 3, color = "default", moreLabel = "+{count} more", }: TagListProps) { if (!tags || tags.length === 0) return null; const visibleTags = tags.slice(0, max); const hiddenTags = tags.slice(max); return ( <div className="flex flex-wrap gap-1"> {visibleTags.map((tag) => ( <Badge key={tag} variant="secondary" className="text-xs"> {tag} </Badge> ))} {hiddenTags.length > 0 && ( <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <Badge variant="outline" className="text-xs cursor-help"> {moreLabel.replace("{count}", hiddenTags.length.toString())} </Badge> </TooltipTrigger> <TooltipContent> <div className="flex flex-wrap gap-1 max-w-xs"> {hiddenTags.map((tag) => ( <Badge key={tag} variant="secondary" className="text-xs"> {tag} </Badge> ))} </div> </TooltipContent> </Tooltip> </TooltipProvider> )} </div> ); } ``` --- ## Empty State with shadcn/ui ```tsx // frontend/src/components/generic/EmptyState.tsx import { type EmptyStateConfig } from "@/ontology/types"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; interface EmptyStateProps { config: EmptyStateConfig; onAction?: () => void; } export function EmptyState({ config, onAction }: EmptyStateProps) { return ( <Card className="border-dashed"> <CardContent className="flex flex-col items-center justify-center py-12"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-muted mb-4"> {/* Icon from lucide-react */} <span className="h-10 w-10 text-muted-foreground" /> </div> <h3 className="text-xl font-semibold mb-2">{config.title}</h3> <p className="text-sm text-muted-foreground text-center mb-6 max-w-sm"> {config.description} </p> {config.action && ( <Button onClick={onAction}>{config.action.label}</Button> )} </CardContent> </Card> ); } ``` --- ## ThingTable with shadcn/ui ```tsx // frontend/src/components/generic/ThingTable.tsx import { type Thing } from "@one-platform/core"; import { useThingConfig } from "@/ontology/hooks/useThingConfig"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Field } from "./Field"; import { ArrowUpDown } from "lucide-react"; interface ThingTableProps { things: Thing[]; onRowClick?: (thing: Thing) => void; } export function ThingTable({ things, onRowClick }: ThingTableProps) { if (things.length === 0) return null; const config = useThingConfig(things[0].type); const tableView = config.ui.views.table; if (!tableView) return null; return ( <div className="rounded-md border"> <Table> <TableHeader> <TableRow> {tableView.fields.map((fieldName) => { const fieldConfig = config.ui.fields[fieldName]; return ( <TableHead key={fieldName}> <Button variant="ghost" size="sm" className="-ml-3 h-8" onClick={() => { // Handle sort }} > {fieldConfig?.label || fieldName} <ArrowUpDown className="ml-2 h-4 w-4" /> </Button> </TableHead> ); })} </TableRow> </TableHeader> <TableBody> {things.map((thing) => ( <TableRow key={thing._id} className="cursor-pointer" onClick={() => onRowClick?.(thing)} > {tableView.fields.map((fieldName) => ( <TableCell key={fieldName}> <Field name={fieldName} value={thing.properties[fieldName]} config={config.ui.fields[fieldName]} thing={thing} /> </TableCell> ))} </TableRow> ))} </TableBody> </Table> </div> ); } ``` --- ## Theme Support ### Dark Mode shadcn/ui includes built-in dark mode support: ```tsx // frontend/src/components/ThemeProvider.tsx import { createContext, useContext, useEffect, useState } from "react"; type Theme = "dark" | "light" | "system"; const ThemeProviderContext = createContext<{ theme: Theme; setTheme: (theme: Theme) => void; }>({ theme: "system", setTheme: () => null, }); export function ThemeProvider({ children, defaultTheme = "system", }: { children: React.ReactNode; defaultTheme?: Theme; }) { const [theme, setTheme] = useState<Theme>(defaultTheme); useEffect(() => { const root = window.document.documentElement; root.classList.remove("light", "dark"); if (theme === "system") { const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") .matches ? "dark" : "light"; root.classList.add(systemTheme); return; } root.classList.add(theme); }, [theme]); return ( <ThemeProviderContext.Provider value={{ theme, setTheme }}> {children} </ThemeProviderContext.Provider> ); } export const useTheme = () => useContext(ThemeProviderContext); ``` ### Theme Toggle ```tsx // frontend/src/components/ThemeToggle.tsx import { Moon, Sun } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useTheme } from "./ThemeProvider"; export function ThemeToggle() { const { setTheme } = useTheme(); return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon"> <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">Toggle theme</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={() => setTheme("light")}> Light </DropdownMenuItem> <DropdownMenuItem onClick={() => setTheme("dark")}> Dark </DropdownMenuItem> <DropdownMenuItem onClick={() => setTheme("system")}> System </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); } ``` --- ## Responsive Design shadcn/ui components are responsive by default. Use Tailwind's responsive utilities: ```tsx <Card className="w-full sm:w-1/2 lg:w-1/3"> {/* Responsive card width */} </Card> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {/* Responsive grid */} </div> ``` --- ## Accessibility shadcn/ui components are built on Radix UI, which provides: - ✅ **Keyboard navigation** - Full keyboard support - ✅ **Screen reader support** - ARIA labels and descriptions - ✅ **Focus management** - Proper focus handling - ✅ **Color contrast** - WCAG AA compliant - ✅ **RTL support** - Right-to-left languages ### Example: Accessible Form ```tsx <form className="space-y-4"> <div className="space-y-2"> <Label htmlFor="title">Course Title</Label> <Input id="title" placeholder="Enter title" aria-describedby="title-description" /> <p id="title-description" className="text-sm text-muted-foreground"> This will be the main title displayed to students </p> </div> </form> ``` --- ## Usage Examples ### Course Card ```tsx import { ThingCard } from "@/components/generic/ThingCard"; export function CoursesPage() { const courses = await getCourses(); return ( <div className="container mx-auto py-8"> <h1 className="text-3xl font-bold mb-6">Courses</h1> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {courses.map((course) => ( <ThingCard key={course._id} thing={course} onClick={(thing) => { router.push(`/courses/${thing._id}`); }} /> ))} </div> </div> ); } ``` ### Product Grid with Filters ```tsx import { ThingCard } from "@/components/generic/ThingCard"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; export function ProductsPage() { const [products, setProducts] = useState([]); const [category, setCategory] = useState("all"); return ( <div className="container mx-auto py-8"> <div className="flex items-center gap-4 mb-6"> <Input placeholder="Search products..." className="max-w-sm" /> <Select value={category} onValueChange={setCategory}> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Category" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Categories</SelectItem> <SelectItem value="books">Books</SelectItem> <SelectItem value="courses">Courses</SelectItem> </SelectContent> </Select> </div> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> {products.map((product) => ( <ThingCard key={product._id} thing={product} /> ))} </div> </div> ); } ``` --- ## Customization ### Custom Theme Colors ```css /* frontend/src/styles/globals.css */ @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; /* ... other colors */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --primary: 217.2 91.2% 59.8%; --primary-foreground: 222.2 47.4% 11.2%; /* ... other colors */ } } ``` ### Organization-Specific Overrides ```tsx // Custom badge colors per organization const config = { ...defaultCourseConfig, ui: { ...defaultCourseConfig.ui, fields: { ...defaultCourseConfig.ui.fields, level: { ...defaultCourseConfig.ui.fields.level, colors: { beginner: "blue", // Override to blue intermediate: "purple", advanced: "orange", }, }, }, }, }; ``` --- ## Next Steps ### Week 1: Setup - [ ] Install shadcn/ui - [ ] Add all required components - [ ] Setup theme provider - [ ] Configure Tailwind ### Week 2: Core Components - [ ] Build ThingCard with shadcn/ui - [ ] Build Field component - [ ] Build Actions component - [ ] Test with 3 thing types ### Week 3: Field Components - [ ] Build all field components - [ ] Add Price, Image, TagList - [ ] Add DateField, Avatar, Link - [ ] Test responsive design ### Week 4: Advanced - [ ] Build ThingTable - [ ] Add dark mode support - [ ] Add accessibility features - [ ] Performance optimization --- ## Resources - **shadcn/ui Docs**: https://ui.shadcn.com - **Radix UI Docs**: https://www.radix-ui.com - **Tailwind CSS**: https://tailwindcss.com - **Lucide Icons**: https://lucide.dev --- **With shadcn/ui, every thing type gets beautiful, accessible UI automatically.**