feat: US-004 — App shell layout and sidebar navigation

- Add electron-store@8 for sidebar collapse state persistence via settings tRPC router
- Add @fontsource/geist for self-hosted Geist font (remove Google Fonts CDN)
- Add right-edge vertical 'keep scrolling for AI' label with chevron-down in all views
- Wire AppShell collapse toggle to settings.setSidebarCollapsed tRPC mutation
- Fix ESLint config with eslint-import-resolver-typescript to resolve @/* path aliases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Roberto Musso
2026-02-19 16:50:38 +01:00
parent e7f64b385a
commit c99799bb05
8 changed files with 863 additions and 26 deletions

View File

@@ -6,8 +6,10 @@ import {
ClipboardCheck,
FolderKanban,
PanelLeft,
ChevronDown,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { trpc } from '@/lib/trpc';
const NAV_ITEMS = [
{ to: '/', icon: House, label: 'Home' },
@@ -21,10 +23,25 @@ interface AppShellProps {
}
export function AppShell({ children }: AppShellProps) {
const [collapsed, setCollapsed] = useState(false);
const collapsedQuery = trpc.settings.getSidebarCollapsed.useQuery(undefined, {
staleTime: Infinity,
});
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);
const handleToggle = () => {
const next = !collapsed;
setLocalCollapsed(next);
setSidebarCollapsedMutation.mutate({ collapsed: next });
};
return (
<div className="flex h-screen w-screen overflow-hidden bg-background">
{/* Sidebar */}
@@ -90,7 +107,7 @@ export function AppShell({ children }: AppShellProps) {
{/* Collapse toggle */}
<div className="px-2 pb-3 shrink-0">
<button
onClick={() => setCollapsed((c) => !c)}
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',
@@ -107,6 +124,19 @@ export function AppShell({ children }: AppShellProps) {
{/* Main content */}
<main className="flex-1 min-w-0 overflow-hidden relative">
{children}
{/* Right-edge vertical 'keep scrolling for AI' affordance (non-interactive) */}
<div className="absolute right-0 top-0 bottom-0 flex items-end justify-center pb-8 pointer-events-none select-none">
<div className="flex flex-col items-center gap-1.5 pr-2">
<span
className="text-[9px] text-muted-foreground/30 tracking-widest uppercase font-medium"
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
>
keep scrolling for AI
</span>
<ChevronDown size={10} className="text-muted-foreground/30" />
</div>
</div>
</main>
</div>
);

View File

@@ -1,3 +1,7 @@
@import '@fontsource/geist/400.css';
@import '@fontsource/geist/500.css';
@import '@fontsource/geist/600.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -44,5 +48,3 @@
overflow: hidden;
}
}
/* Geist font — loaded via CDN in index.html */