UI Components
Complete reference for ProductReady's UI component library (KUI)
UI Components
ProductReady uses KUI - a comprehensive UI component library built on shadcn/ui with additional enhancements. All components are:
- ✅ Type-safe - Full TypeScript support
- ✅ Accessible - WCAG 2.1 AA compliant
- ✅ Themeable - CSS variables for customization
- ✅ Dark mode - Built-in dark mode support
- ✅ Tree-shakeable - Import only what you need
Location: All components live in packages/kui/src/components/ui/
Quick Import
import { Button } from 'kui/button';
import { Card } from 'kui/card';
import { Input } from 'kui/input';
export function MyComponent() {
return (
<Card>
<Input placeholder="Enter text" />
<Button>Submit</Button>
</Card>
);
}Component Categories
Form Components
import { Input } from 'kui/input';
// Basic input
<Input placeholder="Email" type="email" />
// With label
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
</div>
// Disabled
<Input disabled placeholder="Disabled" />import { Textarea } from 'kui/textarea';
<Textarea placeholder="Your message..." rows={4} />import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'kui/select';
<Select>
<SelectTrigger>
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
</SelectContent>
</Select>import { Checkbox } from 'kui/checkbox';
import { Label } from 'kui/label';
<div className="flex items-center gap-2">
<Checkbox id="terms" />
<Label htmlFor="terms">Accept terms</Label>
</div>Available Form Components:
Input- Text input with variantsTextarea- Multi-line text inputSelect- Dropdown selectCheckbox- Checkbox with labelRadio Group- Radio button groupSwitch- Toggle switchSlider- Range sliderCalendar- Date pickerInput OTP- One-time password inputForm- Form wrapper with validation
Layout Components
Card
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'kui/card';
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>Separator
import { Separator } from 'kui/separator';
<div>
<p>Content above</p>
<Separator className="my-4" />
<p>Content below</p>
</div>
// Vertical separator
<Separator orientation="vertical" className="h-4" />Tabs
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'kui/tabs';
<Tabs defaultValue="tab1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content 1</TabsContent>
<TabsContent value="tab2">Content 2</TabsContent>
</Tabs>Accordion
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'kui/accordion';
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
</Accordion>Buttons & Actions
Button
import { Button } from 'kui/button';
// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// Sizes
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon /></Button>
// States
<Button disabled>Disabled</Button>
<Button loading>Loading...</Button>Button Group
import { ButtonGroup } from 'kui/button-group';
<ButtonGroup>
<Button>Left</Button>
<Button>Center</Button>
<Button>Right</Button>
</ButtonGroup>Toggle
import { Toggle } from 'kui/toggle';
<Toggle aria-label="Toggle italic">
<Italic className="h-4 w-4" />
</Toggle>Overlay Components
Dialog
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from 'kui/dialog';
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog description</DialogDescription>
</DialogHeader>
<p>Dialog content goes here</p>
</DialogContent>
</Dialog>Drawer
import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from 'kui/drawer';
<Drawer>
<DrawerTrigger>Open Drawer</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer Title</DrawerTitle>
<DrawerDescription>Drawer description</DrawerDescription>
</DrawerHeader>
</DrawerContent>
</Drawer>Sheet
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from 'kui/sheet';
<Sheet>
<SheetTrigger>Open Sheet</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>Sheet description</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>Popover
import { Popover, PopoverContent, PopoverTrigger } from 'kui/popover';
<Popover>
<PopoverTrigger>Open Popover</PopoverTrigger>
<PopoverContent>
<p>Popover content</p>
</PopoverContent>
</Popover>Tooltip
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'kui/tooltip';
<TooltipProvider>
<Tooltip>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>
<p>Tooltip text</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>Feedback Components
Alert
import { Alert, AlertDescription, AlertTitle } from 'kui/alert';
import { AlertCircle } from 'lucide-react';
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
This is an alert message.
</AlertDescription>
</Alert>
// Variants
<Alert variant="default">Default Alert</Alert>
<Alert variant="destructive">Error Alert</Alert>Toast (Sonner)
import { toast } from 'sonner';
// Success
toast.success('Task created successfully!');
// Error
toast.error('Something went wrong');
// Info
toast.info('New update available');
// Custom
toast('Custom message', {
description: 'With description',
action: {
label: 'Undo',
onClick: () => console.log('Undo'),
},
});Progress
import { Progress } from 'kui/progress';
<Progress value={60} /> // 60%Spinner
import { Spinner } from 'kui/spinner';
<Spinner size="sm" />
<Spinner size="default" />
<Spinner size="lg" />Skeleton
import { Skeleton } from 'kui/skeleton';
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>Data Display
Table
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'kui/table';
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>John Doe</TableCell>
<TableCell>john@example.com</TableCell>
</TableRow>
</TableBody>
</Table>Badge
import { Badge } from 'kui/badge';
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>Avatar
import { Avatar, AvatarFallback, AvatarImage } from 'kui/avatar';
<Avatar>
<AvatarImage src="/avatar.jpg" alt="User" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>Stats Card
import { StatsCard } from 'kui/stats-card';
<StatsCard
title="Total Users"
value="1,234"
change="+12%"
trend="up"
/>Chart
import { Chart } from 'kui/chart';
<Chart
data={chartData}
type="line"
xKey="date"
yKey="value"
/>Navigation Components
Breadcrumb
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from 'kui/breadcrumb';
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Current Page</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>Sidebar
import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from 'kui/sidebar';
<Sidebar>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="/dashboard">Dashboard</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>Menubar
import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarTrigger } from 'kui/menubar';
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>New File</MenubarItem>
<MenubarItem>Save</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>Pagination
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from 'kui/pagination';
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>Special Components
Command Palette
import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from 'kui/command';
<Command>
<CommandInput placeholder="Type a command..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Dashboard</CommandItem>
<CommandItem>Settings</CommandItem>
</CommandGroup>
</CommandList>
</Command>Carousel
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from 'kui/carousel';
<Carousel>
<CarouselContent>
<CarouselItem>Slide 1</CarouselItem>
<CarouselItem>Slide 2</CarouselItem>
<CarouselItem>Slide 3</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Context Menu
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from 'kui/context-menu';
<ContextMenu>
<ContextMenuTrigger>Right click me</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Edit</ContextMenuItem>
<ContextMenuItem>Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Complete Component List
| Component | Description | Category |
|---|---|---|
accordion | Collapsible content sections | Layout |
alert-dialog | Modal confirmation dialog | Overlay |
alert | Feedback messages | Feedback |
aspect-ratio | Maintain aspect ratio | Layout |
avatar | User profile image | Data Display |
badge | Status indicators | Data Display |
breadcrumb | Navigation breadcrumbs | Navigation |
button-group | Grouped buttons | Actions |
button | Clickable button | Actions |
calendar | Date picker | Form |
card | Content container | Layout |
carousel | Image/content slider | Special |
chart | Data visualization | Data Display |
checkbox | Checkbox input | Form |
collapsible | Expandable content | Layout |
command | Command palette | Special |
context-menu | Right-click menu | Navigation |
dialog | Modal dialog | Overlay |
drawer | Side panel | Overlay |
dropdown-menu | Dropdown menu | Navigation |
empty | Empty state | Feedback |
field | Form field wrapper | Form |
form | Form container | Form |
hover-card | Hover preview card | Overlay |
input-group | Grouped inputs | Form |
input-otp | OTP input | Form |
input | Text input | Form |
item | List item | Data Display |
kbd | Keyboard shortcut | Data Display |
label | Input label | Form |
menubar | Menu bar | Navigation |
navigation-menu | Navigation menu | Navigation |
pagination | Page navigation | Navigation |
popover | Floating content | Overlay |
progress | Progress bar | Feedback |
radio-group | Radio buttons | Form |
resizable | Resizable panels | Layout |
scroll-area | Scrollable area | Layout |
select | Dropdown select | Form |
separator | Divider line | Layout |
sheet | Side sheet | Overlay |
sidebar | App sidebar | Navigation |
skeleton | Loading placeholder | Feedback |
slider | Range slider | Form |
sonner | Toast notifications | Feedback |
spinner | Loading spinner | Feedback |
stats-card | Metrics card | Data Display |
switch | Toggle switch | Form |
table-list-view | Table/list view | Data Display |
table | Data table | Data Display |
tabs | Tabbed content | Layout |
textarea | Multi-line input | Form |
toggle-group | Toggle button group | Actions |
toggle | Toggle button | Actions |
tooltip | Hover tooltip | Overlay |
Customization
Theme Colors
Edit apps/productready/src/app/globals.css:
@layer base {
:root {
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* ... more variables */
}
.dark {
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... more variables */
}
}Component Variants
Extend component variants:
// Create custom button variant
<Button className="bg-purple-600 hover:bg-purple-700">
Custom Color
</Button>Best Practices
✅ Do
- Use semantic HTML - Components render proper HTML
- Import only needed - Tree-shaking optimizes bundle
- Follow accessibility - Use labels, ARIA attributes
- Compose components - Build complex UIs from simple parts
- Test dark mode - All components support dark mode
❌ Don't
- Don't modify KUI directly - Create wrapper components instead
- Don't skip labels - Always label form inputs
- Don't inline styles - Use className with Tailwind
- Don't forget loading states - Show spinners for async actions
Common Patterns & Real-World Examples
User Profile Card
Combine multiple components for a complete user profile card:
import { Card, CardContent, CardFooter, CardHeader } from 'kui/card';
import { Avatar, AvatarFallback, AvatarImage } from 'kui/avatar';
import { Button } from 'kui/button';
import { Badge } from 'kui/badge';
export function UserProfileCard({ user }: { user: User }) {
return (
<Card className="w-[350px]">
<CardHeader className="flex flex-row items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<h3 className="font-semibold">{user.name}</h3>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</CardHeader>
<CardContent>
<div className="flex gap-2 flex-wrap">
<Badge variant="secondary">Pro Member</Badge>
<Badge variant="outline">Active</Badge>
</div>
<p className="mt-4 text-sm">{user.bio}</p>
</CardContent>
<CardFooter className="flex gap-2">
<Button variant="default" className="flex-1">Message</Button>
<Button variant="outline" className="flex-1">Follow</Button>
</CardFooter>
</Card>
);
}Settings Form with Validation
Complete form example with multiple input types:
'use client';
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'kui/card';
import { Input } from 'kui/input';
import { Label } from 'kui/label';
import { Switch } from 'kui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'kui/select';
import { Button } from 'kui/button';
import { Separator } from 'kui/separator';
import { toast } from 'sonner';
export function SettingsForm() {
const [settings, setSettings] = useState({
name: '',
email: '',
theme: 'light',
notifications: true,
});
const handleSave = () => {
// Save logic here
toast.success('Settings saved successfully!');
};
return (
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Account Settings</CardTitle>
<CardDescription>Manage your account preferences</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name">Display Name</Label>
<Input
id="name"
placeholder="Enter your name"
value={settings.name}
onChange={(e) => setSettings({ ...settings, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
placeholder="your@email.com"
value={settings.email}
onChange={(e) => setSettings({ ...settings, email: e.target.value })}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="theme">Theme</Label>
<Select
value={settings.theme}
onValueChange={(value) => setSettings({ ...settings, theme: value })}
>
<SelectTrigger id="theme">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>Email Notifications</Label>
<p className="text-sm text-muted-foreground">
Receive emails about account activity
</p>
</div>
<Switch
checked={settings.notifications}
onCheckedChange={(checked) => setSettings({ ...settings, notifications: checked })}
/>
</div>
<div className="flex gap-2 justify-end">
<Button variant="outline">Cancel</Button>
<Button onClick={handleSave}>Save Changes</Button>
</div>
</CardContent>
</Card>
);
}Data Table with Actions
Interactive table with row actions:
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'kui/table';
import { Badge } from 'kui/badge';
import { Button } from 'kui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'kui/dropdown-menu';
import { MoreHorizontal, Pencil, Trash } from 'lucide-react';
export function UsersTable({ users }: { users: User[] }) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead>Role</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge variant={user.active ? 'default' : 'secondary'}>
{user.active ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}Confirmation Dialog
Delete confirmation with proper UX:
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from 'kui/alert-dialog';
import { Button } from 'kui/button';
import { Trash } from 'lucide-react';
export function DeleteButton({ itemName, onDelete }: { itemName: string; onDelete: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm">
<Trash className="mr-2 h-4 w-4" />
Delete
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete "{itemName}"
and remove the data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onDelete} className="bg-destructive text-destructive-foreground">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}Dashboard Stats Cards
Display metrics with stats cards:
import { Card, CardContent, CardHeader, CardTitle } from 'kui/card';
import { ArrowUp, ArrowDown, Users, DollarSign, Activity, TrendingUp } from 'lucide-react';
export function DashboardStats() {
const stats = [
{ title: 'Total Revenue', value: '$45,231', change: '+20.1%', icon: DollarSign, positive: true },
{ title: 'Active Users', value: '2,345', change: '+12%', icon: Users, positive: true },
{ title: 'Conversion Rate', value: '3.24%', change: '-2.4%', icon: TrendingUp, positive: false },
{ title: 'API Calls', value: '1.2M', change: '+45%', icon: Activity, positive: true },
];
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{stats.map((stat) => (
<Card key={stat.title}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{stat.title}
</CardTitle>
<stat.icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stat.value}</div>
<p className={`text-xs flex items-center gap-1 ${stat.positive ? 'text-green-600' : 'text-red-600'}`}>
{stat.positive ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />}
{stat.change} from last month
</p>
</CardContent>
</Card>
))}
</div>
);
}Loading States & Skeletons
Show loading placeholders:
import { Card, CardContent, CardHeader } from 'kui/card';
import { Skeleton } from 'kui/skeleton';
export function UserCardSkeleton() {
return (
<Card>
<CardHeader className="flex flex-row items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-3 w-[150px]" />
</div>
</CardHeader>
<CardContent>
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
);
}
// Usage in component
export function UsersList() {
const { data: users, isLoading } = trpc.users.list.useQuery();
if (isLoading) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<UserCardSkeleton key={i} />
))}
</div>
);
}
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}Command Palette (Search)
Quick navigation with command palette:
'use client';
import { useState, useEffect } from 'react';
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from 'kui/command';
import { Search, FileText, Settings, Users, DollarSign } from 'lucide-react';
export function CommandPalette() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem onSelect={() => console.log('Navigate to Dashboard')}>
<FileText className="mr-2 h-4 w-4" />
<span>Dashboard</span>
</CommandItem>
<CommandItem onSelect={() => console.log('Navigate to Users')}>
<Users className="mr-2 h-4 w-4" />
<span>Users</span>
</CommandItem>
<CommandItem onSelect={() => console.log('Navigate to Billing')}>
<DollarSign className="mr-2 h-4 w-4" />
<span>Billing</span>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Settings">
<CommandItem onSelect={() => console.log('Navigate to Settings')}>
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}Component Composition Tips
Building Complex Forms
Combine form components effectively:
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from 'kui/form';
import { Input } from 'kui/input';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const formSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
});
export function SignupForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Create Account</Button>
</form>
</Form>
);
}Responsive Layouts
Use cards and grids for responsive design:
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{items.map((item) => (
<Card key={item.id} className="hover:shadow-lg transition-shadow">
<CardHeader>
<CardTitle>{item.title}</CardTitle>
</CardHeader>
<CardContent>{item.content}</CardContent>
</Card>
))}
</div>Accessibility Tips
Keyboard Navigation
All KUI components support keyboard navigation:
- Tab - Move between focusable elements
- Enter/Space - Activate buttons and toggles
- Arrow keys - Navigate menus, selects, radio groups
- Escape - Close dialogs, popovers, sheets
- Home/End - Navigate to first/last item in lists
Screen Reader Support
Components include ARIA attributes:
// Button with aria-label
<Button aria-label="Close dialog">
<X className="h-4 w-4" />
</Button>
// Input with proper labeling
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" aria-describedby="email-description" />
<p id="email-description" className="text-sm text-muted-foreground">
We'll never share your email.
</p>Performance Optimization
Lazy Loading Components
For large applications, lazy load heavy components:
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('~/components/heavy-chart'), {
loading: () => <Skeleton className="h-[400px] w-full" />,
ssr: false,
});Memoization
Prevent unnecessary re-renders:
import { memo } from 'react';
export const UserCard = memo(function UserCard({ user }: { user: User }) {
return (
<Card>
{/* Component content */}
</Card>
);
});Next Steps
- Design System - Customize theme and colors
- Tailwind CSS - Utility classes reference
- Lucide Icons - Icon library used in ProductReady
- Radix UI - Unstyled component primitives
All components are production-ready and fully accessible! 🎨
For live component demonstrations and interactive examples, explore the ProductReady dashboard in development mode or check out the KUI Storybook (if available).