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

@@ -1,5 +1,6 @@
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
import { getStore } from '../store';
const t = initTRPC.create();
@@ -134,6 +135,16 @@ const notesRouter = router({
.mutation(() => null),
});
const settingsRouter = router({
getSidebarCollapsed: publicProcedure.query(() => getStore().get('sidebarCollapsed')),
setSidebarCollapsed: publicProcedure
.input(z.object({ collapsed: z.boolean() }))
.mutation(({ input }) => {
getStore().set('sidebarCollapsed', input.collapsed);
return null;
}),
});
const aiRouter = router({
chat: publicProcedure
.input(z.object({
@@ -152,6 +163,7 @@ const aiRouter = router({
export const appRouter = router({
health: healthRouter,
settings: settingsRouter,
clients: clientsRouter,
projects: projectsRouter,
tasks: tasksRouter,

18
src/main/store.ts Normal file
View File

@@ -0,0 +1,18 @@
import Store from 'electron-store';
interface AppSettings {
sidebarCollapsed: boolean;
}
let _store: Store<AppSettings> | null = null;
export function getStore(): Store<AppSettings> {
if (!_store) {
_store = new Store<AppSettings>({
defaults: {
sidebarCollapsed: false,
},
});
}
return _store;
}

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 */