Files
myeasycms-v2/apps/dev-tool/app/components/components/docs-sidebar.tsx
Giancarlo Buomprisco 7ebff31475 Next.js Supabase V3 (#463)
Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
2026-03-24 13:40:38 +08:00

278 lines
8.3 KiB
TypeScript

'use client';
import { useEffect, useRef, useState } from 'react';
import { useRouter, useSearchParams } 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 searchParams = useSearchParams();
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 sp = new URLSearchParams(searchParams);
sp.set('category', category || '');
router.push(`/components?${sp.toString()}`);
};
const onComponentSelect = (component: ComponentInfo) => {
const sp = new URLSearchParams(searchParams);
sp.set('component', component.name);
router.push(`/components?${sp.toString()}`);
};
return (
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
{/* Header */}
<div className="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="shrink-0 space-y-2 border-b p-4">
{/* Category Select */}
<div className="space-y-2">
<Select
defaultValue={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 className="w-full">
<SelectValue>
{(category) => {
return category === 'all' ? 'All Categories' : category;
}}
</SelectValue>
</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="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>
);
}