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/electron",
|
||||||
"plugin:import/typescript"
|
"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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Adiuva</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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",
|
"drizzle-kit": "^0.31.9",
|
||||||
"electron": "40.6.0",
|
"electron": "40.6.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.19",
|
"tailwindcss": "^3.4.19",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"vite": "^5.4.21"
|
"vite": "^5.4.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/geist": "^5.2.8",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-router": "^1.161.1",
|
"@tanstack/react-router": "^1.161.1",
|
||||||
"@trpc/client": "^11.10.0",
|
"@trpc/client": "^11.10.0",
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"electron-store": "^8.2.0",
|
||||||
"electron-trpc": "^0.7.1",
|
"electron-trpc": "^0.7.1",
|
||||||
"framer-motion": "^12.34.2",
|
"framer-motion": "^12.34.2",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { initTRPC } from '@trpc/server';
|
import { initTRPC } from '@trpc/server';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { getStore } from '../store';
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
|
|
||||||
@@ -134,6 +135,16 @@ const notesRouter = router({
|
|||||||
.mutation(() => null),
|
.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({
|
const aiRouter = router({
|
||||||
chat: publicProcedure
|
chat: publicProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
@@ -152,6 +163,7 @@ const aiRouter = router({
|
|||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
health: healthRouter,
|
health: healthRouter,
|
||||||
|
settings: settingsRouter,
|
||||||
clients: clientsRouter,
|
clients: clientsRouter,
|
||||||
projects: projectsRouter,
|
projects: projectsRouter,
|
||||||
tasks: tasksRouter,
|
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,
|
ClipboardCheck,
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
|
ChevronDown,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { trpc } from '@/lib/trpc';
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ to: '/', icon: House, label: 'Home' },
|
{ to: '/', icon: House, label: 'Home' },
|
||||||
@@ -21,10 +23,25 @@ interface AppShellProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AppShell({ children }: 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 routerState = useRouterState();
|
||||||
const currentPath = routerState.location.pathname;
|
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 (
|
return (
|
||||||
<div className="flex h-screen w-screen overflow-hidden bg-background">
|
<div className="flex h-screen w-screen overflow-hidden bg-background">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
@@ -90,7 +107,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
{/* Collapse toggle */}
|
{/* Collapse toggle */}
|
||||||
<div className="px-2 pb-3 shrink-0">
|
<div className="px-2 pb-3 shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCollapsed((c) => !c)}
|
onClick={handleToggle}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 h-8 px-3 rounded-md text-sm text-sidebar-foreground w-full',
|
'flex items-center gap-2 h-8 px-3 rounded-md text-sm text-sidebar-foreground w-full',
|
||||||
'hover:bg-sidebar-accent transition-colors',
|
'hover:bg-sidebar-accent transition-colors',
|
||||||
@@ -107,6 +124,19 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<main className="flex-1 min-w-0 overflow-hidden relative">
|
<main className="flex-1 min-w-0 overflow-hidden relative">
|
||||||
{children}
|
{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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
@import '@fontsource/geist/400.css';
|
||||||
|
@import '@fontsource/geist/500.css';
|
||||||
|
@import '@fontsource/geist/600.css';
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -44,5 +48,3 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Geist font — loaded via CDN in index.html */
|
|
||||||
|
|||||||
Reference in New Issue
Block a user