* feat(docs): add interactive examples and API references for Button, Card, and LoadingFallback components - Updated dependencies - Set `retries` to a fixed value of 3 for consistent test retries across environments. - Increased `timeout` from 60 seconds to 120 seconds to allow more time for tests to complete. - Reduced `expect` timeout from 10 seconds to 5 seconds for quicker feedback on assertions.
272 lines
8.2 KiB
TypeScript
272 lines
8.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
import { Code2, FileText, Search } from 'lucide-react';
|
|
|
|
import { Badge } from '@kit/ui/badge';
|
|
import { Input } from '@kit/ui/input';
|
|
import { ScrollArea } from '@kit/ui/scroll-area';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@kit/ui/select';
|
|
import { cn } from '@kit/ui/utils';
|
|
|
|
import type { ComponentInfo } from '../lib/components-data';
|
|
import {
|
|
COMPONENTS_REGISTRY,
|
|
categories,
|
|
categoryInfo,
|
|
} from '../lib/components-data';
|
|
|
|
interface DocsSidebarProps {
|
|
selectedComponent: string;
|
|
selectedCategory: string | null;
|
|
}
|
|
|
|
export function DocsSidebar({
|
|
selectedComponent,
|
|
selectedCategory,
|
|
}: DocsSidebarProps) {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const router = useRouter();
|
|
|
|
const filteredComponents = COMPONENTS_REGISTRY.filter((c) =>
|
|
selectedCategory ? c.category === selectedCategory : true,
|
|
)
|
|
.filter(
|
|
(c) =>
|
|
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
c.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
c.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
c.subcategory.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
)
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
const onCategorySelect = (category: string | null) => {
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
searchParams.set('category', category || '');
|
|
router.push(`/components?${searchParams.toString()}`);
|
|
};
|
|
|
|
const onComponentSelect = (component: ComponentInfo) => {
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
searchParams.set('component', component.name);
|
|
router.push(`/components?${searchParams.toString()}`);
|
|
};
|
|
|
|
return (
|
|
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
|
|
{/* Header */}
|
|
<div className="flex-shrink-0 border-b p-4">
|
|
<div className="mb-2 flex items-center gap-2">
|
|
<Code2 className="text-primary h-6 w-6" />
|
|
|
|
<h1 className="text-xl font-bold">Components</h1>
|
|
</div>
|
|
|
|
<p className="text-muted-foreground text-sm">
|
|
This is the documentation for the components of the UI Kit.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<div className="flex-shrink-0 space-y-2 border-b p-4">
|
|
{/* Category Select */}
|
|
<div className="space-y-2">
|
|
<Select
|
|
value={selectedCategory || 'all'}
|
|
onValueChange={(value) => {
|
|
const category = value === 'all' ? null : value;
|
|
onCategorySelect(category);
|
|
|
|
// Select first component in the filtered results
|
|
const firstComponent = category
|
|
? COMPONENTS_REGISTRY.find((c) => c.category === category)
|
|
: COMPONENTS_REGISTRY[0];
|
|
|
|
if (firstComponent) {
|
|
onComponentSelect(firstComponent);
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={'Select a category'} />
|
|
</SelectTrigger>
|
|
|
|
<SelectContent>
|
|
<SelectItem value="all">
|
|
<div className="flex items-center gap-2">
|
|
<span>All Components</span>
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
{COMPONENTS_REGISTRY.length}
|
|
</Badge>
|
|
</div>
|
|
</SelectItem>
|
|
|
|
{categories.map((category) => {
|
|
const categoryData =
|
|
categoryInfo[category as keyof typeof categoryInfo];
|
|
|
|
const categoryComponents = COMPONENTS_REGISTRY.filter(
|
|
(c) => c.category === category,
|
|
);
|
|
|
|
return (
|
|
<SelectItem key={category} value={category}>
|
|
<div className="flex items-center gap-2">
|
|
{categoryData && (
|
|
<categoryData.icon className="h-4 w-4" />
|
|
)}
|
|
<span>{category}</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{categoryComponents.length}
|
|
</Badge>
|
|
</div>
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Search Input */}
|
|
<div className="space-y-2">
|
|
<div className="relative">
|
|
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
|
|
|
<Input
|
|
placeholder={'Search for a component'}
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Components List - Scrollable */}
|
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
|
<div className="flex-shrink-0 p-4 pb-2">
|
|
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
|
<FileText className="h-4 w-4" />
|
|
Components
|
|
<Badge variant="outline" className="text-xs">
|
|
{filteredComponents.length}
|
|
</Badge>
|
|
{selectedCategory && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
{selectedCategory}
|
|
</Badge>
|
|
)}
|
|
</h3>
|
|
</div>
|
|
|
|
<ScrollArea className="flex-1">
|
|
<div className="px-4 pb-4">
|
|
<div className="space-y-1">
|
|
{filteredComponents.length === 0 ? (
|
|
<div className="text-muted-foreground py-8 text-center">
|
|
<p className="text-sm">No components found</p>
|
|
<p className="mt-1 text-xs">
|
|
Try adjusting your search or category filter
|
|
</p>
|
|
</div>
|
|
) : (
|
|
filteredComponents.map((item) => {
|
|
const IconComponent = item.icon;
|
|
|
|
return (
|
|
<ComponentItem
|
|
key={item.id}
|
|
item={item}
|
|
isSelected={item.name === selectedComponent}
|
|
onComponentSelect={onComponentSelect}
|
|
IconComponent={IconComponent}
|
|
/>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ComponentItem({
|
|
item,
|
|
isSelected,
|
|
onComponentSelect,
|
|
IconComponent,
|
|
}: {
|
|
item: ComponentInfo;
|
|
IconComponent: React.ComponentType<{ className?: string }>;
|
|
isSelected: boolean;
|
|
onComponentSelect: (item: ComponentInfo) => void;
|
|
}) {
|
|
const ref = useRef<HTMLButtonElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (isSelected) {
|
|
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
}, [isSelected]);
|
|
|
|
return (
|
|
<button
|
|
ref={ref}
|
|
key={item.id}
|
|
onClick={() => onComponentSelect(item)}
|
|
className={cn(
|
|
'w-full rounded-lg px-3 py-2.5 text-left text-sm transition-colors',
|
|
isSelected
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'hover:bg-accent hover:text-accent-foreground',
|
|
)}
|
|
>
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center gap-2">
|
|
<IconComponent className="h-4 w-4 flex-shrink-0" />
|
|
|
|
<span className="flex-1 truncate font-medium">{item.name}</span>
|
|
</div>
|
|
<p
|
|
className={cn(
|
|
'ml-6 line-clamp-1 text-xs',
|
|
isSelected ? 'opacity-90' : 'text-muted-foreground',
|
|
)}
|
|
>
|
|
{item.description}
|
|
</p>
|
|
|
|
<div className="ml-6 flex items-center gap-1.5 text-xs">
|
|
<span
|
|
className={cn(
|
|
'font-medium',
|
|
isSelected ? 'opacity-80' : 'text-muted-foreground',
|
|
)}
|
|
>
|
|
{item.subcategory}
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
isSelected ? 'opacity-60' : 'text-muted-foreground/60',
|
|
)}
|
|
>
|
|
• {item.props.length} props
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
}
|