Files
adiuva/src/renderer/components/layout/AppShell.tsx
Roberto Musso 8c1fb54afd feat: add context menu component and update dropdown, select styles
- Implemented a new ContextMenu component with various subcomponents for better UI interaction.
- Updated DropdownMenu and Select components to include hover effects for improved user experience.
- Enhanced global styles with new CSS variables for better theming and consistency across components.
2026-02-20 12:17:50 +01:00

159 lines
4.8 KiB
TypeScript

import { useState } from 'react';
import { Link, useRouterState } from '@tanstack/react-router';
import {
House,
ChartGantt,
ClipboardCheck,
FolderKanban,
PanelLeft,
ChevronUp,
} from 'lucide-react';
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' },
{ to: '/timeline', icon: ChartGantt, label: 'Timeline' },
{ to: '/tasks', icon: ClipboardCheck, label: 'Tasks' },
{ to: '/projects', icon: FolderKanban, label: 'Projects' },
] as const;
interface AppShellProps {
children: React.ReactNode;
}
export function AppShell({ children }: AppShellProps) {
const collapsedQuery = trpc.settings.getSidebarCollapsed.useQuery(undefined, {
staleTime: Infinity,
});
const setSidebarCollapsedMutation = trpc.settings.setSidebarCollapsed.useMutation();
const routerState = useRouterState();
const currentPath = routerState.location.pathname;
// Controlled open state (spec: "Controlled Sidebar" pattern)
const [open, setOpen] = useState(() =>
collapsedQuery.data === undefined ? true : !collapsedQuery.data
);
const handleOpenChange = (value: boolean) => {
setOpen(value);
setSidebarCollapsedMutation.mutate({ collapsed: !value });
};
return (
<SidebarProvider open={open} onOpenChange={handleOpenChange}>
<AppSidebar currentPath={currentPath} />
<SidebarInset>
{children}
{/* Right-edge vertical 'keep scrolling for AI' affordance (non-interactive) */}
<div className="absolute right-0 top-0 flex items-end justify-center pt-8 pointer-events-none select-none">
<div className="flex flex-col items-center gap-1.5 pr-2">
<ChevronUp size={10} className="text-muted-foreground/30" />
<span
className="text-[9px] text-muted-foreground/30 tracking-widest uppercase font-medium"
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
>
keep scrolling up for AI
</span>
</div>
</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>
);
}