feat: add Input, Separator, Sheet, and Sidebar components
- Implemented Input component for user input fields. - Created Separator component for visual separation in UI. - Added Sheet component for modal-like overlays with customizable content. - Developed Sidebar component with collapsible functionality and mobile responsiveness. - Introduced Skeleton component for loading placeholders. - Implemented Tooltip component for contextual hints. - Updated global CSS variables for sidebar theming. - Added useIsMobile hook for responsive design handling. - Modified projects route to include ProjectSidebar. - Enhanced Tailwind CSS configuration for improved styling. - Updated Vite preload configuration for custom entry file naming.
This commit is contained in:
@@ -8,8 +8,21 @@ import {
|
||||
PanelLeft,
|
||||
ChevronDown,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ to: '/', icon: House, label: 'Home' },
|
||||
@@ -28,101 +41,23 @@ export function AppShell({ children }: AppShellProps) {
|
||||
});
|
||||
const setSidebarCollapsedMutation = trpc.settings.setSidebarCollapsed.useMutation();
|
||||
|
||||
// localCollapsed tracks user toggles after load; null means "use server value"
|
||||
const [localCollapsed, setLocalCollapsed] = useState<boolean | null>(null);
|
||||
|
||||
const routerState = useRouterState();
|
||||
const currentPath = routerState.location.pathname;
|
||||
|
||||
const collapsed = localCollapsed !== null ? localCollapsed : (collapsedQuery.data ?? false);
|
||||
// Controlled open state (spec: "Controlled Sidebar" pattern)
|
||||
const [open, setOpen] = useState(() =>
|
||||
collapsedQuery.data === undefined ? true : !collapsedQuery.data
|
||||
);
|
||||
|
||||
const handleToggle = () => {
|
||||
const next = !collapsed;
|
||||
setLocalCollapsed(next);
|
||||
setSidebarCollapsedMutation.mutate({ collapsed: next });
|
||||
const handleOpenChange = (value: boolean) => {
|
||||
setOpen(value);
|
||||
setSidebarCollapsedMutation.mutate({ collapsed: !value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen overflow-hidden bg-background">
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
'flex flex-col h-full bg-sidebar border-r border-sidebar-border transition-all duration-200 overflow-hidden shrink-0',
|
||||
collapsed ? 'w-16' : 'w-60',
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-3 shrink-0',
|
||||
collapsed && 'justify-center',
|
||||
)}
|
||||
>
|
||||
<div className="size-7 rounded-lg bg-primary flex items-center justify-center shrink-0">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="text-primary-foreground"
|
||||
>
|
||||
<path
|
||||
d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5L12 2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className="font-semibold text-sm text-foreground">
|
||||
Adiuva
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex flex-col gap-0.5 px-2 flex-1 mt-2">
|
||||
{NAV_ITEMS.map(({ to, icon: Icon, label }) => {
|
||||
const isActive =
|
||||
to === '/'
|
||||
? currentPath === '/'
|
||||
: currentPath.startsWith(to);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={to}
|
||||
to={to}
|
||||
className={cn(
|
||||
'flex items-center gap-2 h-8 px-3 rounded-md text-sm text-sidebar-foreground transition-colors',
|
||||
'hover:bg-sidebar-accent',
|
||||
isActive && 'bg-sidebar-accent font-medium',
|
||||
)}
|
||||
>
|
||||
<Icon size={16} className="shrink-0" />
|
||||
{!collapsed && <span className="truncate">{label}</span>}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Collapse toggle */}
|
||||
<div className="px-2 pb-3 shrink-0">
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className={cn(
|
||||
'flex items-center gap-2 h-8 px-3 rounded-md text-sm text-sidebar-foreground w-full',
|
||||
'hover:bg-sidebar-accent transition-colors',
|
||||
collapsed && 'justify-center',
|
||||
)}
|
||||
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
<PanelLeft size={16} className="shrink-0" />
|
||||
{!collapsed && <span>Collapse</span>}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 min-w-0 overflow-hidden relative">
|
||||
<SidebarProvider open={open} onOpenChange={handleOpenChange}>
|
||||
<AppSidebar currentPath={currentPath} />
|
||||
<SidebarInset className="overflow-hidden">
|
||||
{children}
|
||||
|
||||
{/* Right-edge vertical 'keep scrolling for AI' affordance (non-interactive) */}
|
||||
@@ -137,7 +72,87 @@ export function AppShell({ children }: AppShellProps) {
|
||||
<ChevronDown size={10} className="text-muted-foreground/30" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppSidebar({ currentPath }: { currentPath: string }) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
{/* Logo */}
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<div className="cursor-default">
|
||||
<div className="size-7 rounded-lg bg-primary flex items-center justify-center shrink-0">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="text-primary-foreground"
|
||||
>
|
||||
<path
|
||||
d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5L12 2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="font-semibold text-sm text-foreground">
|
||||
Adiuva
|
||||
</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
|
||||
{/* Nav */}
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{NAV_ITEMS.map(({ to, icon: Icon, label }) => {
|
||||
const isActive =
|
||||
to === '/'
|
||||
? currentPath === '/'
|
||||
: currentPath.startsWith(to);
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={to}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isActive}
|
||||
tooltip={label}
|
||||
>
|
||||
<Link to={to}>
|
||||
<Icon />
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
{/* Collapse toggle — spec: useSidebar() + custom trigger */}
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton onClick={toggleSidebar} tooltip="Toggle Sidebar">
|
||||
<PanelLeft />
|
||||
<span>Collapse</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user