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:
@@ -12,5 +12,16 @@
|
||||
"plugin:import/electron",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true,
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Adiuva</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
800
package-lock.json
generated
800
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"electron": "40.6.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.19",
|
||||
@@ -44,6 +45,7 @@
|
||||
"vite": "^5.4.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/geist": "^5.2.8",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router": "^1.161.1",
|
||||
"@trpc/client": "^11.10.0",
|
||||
@@ -54,6 +56,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-trpc": "^0.7.1",
|
||||
"framer-motion": "^12.34.2",
|
||||
"lucide-react": "^0.575.0",
|
||||
|
||||
@@ -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
18
src/main/store.ts
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user