diff --git a/.claude/agents/architect.md b/.claude/agents/architect.md index 922cceb..aabb0aa 100644 --- a/.claude/agents/architect.md +++ b/.claude/agents/architect.md @@ -58,7 +58,7 @@ MODIFY: src/shared/ipc//schemas.ts — add Zod schemas src/shared/types/.ts — add type definitions src/renderer/app/routes/.routes.ts — add route - src/renderer/app/layouts/Sidebar.tsx — add nav item + src/renderer/app/layouts/sidebar-layouts/shared-nav.ts — add nav item ``` ### 3. Data Flow diff --git a/.claude/agents/codebase-guardian.md b/.claude/agents/codebase-guardian.md index 7edd1b6..4cbd931 100644 --- a/.claude/agents/codebase-guardian.md +++ b/.claude/agents/codebase-guardian.md @@ -176,6 +176,7 @@ projects: Project[]; // Should be in React Query // CORRECT — UI state in Zustand selectedTaskId: string | null; sidebarCollapsed: boolean; + ``` **Check:** Read each store file. Flag any server data stored in Zustand. diff --git a/.claude/agents/fitness-engineer.md b/.claude/agents/fitness-engineer.md index 9d757d4..d61459b 100644 --- a/.claude/agents/fitness-engineer.md +++ b/.claude/agents/fitness-engineer.md @@ -98,7 +98,7 @@ export interface FitnessService { ## Design System Awareness -This project has a design system at `src/renderer/shared/components/ui/` (30 primitives), imported via `@ui`. All UI-facing code must use these primitives instead of raw HTML elements. Key exports: Button, Input, Textarea, Label, Badge, Card, Spinner, Dialog, AlertDialog, Select, DropdownMenu, Tooltip, Tabs, Switch, Checkbox, Toast, ScrollArea, Popover, Progress, Slider, Collapsible, PageLayout, Typography, Grid, Stack, Flex, Container, Separator, Form system (FormField, FormInput, etc.). +This project has a design system at `src/renderer/shared/components/ui/` (30 primitives), imported via `@ui`. All UI-facing code must use these primitives instead of raw HTML elements. Key exports: Button, Input, Textarea, Label, Badge, Card, Spinner, Dialog, AlertDialog, Select, DropdownMenu, Tooltip, Tabs, Switch, Checkbox, Toast, ScrollArea, Popover, Progress, Slider, Collapsible, Sidebar (composable sidebar system), Breadcrumb (composable breadcrumb navigation), PageLayout, Typography, Grid, Stack, Flex, Container, Separator, Form system (FormField, FormInput, etc.). ## Handoff diff --git a/.claude/agents/hook-engineer.md b/.claude/agents/hook-engineer.md index dfee3d8..0c3ad84 100644 --- a/.claude/agents/hook-engineer.md +++ b/.claude/agents/hook-engineer.md @@ -236,7 +236,7 @@ Before marking work complete: ## Design System Awareness -This project has a design system at `src/renderer/shared/components/ui/` (30 primitives), imported via `@ui`. All UI-facing code must use these primitives instead of raw HTML elements. Key exports: Button, Input, Textarea, Label, Badge, Card, Spinner, Dialog, AlertDialog, Select, DropdownMenu, Tooltip, Tabs, Switch, Checkbox, Toast, ScrollArea, Popover, Progress, Slider, Collapsible, PageLayout, Typography, Grid, Stack, Flex, Container, Separator, Form system (FormField, FormInput, etc.). +This project has a design system at `src/renderer/shared/components/ui/` (30 primitives), imported via `@ui`. All UI-facing code must use these primitives instead of raw HTML elements. Key exports: Button, Input, Textarea, Label, Badge, Card, Spinner, Dialog, AlertDialog, Select, DropdownMenu, Tooltip, Tabs, Switch, Checkbox, Toast, ScrollArea, Popover, Progress, Slider, Collapsible, Sidebar (composable sidebar system), Breadcrumb (composable breadcrumb navigation), PageLayout, Typography, Grid, Stack, Flex, Container, Separator, Form system (FormField, FormInput, etc.). ## Handoff diff --git a/.claude/agents/router-engineer.md b/.claude/agents/router-engineer.md index 394c73f..df46ebc 100644 --- a/.claude/agents/router-engineer.md +++ b/.claude/agents/router-engineer.md @@ -24,7 +24,11 @@ Before modifying routing, read: - `communication.routes.ts` — Alerts, briefing - `settings.routes.ts` — Settings page - `misc.routes.ts` — Insights, changelog, health, screen, fitness -6. `src/renderer/app/layouts/Sidebar.tsx` — Sidebar navigation +6. `src/renderer/app/layouts/sidebar-layouts/shared-nav.ts` — Shared nav items (personalItems, developmentItems) +7. `src/renderer/app/layouts/sidebar-layouts/SidebarLayout*.tsx` — 16 sidebar layout variants +8. `src/renderer/app/layouts/LayoutWrapper.tsx` — Switches between sidebar layouts +9. `src/renderer/app/layouts/ContentHeader.tsx` — SidebarTrigger + Breadcrumbs +10. `src/renderer/app/layouts/AppBreadcrumbs.tsx` — Breadcrumb trail from route staticData 7. `src/renderer/app/layouts/ProjectTabBar.tsx` — Project tab bar 8. `src/renderer/app/layouts/RootLayout.tsx` — Root layout wrapper 9. `src/renderer/app/layouts/TopBar.tsx` — Top bar with CommandBar trigger @@ -38,7 +42,10 @@ ONLY modify these files: src/shared/constants/routes.ts — Route constants src/renderer/app/router.tsx — Root route tree assembly src/renderer/app/routes/*.ts(x) — Domain route group files - src/renderer/app/layouts/Sidebar.tsx — Sidebar nav items + src/renderer/app/layouts/sidebar-layouts/shared-nav.ts — Shared nav items (add here) + src/renderer/app/layouts/sidebar-layouts/*.tsx — Sidebar layout variants + src/renderer/app/layouts/LayoutWrapper.tsx — Layout switching + src/renderer/app/layouts/AppBreadcrumbs.tsx — Breadcrumbs src/renderer/app/layouts/ProjectTabBar.tsx — Project tabs src/renderer/app/layouts/TopBar.tsx — Top bar src/renderer/app/layouts/CommandBar.tsx — Command palette @@ -118,7 +125,7 @@ const routeTree = rootRoute.addChildren([ ### Step 3: Add Sidebar Nav Item ```typescript -// src/renderer/app/layouts/Sidebar.tsx +// src/renderer/app/layouts/sidebar-layouts/shared-nav.ts import { Calendar } from 'lucide-react'; @@ -157,7 +164,7 @@ const plannerRoute = createRoute({ ### Step 3: Add to Sidebar navItems ```typescript -// src/renderer/app/layouts/Sidebar.tsx +// src/renderer/app/layouts/sidebar-layouts/shared-nav.ts import { Calendar } from 'lucide-react'; @@ -249,7 +256,8 @@ Before marking work complete: - [ ] Route constants added to `src/shared/constants/routes.ts` - [ ] Route uses constants (not hardcoded strings) - [ ] Route added to `routeTree` in `router.tsx` -- [ ] Sidebar nav item added (if user-facing feature) +- [ ] Sidebar nav item added to shared-nav.ts (if user-facing feature) +- [ ] Route has staticData.breadcrumbLabel (for breadcrumb trail) - [ ] Navigation calls use `void` operator - [ ] Redirect uses `throw redirect()` with eslint-disable comment - [ ] Feature imported from barrel export (not internal path) @@ -260,7 +268,7 @@ Before marking work complete: ## Design System Awareness -This project has a design system at `src/renderer/shared/components/ui/` (30 primitives), imported via `@ui`. All UI-facing code must use these primitives instead of raw HTML elements. Key exports: Button, Input, Textarea, Label, Badge, Card, Spinner, Dialog, AlertDialog, Select, DropdownMenu, Tooltip, Tabs, Switch, Checkbox, Toast, ScrollArea, Popover, Progress, Slider, Collapsible, PageLayout, Typography, Grid, Stack, Flex, Container, Separator, Form system (FormField, FormInput, etc.). +This project has a design system at `src/renderer/shared/components/ui/` (30 primitives), imported via `@ui`. All UI-facing code must use these primitives instead of raw HTML elements. Key exports: Button, Input, Textarea, Label, Badge, Card, Spinner, Dialog, AlertDialog, Select, DropdownMenu, Tooltip, Tabs, Switch, Checkbox, Toast, ScrollArea, Popover, Progress, Slider, Collapsible, Sidebar (composable sidebar system), Breadcrumb (composable breadcrumb navigation), PageLayout, Typography, Grid, Stack, Flex, Container, Separator, Form system (FormField, FormInput, etc.). ## Handoff diff --git a/.claude/agents/store-engineer.md b/.claude/agents/store-engineer.md index e78917c..2537be2 100644 --- a/.claude/agents/store-engineer.md +++ b/.claude/agents/store-engineer.md @@ -107,10 +107,13 @@ import { create } from 'zustand'; interface LayoutState { sidebarCollapsed: boolean; + sidebarLayout: SidebarLayoutId; // Which of 16 sidebar layouts to use activeProjectId: string | null; - projectTabs: string[]; + projectTabOrder: string[]; toggleSidebar: () => void; + setSidebarCollapsed: (collapsed: boolean) => void; + setSidebarLayout: (id: SidebarLayoutId) => void; setActiveProject: (id: string | null) => void; addProjectTab: (id: string) => void; removeProjectTab: (id: string) => void; @@ -118,17 +121,20 @@ interface LayoutState { export const useLayoutStore = create()((set) => ({ sidebarCollapsed: false, + sidebarLayout: 'sidebar-07', activeProjectId: null, - projectTabs: [], + projectTabOrder: [], toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })), + setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }), + setSidebarLayout: (id) => set({ sidebarLayout: id }), setActiveProject: (id) => set({ activeProjectId: id }), addProjectTab: (id) => set((state) => ({ - projectTabs: state.projectTabs.includes(id) - ? state.projectTabs - : [...state.projectTabs, id], + projectTabOrder: state.projectTabOrder.includes(id) + ? state.projectTabOrder + : [...state.projectTabOrder, id], })), removeProjectTab: (id) => set((state) => ({ @@ -247,7 +253,7 @@ Before marking work complete: ## Design System Awareness -This project has a design system at `src/renderer/shared/components/ui/` (30 primitives), imported via `@ui`. All UI-facing code must use these primitives instead of raw HTML elements. Key exports: Button, Input, Textarea, Label, Badge, Card, Spinner, Dialog, AlertDialog, Select, DropdownMenu, Tooltip, Tabs, Switch, Checkbox, Toast, ScrollArea, Popover, Progress, Slider, Collapsible, PageLayout, Typography, Grid, Stack, Flex, Container, Separator, Form system (FormField, FormInput, etc.). +This project has a design system at `src/renderer/shared/components/ui/` (30 primitives), imported via `@ui`. All UI-facing code must use these primitives instead of raw HTML elements. Key exports: Button, Input, Textarea, Label, Badge, Card, Spinner, Dialog, AlertDialog, Select, DropdownMenu, Tooltip, Tabs, Switch, Checkbox, Toast, ScrollArea, Popover, Progress, Slider, Collapsible, Sidebar (composable sidebar system), Breadcrumb (composable breadcrumb navigation), PageLayout, Typography, Grid, Stack, Flex, Container, Separator, Form system (FormField, FormInput, etc.). ## Handoff diff --git a/ai-docs/DATA-FLOW.md b/ai-docs/DATA-FLOW.md index 0171262..e15a159 100644 --- a/ai-docs/DATA-FLOW.md +++ b/ai-docs/DATA-FLOW.md @@ -216,8 +216,9 @@ components/ | Shared stores: | | layout-store: | | sidebarCollapsed | + | sidebarLayout | | activeProjectId | - | projectTabs | + | projectTabOrder | | theme-store: | | mode (light/dark) | | colorTheme | @@ -585,7 +586,7 @@ Toast notification shows success/failure User clicks sidebar nav item | v -handleNav(path) Sidebar.tsx +handleNav(path) SidebarLayoutXX.tsx (via shared-nav.ts) | v navigate({ to: projectViewPath(id, path) }) diff --git a/ai-docs/FEATURES-INDEX.md b/ai-docs/FEATURES-INDEX.md index 503c9f9..620f38c 100644 --- a/ai-docs/FEATURES-INDEX.md +++ b/ai-docs/FEATURES-INDEX.md @@ -42,7 +42,7 @@ Location: `src/renderer/features/` | **productivity** | Productivity hub (8 tabs: Overview, Calendar, Spotify, Briefing, Notes, Planner, Alerts, Comms) | ProductivityPage (tabbed), CalendarWidget, SpotifyWidget; embeds BriefingPage, NotesPage, PlannerPage, AlertsPage, CommunicationsPage as tab content | `calendar.*`, `spotify.*`, `briefing.*`, `notes.*`, `planner.*`, `alerts.*` | | **projects** | Project management | ProjectListPage, ProjectSettings, WorktreeManager, ProjectEditDialog, GitStatusIndicator (branch name + clean/changed badge in project list) | `projects.*`, `git.status` | | **roadmap** | Project roadmap | RoadmapPage, MilestoneCard | `milestones.*` | -| **settings** | App settings (6-tab layout: Display, Profile, Hub, Integrations, Storage, Advanced) | SettingsPage (tabbed), ProfileFormModal (TanStack Form + Zod + FormSelect), HubSettings (ConnectionForm uses TanStack Form + Zod), OAuthProviderSettings, OAuthConnectionStatus, WebhookSettings (SlackForm + GitHubForm use TanStack Form + Zod), StorageManagementSection, StorageUsageBar, RetentionControl, ColorThemeSection; **Theme Editor** (`components/theme-editor/`, 10 files): ThemeEditorPage, ColorControl, ColorSection, ThemePreview, SavedThemesBar, CssImportDialog, css-parser.ts, css-exporter.ts, token-sections.ts — Route: `/settings/themes` | `settings.*`, `oauth.*`, `dataManagement.*` | +| **settings** | App settings (6-tab layout: Display, Profile, Hub, Integrations, Storage, Advanced) | SettingsPage (tabbed), LayoutSection (sidebar layout selector with SVG wireframe preview), ProfileFormModal (TanStack Form + Zod + FormSelect), HubSettings (ConnectionForm uses TanStack Form + Zod), OAuthProviderSettings, OAuthConnectionStatus, WebhookSettings (SlackForm + GitHubForm use TanStack Form + Zod), StorageManagementSection, StorageUsageBar, RetentionControl, ColorThemeSection; **Theme Editor** (`components/theme-editor/`, 10 files): ThemeEditorPage, ColorControl, ColorSection, ThemePreview, SavedThemesBar, CssImportDialog, css-parser.ts, css-exporter.ts, token-sections.ts — Route: `/settings/themes` | `settings.*`, `oauth.*`, `dataManagement.*` | | **tasks** | Task management (AG-Grid dashboard) | TaskDataGrid (AG-Grid v35 wrapped in `` from `@ui`; themed via compound `.ag-theme-quartz.ag-theme-claude` CSS class with design-system token overrides via AG-Grid Theming API in `ag-grid-modules.ts`), TaskFiltersToolbar, TaskDetailRow (subtasks use `?? []` fallback for Hub data), TaskStatusBadge, CreateTaskDialog, PlanFeedbackDialog, TaskResultView (status/duration/cost/log summary for completed tasks), CreatePrDialog (GitHub PR creation post-task-completion); **Hooks**: useTaskEvents (→ useAgentEvents + useQaEvents), useAgentMutations (useStartPlanning, useStartExecution, useReplanWithFeedback, useKillAgent, useRestartFromCheckpoint), useQaMutations, QaReportViewer | `hub.tasks.*`, `tasks.*`, `agent.*` (incl. `agent.replanWithFeedback`), `qa.*`, `git.createPr`, `event:agent.orchestrator.*`, `event:qa.*` | | **terminals** | Terminal emulator | TerminalGrid, TerminalInstance | `terminals.*` | | **briefing** | Daily briefing & suggestions *(accessed via Productivity > Briefing tab)* | BriefingPage, SuggestionCard | `briefing.*` | @@ -389,14 +389,41 @@ Location: `src/renderer/app/layouts/` | Layout | Purpose | |--------|---------| -| `RootLayout.tsx` | Root shell: renders TitleBar at top, then `react-resizable-panels` (Group/Panel/Separator) for sidebar + content layout with localStorage persistence. Sidebar panel is collapsible (collapses to 56px, minSize 160px, maxSize 300px) and syncs with layout store. | +| `RootLayout.tsx` | Root shell: renders TitleBar at top, then `LayoutWrapper` with the selected sidebar layout variant. Layout selection stored in layout store + persisted via settings IPC. | +| `LayoutWrapper.tsx` | Switches between 16 sidebar layout variants. Lazy-loads `SidebarLayoutXX` components. Wraps content in `SidebarProvider` + `SidebarInset` + `ContentHeader`. | +| `ContentHeader.tsx` | Shared content header bar: `[SidebarTrigger] \| [Breadcrumbs]`. Renders inside SidebarInset across all layout variants. | +| `AppBreadcrumbs.tsx` | Breadcrumb trail from TanStack Router `useMatches()`. Reads `staticData.breadcrumbLabel` from route matches. | | `TitleBar.tsx` | Custom frameless window title bar (32px). Drag region for window movement, utility buttons (screenshot, health indicator, hub status) separated by vertical divider from minimize/maximize/close window controls. Uses `window.*` IPC channels. | | `TitleBarScreenshot.tsx` | Camera icon button that captures the primary screen via `screen.listSources` + `screen.capture` IPC channels and copies PNG to clipboard. Shows checkmark feedback for 1.5s on success. | -| `Sidebar.tsx` | Navigation sidebar (fills its parent panel, collapse state driven by layout store). Uses `bg-sidebar text-sidebar-foreground` theme variables. | +| `Sidebar.tsx` | Legacy navigation sidebar (kept for reference). Replaced by `sidebar-layouts/SidebarLayoutXX.tsx` components. | | `TopBar.tsx` | Top bar with project tabs + add button | | `ProjectTabBar.tsx` | Horizontal tab bar for switching between open projects | | `UserMenu.tsx` | Avatar + logout dropdown in sidebar footer | +### Sidebar Layout Variants + +Location: `src/renderer/app/layouts/sidebar-layouts/` + +| File | Layout | Description | +|------|--------|-------------| +| `shared-nav.ts` | — | Shared nav items (`personalItems`, `developmentItems`, `settingsItem`, `addProjectItem`, `NavItem` type) | +| `SidebarLayout01.tsx` | Grouped | Simple sidebar with labeled section groups | +| `SidebarLayout02.tsx` | Collapsible Sections | Sidebar with collapsible section groups | +| `SidebarLayout03.tsx` | Submenus | Sidebar with inline expandable sub-items | +| `SidebarLayout04.tsx` | Floating | Floating sidebar with `variant="floating"` | +| `SidebarLayout05.tsx` | Collapsible Submenus | Collapsible sub-items with search input | +| `SidebarLayout06.tsx` | Dropdown Submenus | Sidebar with dropdown-style sub-items | +| `SidebarLayout07.tsx` | Icon Collapse | Sidebar that collapses to icons (`collapsible="icon"`) | +| `SidebarLayout08.tsx` | Inset + Secondary | Inset sidebar with `variant="inset"` | +| `SidebarLayout09.tsx` | Nested | Wide sidebar with deeply nested collapsible groups | +| `SidebarLayout10.tsx` | Popover | Floating icon-collapsible sidebar | +| `SidebarLayout11.tsx` | File Tree | Sidebar with tree hierarchy using SidebarMenuSub | +| `SidebarLayout12.tsx` | Calendar | Sidebar with date display widget | +| `SidebarLayout13.tsx` | Dialog | Offcanvas sidebar (`collapsible="offcanvas"`) | +| `SidebarLayout14.tsx` | Right Side | Right-aligned sidebar (`side="right"`) | +| `SidebarLayout15.tsx` | Dual | Left + right sidebar layout | +| `SidebarLayout16.tsx` | Sticky Header | Sidebar with sticky header section | + --- ## 6.5 Shared UI Components @@ -453,6 +480,8 @@ All primitives follow the **shadcn/ui pattern**: CVA variants, `data-slot` attri | **2: Radix** | Progress | `ui/progress.tsx` | Progress bar with CVA sizes | | **2: Radix** | Slider | `ui/slider.tsx` | Slider with track/range/thumb | | **2: Radix** | Collapsible | `ui/collapsible.tsx` | Collapsible, CollapsibleTrigger, CollapsibleContent | +| **2: Sidebar** | Sidebar | `ui/sidebar.tsx` | `SidebarProvider`, `Sidebar`, `SidebarContent`, `SidebarFooter`, `SidebarGroup`, `SidebarGroupAction`, `SidebarGroupContent`, `SidebarGroupLabel`, `SidebarHeader`, `SidebarInset`, `SidebarInput`, `SidebarMenu`, `SidebarMenuAction`, `SidebarMenuBadge`, `SidebarMenuButton`, `SidebarMenuItem`, `SidebarMenuSkeleton`, `SidebarMenuSub`, `SidebarMenuSubButton`, `SidebarMenuSubItem`, `SidebarRail`, `SidebarSeparator`, `SidebarTrigger`, `useSidebar`, `sidebarMenuButtonVariants` — shadcn composable sidebar system, adapted for Electron (no mobile Sheet) | +| **2: Breadcrumb** | Breadcrumb | `ui/breadcrumb.tsx` | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis` — composable breadcrumb navigation | | **3: Form** | Form System | `ui/form.tsx` | `Form`, `FormField`, `FormInput`, `FormTextarea`, `FormSelect`, `FormCheckbox`, `FormSwitch` — TanStack Form + Zod v4 integration. `Form` and field components exported from `@ui` barrel. Import `useForm` from `@tanstack/react-form` directly. | ## 6.6 Shared Hooks diff --git a/ai-docs/PATTERNS.md b/ai-docs/PATTERNS.md index 973ebc5..a7d0c52 100644 --- a/ai-docs/PATTERNS.md +++ b/ai-docs/PATTERNS.md @@ -948,49 +948,44 @@ Custom animation utility classes live **outside** the `@theme` block in `globals - Custom animations outside `@theme` need explicit `.animate-*` class definitions - Always use `color-mix(in srgb, var(--token), transparent)` for semi-transparent effects in animations -## Sidebar Collapse Pattern +## Customizable Sidebar Layout Pattern -The sidebar uses `react-resizable-panels` with bidirectional sync between the panel and the layout store. +The app uses a **composable sidebar layout system** with 16 layout variants. The selected layout is stored in `layout-store.sidebarLayout` and persisted via `settings.sidebarLayout`. -**Key configuration** in `RootLayout.tsx`: -```tsx - +**Architecture**: ``` - -**Collapse detection** — use `panel.isCollapsed()`, NOT layout size comparison: -```tsx -const handleLayoutChanged = useCallback( - (layout: Record) => { - onLayoutChanged(layout); - setPanelLayout(layout); - // Use imperative API — layout values are flexGrow numbers, not px/% sizes - const collapsed = sidebarPanelRef.current?.isCollapsed() ?? false; - setSidebarCollapsed(collapsed); - }, - [onLayoutChanged, setPanelLayout, setSidebarCollapsed, sidebarPanelRef], -); +RootLayout → LayoutWrapper(layoutId) → SidebarProvider + SidebarLayoutXX + SidebarInset + └→ ContentHeader (SidebarTrigger + Breadcrumbs) ``` -**Store-to-panel sync** — `useEffect` watches `sidebarCollapsed` and calls `panel.collapse()` / `panel.expand()`: +**Layout selection**: ```tsx -useEffect(() => { - const panel = sidebarPanelRef.current; - if (!panel) return; - if (sidebarCollapsed && !panel.isCollapsed()) panel.collapse(); - else if (!sidebarCollapsed && panel.isCollapsed()) panel.expand(); -}, [sidebarCollapsed, sidebarPanelRef]); +// Read from store +const sidebarLayout = useLayoutStore((s) => s.sidebarLayout); + +// Change layout (updates store + persists via IPC) +const { setSidebarLayout } = useLayoutStore(); +const updateSettings = useUpdateSettings(); +setSidebarLayout('sidebar-07'); +updateSettings.mutate({ sidebarLayout: 'sidebar-07' }); ``` -Rules: -- `collapsedSize` MUST match `minSize` so the sidebar snaps to icon-only when dragged to minimum -- Always use `panel.isCollapsed()` to detect collapsed state — never compare flexGrow values -- The `Sidebar` component reads `sidebarCollapsed` from `useLayoutStore` to conditionally render labels +**Adding a new sidebar layout**: +1. Create `SidebarLayoutXX.tsx` in `src/renderer/app/layouts/sidebar-layouts/` +2. Import nav items from `./shared-nav` (personalItems, developmentItems, settingsItem, addProjectItem) +3. Use shadcn Sidebar composables from `@ui/sidebar` +4. Add lazy import to `LAYOUT_MAP` in `LayoutWrapper.tsx` +5. Add entry to `SIDEBAR_LAYOUTS` and `SIDEBAR_LAYOUT_IDS` in `src/shared/types/layout.ts` +6. Add preview config to `LAYOUT_PREVIEWS` in `LayoutSection.tsx` + +**Breadcrumbs**: +Routes declare `staticData: { breadcrumbLabel: 'Name' }` and `AppBreadcrumbs` reads from `useMatches()` to build the trail. + +**Key shadcn Sidebar props**: +- `side="left" | "right"` — sidebar position +- `variant="default" | "floating" | "inset"` — visual style +- `collapsible="offcanvas" | "icon" | "none"` — collapse behavior +- `SidebarMenuButton tooltip="Label"` — shows tooltip when icon-collapsed ## Agent Orchestrator Pattern diff --git a/ai-docs/user-interface-flow.md b/ai-docs/user-interface-flow.md index a72b85d..226839f 100644 --- a/ai-docs/user-interface-flow.md +++ b/ai-docs/user-interface-flow.md @@ -276,8 +276,11 @@ After auth + onboarding, the user sees the main app shell: | Component | File | Purpose | |-----------|------|---------| -| `RootLayout` | `src/renderer/app/layouts/RootLayout.tsx` | Shell: sidebar (collapsible, minSize 160px) + topbar + outlet + notifications | -| `Sidebar` | `src/renderer/app/layouts/Sidebar.tsx` | Two accordion sections: Development (first, with +Add Project as first child, then project-scoped items) and Personal (Dashboard, My Work, Fitness, Productivity). Briefing/Notes/Planner/Alerts/Comms removed — now Productivity tabs. Collapsible. Uses `bg-sidebar text-sidebar-foreground` theme vars. | +| `RootLayout` | `src/renderer/app/layouts/RootLayout.tsx` | Shell: TitleBar + LayoutWrapper (selected sidebar layout variant) + topbar + outlet + notifications | +| `LayoutWrapper` | `src/renderer/app/layouts/LayoutWrapper.tsx` | Switches between 16 sidebar layout variants. Lazy-loads SidebarLayoutXX, wraps in SidebarProvider + SidebarInset + ContentHeader. | +| `ContentHeader` | `src/renderer/app/layouts/ContentHeader.tsx` | Shared content header bar: SidebarTrigger + Breadcrumbs separator | +| `AppBreadcrumbs` | `src/renderer/app/layouts/AppBreadcrumbs.tsx` | Breadcrumb trail from TanStack Router useMatches() staticData.breadcrumbLabel | +| `SidebarLayoutXX` | `src/renderer/app/layouts/sidebar-layouts/` | 16 sidebar layout variants (01=Grouped, 02=Collapsible, 03=Submenus, 04=Floating, 05=Collapsible+Search, 06=Dropdown, 07=Icon Collapse, 08=Inset, 09=Nested, 10=Popover, 11=File Tree, 12=Calendar, 13=Dialog/Offcanvas, 14=Right Side, 15=Dual, 16=Sticky Header) | | `TopBar` | `src/renderer/app/layouts/TopBar.tsx` | Project tabs + add button (utility buttons moved to TitleBar; AssistantWidget provides global assistant access) | | `ProjectTabBar` | `src/renderer/app/layouts/ProjectTabBar.tsx` | Horizontal tab bar for switching between open projects | | `UserMenu` | `src/renderer/app/layouts/UserMenu.tsx` | Avatar + logout dropdown in sidebar footer (above HubConnectionIndicator) | diff --git a/docs/tracker.json b/docs/tracker.json index af9ed5e..ee0ff58 100644 --- a/docs/tracker.json +++ b/docs/tracker.json @@ -61,6 +61,16 @@ "branch": "feature/ui-layout-refactor", "pr": null, "tags": ["ui", "refactor", "sidebar", "settings", "ag-grid"] + }, + "customizable-sidebar-layouts": { + "title": "Customizable Sidebar Layout System", + "status": "IN_PROGRESS", + "planFile": null, + "created": "2026-02-21", + "statusChangedAt": "2026-02-21", + "branch": "feature/customizable-sidebar-layouts", + "pr": null, + "tags": ["ui", "sidebar", "layouts", "shadcn", "breadcrumbs"] } } } diff --git a/src/renderer/app/layouts/AppBreadcrumbs.tsx b/src/renderer/app/layouts/AppBreadcrumbs.tsx new file mode 100644 index 0000000..5ac6f04 --- /dev/null +++ b/src/renderer/app/layouts/AppBreadcrumbs.tsx @@ -0,0 +1,52 @@ +/** + * AppBreadcrumbs — Breadcrumb trail from TanStack Router matches + * + * Reads staticData.breadcrumbLabel from route matches to build + * a navigation breadcrumb trail. Used in ContentHeader. + */ + +import { Link, useMatches } from '@tanstack/react-router'; + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@ui/breadcrumb'; + +export function AppBreadcrumbs() { + const matches = useMatches(); + const crumbs = matches.filter( + (m) => typeof m.staticData.breadcrumbLabel === 'string' && m.staticData.breadcrumbLabel.length > 0, + ); + + if (crumbs.length === 0) return null; + + return ( + + + {crumbs.map((match, i) => { + const label = match.staticData.breadcrumbLabel ?? ''; + const isLast = i === crumbs.length - 1; + + return ( + + {isLast ? ( + {label} + ) : ( + <> + + {label} + + + + )} + + ); + })} + + + ); +} diff --git a/src/renderer/app/layouts/ContentHeader.tsx b/src/renderer/app/layouts/ContentHeader.tsx new file mode 100644 index 0000000..f237eea --- /dev/null +++ b/src/renderer/app/layouts/ContentHeader.tsx @@ -0,0 +1,22 @@ +/** + * ContentHeader — Shared content header bar for all sidebar layouts + * + * Renders: [SidebarTrigger] | [Breadcrumbs] + * Used inside SidebarInset across all 16 layout variants. + */ + +import { Separator } from '@radix-ui/react-separator'; + +import { SidebarTrigger } from '@ui/sidebar'; + +import { AppBreadcrumbs } from './AppBreadcrumbs'; + +export function ContentHeader() { + return ( +
+ + + +
+ ); +} diff --git a/src/renderer/app/layouts/LayoutWrapper.tsx b/src/renderer/app/layouts/LayoutWrapper.tsx new file mode 100644 index 0000000..192d929 --- /dev/null +++ b/src/renderer/app/layouts/LayoutWrapper.tsx @@ -0,0 +1,98 @@ +/** + * LayoutWrapper — Switches between 16 sidebar layout variants + * + * Reads the selected sidebarLayout from the layout store and + * lazy-loads the corresponding SidebarLayoutXX component. + * Wraps children in SidebarProvider + SidebarInset. + */ + +import { Suspense, lazy } from 'react'; + +import { Loader2 } from 'lucide-react'; + +import type { SidebarLayoutId } from '@shared/types/layout'; + + +import { SidebarInset, SidebarProvider } from '@ui/sidebar'; + +import { ContentHeader } from './ContentHeader'; + +const LAYOUT_MAP: Record> = { + 'sidebar-01': lazy(() => + import('./sidebar-layouts/SidebarLayout01').then((m) => ({ default: m.SidebarLayout01 })), + ), + 'sidebar-02': lazy(() => + import('./sidebar-layouts/SidebarLayout02').then((m) => ({ default: m.SidebarLayout02 })), + ), + 'sidebar-03': lazy(() => + import('./sidebar-layouts/SidebarLayout03').then((m) => ({ default: m.SidebarLayout03 })), + ), + 'sidebar-04': lazy(() => + import('./sidebar-layouts/SidebarLayout04').then((m) => ({ default: m.SidebarLayout04 })), + ), + 'sidebar-05': lazy(() => + import('./sidebar-layouts/SidebarLayout05').then((m) => ({ default: m.SidebarLayout05 })), + ), + 'sidebar-06': lazy(() => + import('./sidebar-layouts/SidebarLayout06').then((m) => ({ default: m.SidebarLayout06 })), + ), + 'sidebar-07': lazy(() => + import('./sidebar-layouts/SidebarLayout07').then((m) => ({ default: m.SidebarLayout07 })), + ), + 'sidebar-08': lazy(() => + import('./sidebar-layouts/SidebarLayout08').then((m) => ({ default: m.SidebarLayout08 })), + ), + 'sidebar-09': lazy(() => + import('./sidebar-layouts/SidebarLayout09').then((m) => ({ default: m.SidebarLayout09 })), + ), + 'sidebar-10': lazy(() => + import('./sidebar-layouts/SidebarLayout10').then((m) => ({ default: m.SidebarLayout10 })), + ), + 'sidebar-11': lazy(() => + import('./sidebar-layouts/SidebarLayout11').then((m) => ({ default: m.SidebarLayout11 })), + ), + 'sidebar-12': lazy(() => + import('./sidebar-layouts/SidebarLayout12').then((m) => ({ default: m.SidebarLayout12 })), + ), + 'sidebar-13': lazy(() => + import('./sidebar-layouts/SidebarLayout13').then((m) => ({ default: m.SidebarLayout13 })), + ), + 'sidebar-14': lazy(() => + import('./sidebar-layouts/SidebarLayout14').then((m) => ({ default: m.SidebarLayout14 })), + ), + 'sidebar-15': lazy(() => + import('./sidebar-layouts/SidebarLayout15').then((m) => ({ default: m.SidebarLayout15 })), + ), + 'sidebar-16': lazy(() => + import('./sidebar-layouts/SidebarLayout16').then((m) => ({ default: m.SidebarLayout16 })), + ), +}; + +function SidebarSkeleton() { + return ( +
+ +
+ ); +} + +interface LayoutWrapperProps { + children: React.ReactNode; + layoutId: SidebarLayoutId; +} + +export function LayoutWrapper({ children, layoutId }: LayoutWrapperProps) { + const Layout = LAYOUT_MAP[layoutId]; + + return ( + + }> + + + + + {children} + + + ); +} diff --git a/src/renderer/app/layouts/RootLayout.tsx b/src/renderer/app/layouts/RootLayout.tsx index d65e261..c8dd393 100644 --- a/src/renderer/app/layouts/RootLayout.tsx +++ b/src/renderer/app/layouts/RootLayout.tsx @@ -1,19 +1,18 @@ /** * RootLayout — App shell * - * Uses react-resizable-panels for the sidebar + content layout. - * The sidebar panel is collapsible and the content panel fills remaining space. - * This is the only layout component — features render inside . + * Uses LayoutWrapper to render the selected sidebar layout variant. + * The layout selection is stored in the layout store and persisted via settings. + * Features render inside . * * If onboarding is not complete, shows the OnboardingWizard instead. */ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Outlet, useRouterState } from '@tanstack/react-router'; import { Loader2 } from 'lucide-react'; -import { Group, Panel, Separator, useDefaultLayout, usePanelRef } from 'react-resizable-panels'; import { AppUpdateNotification } from '@renderer/shared/components/AppUpdateNotification'; import { AuthNotification } from '@renderer/shared/components/AuthNotification'; @@ -30,14 +29,10 @@ import { OnboardingWizard } from '@features/onboarding'; import { useSettings } from '@features/settings'; import { hubKeys, useHubStatus } from '@features/settings/api/useHub'; -import { Sidebar } from './Sidebar'; +import { LayoutWrapper } from './LayoutWrapper'; import { TitleBar } from './TitleBar'; import { TopBar } from './TopBar'; -const SIDEBAR_PANEL_ID = 'sidebar'; -const CONTENT_PANEL_ID = 'content'; -const LAYOUT_STORAGE_ID = 'adc-main-layout'; - export function RootLayout() { const queryClient = useQueryClient(); const { data: settings, isLoading } = useSettings(); @@ -45,41 +40,7 @@ export function RootLayout() { const [onboardingJustCompleted, setOnboardingJustCompleted] = useState(false); const pathname = useRouterState({ select: (s) => s.location.pathname }); const pushRoute = useRouteHistoryStore((s) => s.pushRoute); - const { sidebarCollapsed, setSidebarCollapsed, setPanelLayout } = useLayoutStore(); - - // Panel refs for imperative control - const sidebarPanelRef = usePanelRef(); - - // Persist layout to localStorage - const { defaultLayout, onLayoutChanged } = useDefaultLayout({ - id: LAYOUT_STORAGE_ID, - storage: localStorage, - }); - - // Sync sidebar collapse state with panel collapse - const handleLayoutChanged = useCallback( - (layout: Record) => { - onLayoutChanged(layout); - setPanelLayout(layout); - - // Sync collapsed state using the panel's imperative API - const collapsed = sidebarPanelRef.current?.isCollapsed() ?? false; - setSidebarCollapsed(collapsed); - }, - [onLayoutChanged, setPanelLayout, setSidebarCollapsed, sidebarPanelRef], - ); - - // When sidebarCollapsed changes from external toggle, sync to panel - useEffect(() => { - const panel = sidebarPanelRef.current; - if (!panel) return; - - if (sidebarCollapsed && !panel.isCollapsed()) { - panel.collapse(); - } else if (!sidebarCollapsed && panel.isCollapsed()) { - panel.expand(); - } - }, [sidebarCollapsed, sidebarPanelRef]); + const sidebarLayout = useLayoutStore((s) => s.sidebarLayout); // Activate error/health event listeners useErrorEvents(); @@ -123,41 +84,21 @@ export function RootLayout() {
- - - - - - -
- - {hubStatus?.status === 'disconnected' || hubStatus?.status === 'error' ? ( -
- Hub disconnected. Some features may be unavailable. -
- ) : null} -
- - - -
-
-
-
+
+ + + {hubStatus?.status === 'disconnected' || hubStatus?.status === 'error' ? ( +
+ Hub disconnected. Some features may be unavailable. +
+ ) : null} +
+ + + +
+
+
diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout01.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout01.tsx new file mode 100644 index 0000000..0f9b424 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout01.tsx @@ -0,0 +1,144 @@ +/** SidebarLayout01 — Grouped: Simple sidebar with labeled section groups */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout01() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout02.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout02.tsx new file mode 100644 index 0000000..f986857 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout02.tsx @@ -0,0 +1,164 @@ +/** SidebarLayout02 — Collapsible Sections: Sidebar with collapsible group headers */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { ChevronDown } from 'lucide-react'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@ui/collapsible'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout02() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + + + + Development + + + + + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + Personal + + + + + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout03.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout03.tsx new file mode 100644 index 0000000..ee3e0e5 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout03.tsx @@ -0,0 +1,144 @@ +/** SidebarLayout03 — Submenus: Sidebar with inline expandable sub-items */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout03() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout04.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout04.tsx new file mode 100644 index 0000000..414f9f7 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout04.tsx @@ -0,0 +1,164 @@ +/** SidebarLayout04 — Floating: Visually detached sidebar with collapsible sections */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { ChevronDown } from 'lucide-react'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@ui/collapsible'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout04() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + + + + Development + + + + + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + Personal + + + + + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout05.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout05.tsx new file mode 100644 index 0000000..f8d3a57 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout05.tsx @@ -0,0 +1,170 @@ +/** SidebarLayout05 — Collapsible Submenus with search input */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { ChevronDown, Settings } from 'lucide-react'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@ui'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout05() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + ADC + + + + + + + + + Development + + + + + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + + + Project Views + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + + + Personal + + + + + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout06.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout06.tsx new file mode 100644 index 0000000..17d4f09 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout06.tsx @@ -0,0 +1,134 @@ +/** SidebarLayout06 — Grouped navigation with action buttons */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { Settings } from 'lucide-react'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout06() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout07.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout07.tsx new file mode 100644 index 0000000..640a8dd --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout07.tsx @@ -0,0 +1,145 @@ +/** SidebarLayout07 — Icon Collapse sidebar that collapses to icons only */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { Settings } from 'lucide-react'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout07() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout08.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout08.tsx new file mode 100644 index 0000000..2a2fd2b --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout08.tsx @@ -0,0 +1,139 @@ +/** SidebarLayout08 — Inset sidebar with secondary navigation */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { Settings } from 'lucide-react'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout08() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + ADC + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout09.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout09.tsx new file mode 100644 index 0000000..9abcc9d --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout09.tsx @@ -0,0 +1,212 @@ +/** SidebarLayout09 — Nested: Collapsible nested sidebars with wider default width */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { ChevronDown } from 'lucide-react'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@ui/collapsible'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +const codeItems = developmentItems.slice(0, 3); +const planItems = developmentItems.slice(3, 6); +const trackItems = developmentItems.slice(6, 9); + +export function SidebarLayout09() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + ADC + + + + + + + + Development + + + + + + +
+ + Code + + + + + {codeItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + +
+
+ + +
+ + Plan + + + + + {planItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + +
+
+ + +
+ + Track + + + + + {trackItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + +
+
+
+
+
+
+ + + + + + Personal + + + + + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + +
+ + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + +
+ ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout10.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout10.tsx new file mode 100644 index 0000000..e039228 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout10.tsx @@ -0,0 +1,144 @@ +/** SidebarLayout10 — Popover: Floating icon-collapsible sidebar with popover visual style */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout10() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout11.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout11.tsx new file mode 100644 index 0000000..9f87f1e --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout11.tsx @@ -0,0 +1,149 @@ +/** SidebarLayout11 — File Tree: Sidebar with collapsible tree hierarchy using sub-menus */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout11() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout12.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout12.tsx new file mode 100644 index 0000000..4a95808 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout12.tsx @@ -0,0 +1,156 @@ +/** SidebarLayout12 — Calendar: Sidebar with a date display widget at the top */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout12() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + const now = new Date(); + const formattedDate = now.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + return ( + + + + ADC + +
+

{now.getDate()}

+

{formattedDate}

+
+
+ + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + +
+ ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout13.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout13.tsx new file mode 100644 index 0000000..17f0fe5 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout13.tsx @@ -0,0 +1,142 @@ +/** SidebarLayout13 — Dialog: Offcanvas sidebar that fully hides when collapsed */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout13() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + ADC + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout14.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout14.tsx new file mode 100644 index 0000000..621baaa --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout14.tsx @@ -0,0 +1,144 @@ +/** SidebarLayout14 — Right Side: Sidebar aligned to the right edge of the viewport */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout14() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout15.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout15.tsx new file mode 100644 index 0000000..ac49db8 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout15.tsx @@ -0,0 +1,174 @@ +/** SidebarLayout15 — Dual: Left navigation sidebar paired with a right utility sidebar */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout15() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + <> + + + + ADC + + + + + + Development + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + Personal + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + + + + + + + Quick Actions + + + + + + Actions + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/SidebarLayout16.tsx b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout16.tsx new file mode 100644 index 0000000..ae6fbab --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/SidebarLayout16.tsx @@ -0,0 +1,166 @@ +/** SidebarLayout16 — Sticky Header: Sidebar with persistent non-scrolling header and collapsible sections */ + +import { useNavigate, useRouterState } from '@tanstack/react-router'; +import { ChevronDown } from 'lucide-react'; + +import { projectViewPath } from '@shared/constants'; + +import { HubConnectionIndicator } from '@renderer/shared/components/HubConnectionIndicator'; +import { useLayoutStore } from '@renderer/shared/stores'; + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@ui/collapsible'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + SidebarSeparator, + useSidebar, +} from '@ui/sidebar'; + +import { UserMenu } from '../UserMenu'; + +import { + addProjectItem, + developmentItems, + personalItems, + settingsItem, +} from './shared-nav'; + +export function SidebarLayout16() { + const navigate = useNavigate(); + const routerState = useRouterState(); + const { activeProjectId, addProjectTab } = useLayoutStore(); + const { open } = useSidebar(); + const currentPath = routerState.location.pathname; + + const urlProjectId = /^\/projects\/([^/]+)/.exec(currentPath)?.[1] ?? null; + if (urlProjectId !== null && urlProjectId !== activeProjectId) { + addProjectTab(urlProjectId); + } + + function isPersonalActive(path: string): boolean { + return currentPath === path || currentPath.startsWith(`${path}/`); + } + + function isDevActive(path: string): boolean { + return activeProjectId !== null && currentPath.endsWith(`/${path}`); + } + + function handlePersonalNav(path: string) { + void navigate({ to: path }); + } + + function handleDevNav(path: string) { + if (activeProjectId === null) return; + void navigate({ to: projectViewPath(activeProjectId, path) }); + } + + return ( + + + + ADC + + + + + + + + + + Development + + + + + + + + handlePersonalNav(addProjectItem.path)} + > + + {addProjectItem.label} + + + {developmentItems.map((item) => ( + + handleDevNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + Personal + + + + + + + {personalItems.map((item) => ( + + handlePersonalNav(item.path)} + > + + {item.label} + + + ))} + + + + + + + + + + + + + handlePersonalNav(settingsItem.path)} + > + + {settingsItem.label} + + + + + + + + ); +} diff --git a/src/renderer/app/layouts/sidebar-layouts/shared-nav.ts b/src/renderer/app/layouts/sidebar-layouts/shared-nav.ts new file mode 100644 index 0000000..68e6f30 --- /dev/null +++ b/src/renderer/app/layouts/sidebar-layouts/shared-nav.ts @@ -0,0 +1,73 @@ +/** + * shared-nav — Shared navigation items for all sidebar layouts + * + * Extracted from Sidebar.tsx. Every SidebarLayoutXX component imports + * these items instead of duplicating the navigation data. + */ + + +import { + BarChart3, + Bot, + Briefcase, + Dumbbell, + GitBranch, + Headphones, + Home, + Lightbulb, + ListTodo, + Map, + Plus, + ScrollText, + Settings, + Terminal, + Workflow, +} from 'lucide-react'; + +import { PROJECT_VIEWS, ROUTES } from '@shared/constants'; + +import type { LucideIcon } from 'lucide-react'; + +// ── Types ────────────────────────────────────────────────────── + +export interface NavItem { + label: string; + icon: LucideIcon; + path: string; +} + +// ── Navigation Data ──────────────────────────────────────────── + +/** Personal nav items (not project-scoped) */ +export const personalItems: NavItem[] = [ + { label: 'Dashboard', icon: Home, path: ROUTES.DASHBOARD }, + { label: 'My Work', icon: Briefcase, path: ROUTES.MY_WORK }, + { label: 'Fitness', icon: Dumbbell, path: ROUTES.FITNESS }, + { label: 'Productivity', icon: Headphones, path: ROUTES.PRODUCTIVITY }, +]; + +/** Development nav items (project-scoped) */ +export const developmentItems: NavItem[] = [ + { label: 'Tasks', icon: ListTodo, path: PROJECT_VIEWS.TASKS }, + { label: 'Terminals', icon: Terminal, path: PROJECT_VIEWS.TERMINALS }, + { label: 'Agents', icon: Bot, path: PROJECT_VIEWS.AGENTS }, + { label: 'Pipeline', icon: Workflow, path: PROJECT_VIEWS.WORKFLOW }, + { label: 'Roadmap', icon: Map, path: PROJECT_VIEWS.ROADMAP }, + { label: 'Ideation', icon: Lightbulb, path: PROJECT_VIEWS.IDEATION }, + { label: 'GitHub', icon: GitBranch, path: PROJECT_VIEWS.GITHUB }, + { label: 'Changelog', icon: ScrollText, path: PROJECT_VIEWS.CHANGELOG }, + { label: 'Insights', icon: BarChart3, path: PROJECT_VIEWS.INSIGHTS }, +]; + +/** Standalone nav items (footer section) */ +export const settingsItem: NavItem = { + label: 'Settings', + icon: Settings, + path: ROUTES.SETTINGS, +}; + +export const addProjectItem: NavItem = { + label: 'Add Project', + icon: Plus, + path: ROUTES.PROJECTS, +}; diff --git a/src/renderer/app/router.tsx b/src/renderer/app/router.tsx index de4dfc5..af2283d 100644 --- a/src/renderer/app/router.tsx +++ b/src/renderer/app/router.tsx @@ -143,6 +143,9 @@ declare module '@tanstack/react-router' { interface Register { router: typeof router; } + interface StaticDataRouteOption { + breadcrumbLabel?: string; + } } export function AppRouter() { diff --git a/src/renderer/app/routes/communication.routes.ts b/src/renderer/app/routes/communication.routes.ts index 9ceb288..14439a5 100644 --- a/src/renderer/app/routes/communication.routes.ts +++ b/src/renderer/app/routes/communication.routes.ts @@ -16,6 +16,7 @@ export function createCommunicationRoutes(appLayoutRoute: AnyRoute) { const communicationsRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.COMMUNICATIONS, + staticData: { breadcrumbLabel: 'Communications' }, pendingComponent: GenericPageSkeleton, component: lazyRouteComponent( () => import('@features/communications'), diff --git a/src/renderer/app/routes/dashboard.routes.ts b/src/renderer/app/routes/dashboard.routes.ts index 97611dd..8265cad 100644 --- a/src/renderer/app/routes/dashboard.routes.ts +++ b/src/renderer/app/routes/dashboard.routes.ts @@ -16,6 +16,7 @@ export function createDashboardRoutes(appLayoutRoute: AnyRoute) { const dashboardRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.DASHBOARD, + staticData: { breadcrumbLabel: 'Dashboard' }, pendingComponent: DashboardSkeleton, component: lazyRouteComponent( () => import('@features/dashboard'), @@ -26,6 +27,7 @@ export function createDashboardRoutes(appLayoutRoute: AnyRoute) { const myWorkRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.MY_WORK, + staticData: { breadcrumbLabel: 'My Work' }, pendingComponent: DashboardSkeleton, component: lazyRouteComponent( () => import('@features/my-work'), diff --git a/src/renderer/app/routes/misc.routes.ts b/src/renderer/app/routes/misc.routes.ts index 988c7e2..6801219 100644 --- a/src/renderer/app/routes/misc.routes.ts +++ b/src/renderer/app/routes/misc.routes.ts @@ -16,6 +16,7 @@ export function createMiscRoutes(appLayoutRoute: AnyRoute) { const briefingRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.BRIEFING, + staticData: { breadcrumbLabel: 'Briefing' }, pendingComponent: GenericPageSkeleton, component: lazyRouteComponent( () => import('@features/briefing'), @@ -26,6 +27,7 @@ export function createMiscRoutes(appLayoutRoute: AnyRoute) { const fitnessRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.FITNESS, + staticData: { breadcrumbLabel: 'Fitness' }, pendingComponent: GenericPageSkeleton, component: lazyRouteComponent( () => import('@features/fitness'), diff --git a/src/renderer/app/routes/productivity.routes.ts b/src/renderer/app/routes/productivity.routes.ts index a4a73d6..a85e209 100644 --- a/src/renderer/app/routes/productivity.routes.ts +++ b/src/renderer/app/routes/productivity.routes.ts @@ -16,6 +16,7 @@ export function createProductivityRoutes(appLayoutRoute: AnyRoute) { const alertsRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.ALERTS, + staticData: { breadcrumbLabel: 'Alerts' }, pendingComponent: GenericPageSkeleton, component: lazyRouteComponent( () => import('@features/alerts'), @@ -26,6 +27,7 @@ export function createProductivityRoutes(appLayoutRoute: AnyRoute) { const notesRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.NOTES, + staticData: { breadcrumbLabel: 'Notes' }, pendingComponent: GenericPageSkeleton, component: lazyRouteComponent( () => import('@features/notes'), @@ -36,6 +38,7 @@ export function createProductivityRoutes(appLayoutRoute: AnyRoute) { const plannerRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.PLANNER, + staticData: { breadcrumbLabel: 'Planner' }, pendingComponent: GenericPageSkeleton, component: lazyRouteComponent( () => import('@features/planner'), @@ -46,6 +49,7 @@ export function createProductivityRoutes(appLayoutRoute: AnyRoute) { const plannerWeeklyRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.PLANNER_WEEKLY, + staticData: { breadcrumbLabel: 'Weekly Review' }, pendingComponent: GenericPageSkeleton, component: lazyRouteComponent( () => import('@features/planner'), @@ -56,6 +60,7 @@ export function createProductivityRoutes(appLayoutRoute: AnyRoute) { const productivityRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.PRODUCTIVITY, + staticData: { breadcrumbLabel: 'Productivity' }, pendingComponent: GenericPageSkeleton, component: lazyRouteComponent( () => import('@features/productivity'), diff --git a/src/renderer/app/routes/project.routes.ts b/src/renderer/app/routes/project.routes.ts index 979e237..d6b5b5d 100644 --- a/src/renderer/app/routes/project.routes.ts +++ b/src/renderer/app/routes/project.routes.ts @@ -17,6 +17,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const projectsRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.PROJECTS, + staticData: { breadcrumbLabel: 'Projects' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/projects'), @@ -27,6 +28,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const projectRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT, + staticData: { breadcrumbLabel: 'Project' }, beforeLoad: ({ params }) => { // eslint-disable-next-line @typescript-eslint/only-throw-error -- TanStack Router redirect pattern throw redirect({ to: ROUTE_PATTERNS.PROJECT_TASKS, params }); @@ -36,6 +38,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const tasksRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_TASKS, + staticData: { breadcrumbLabel: 'Tasks' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/tasks'), @@ -46,6 +49,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const terminalsRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_TERMINALS, + staticData: { breadcrumbLabel: 'Terminals' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/terminals'), @@ -56,6 +60,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const agentsRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_AGENTS, + staticData: { breadcrumbLabel: 'Agents' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/agents'), @@ -66,6 +71,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const githubRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_GITHUB, + staticData: { breadcrumbLabel: 'GitHub' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/github'), @@ -76,6 +82,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const roadmapRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_ROADMAP, + staticData: { breadcrumbLabel: 'Roadmap' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/roadmap'), @@ -86,6 +93,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const ideationRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_IDEATION, + staticData: { breadcrumbLabel: 'Ideation' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/ideation'), @@ -96,6 +104,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const changelogRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_CHANGELOG, + staticData: { breadcrumbLabel: 'Changelog' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/changelog'), @@ -106,6 +115,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const insightsRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_INSIGHTS, + staticData: { breadcrumbLabel: 'Insights' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/insights'), @@ -116,6 +126,7 @@ export function createProjectRoutes(appLayoutRoute: AnyRoute) { const workflowRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTE_PATTERNS.PROJECT_WORKFLOW, + staticData: { breadcrumbLabel: 'Pipeline' }, pendingComponent: ProjectSkeleton, component: lazyRouteComponent( () => import('@features/workflow-pipeline'), diff --git a/src/renderer/app/routes/settings.routes.ts b/src/renderer/app/routes/settings.routes.ts index c7ed72b..0d91372 100644 --- a/src/renderer/app/routes/settings.routes.ts +++ b/src/renderer/app/routes/settings.routes.ts @@ -16,6 +16,7 @@ export function createSettingsRoutes(appLayoutRoute: AnyRoute) { const settingsRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.SETTINGS, + staticData: { breadcrumbLabel: 'Settings' }, pendingComponent: SettingsSkeleton, component: lazyRouteComponent( () => import('@features/settings'), @@ -26,6 +27,7 @@ export function createSettingsRoutes(appLayoutRoute: AnyRoute) { const themesRoute = createRoute({ getParentRoute: () => appLayoutRoute, path: ROUTES.THEMES, + staticData: { breadcrumbLabel: 'Themes' }, component: lazyRouteComponent( () => import('@features/settings'), 'ThemeEditorPage', diff --git a/src/renderer/features/settings/api/useSettings.ts b/src/renderer/features/settings/api/useSettings.ts index ed31bb0..da829d7 100644 --- a/src/renderer/features/settings/api/useSettings.ts +++ b/src/renderer/features/settings/api/useSettings.ts @@ -4,8 +4,11 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import type { SidebarLayoutId } from '@shared/types/layout'; + import { ipc } from '@renderer/shared/lib/ipc'; -import { useThemeStore } from '@renderer/shared/stores'; +import { useLayoutStore, useThemeStore } from '@renderer/shared/stores'; + export const settingsKeys = { all: ['settings'] as const, @@ -17,6 +20,7 @@ export const settingsKeys = { /** Fetch app settings */ export function useSettings() { const { setMode, setColorTheme, setUiScale, setCustomThemes } = useThemeStore(); + const { setSidebarLayout } = useLayoutStore(); return useQuery({ queryKey: settingsKeys.app(), @@ -27,6 +31,9 @@ export function useSettings() { setMode(settings.theme); setColorTheme(settings.colorTheme); setUiScale(settings.uiScale); + if (settings.sidebarLayout) { + setSidebarLayout(settings.sidebarLayout as SidebarLayoutId); + } if (settings.fontFamily) { document.documentElement.style.setProperty('--app-font-sans', settings.fontFamily); } diff --git a/src/renderer/features/settings/components/LayoutSection.tsx b/src/renderer/features/settings/components/LayoutSection.tsx new file mode 100644 index 0000000..8fb86a8 --- /dev/null +++ b/src/renderer/features/settings/components/LayoutSection.tsx @@ -0,0 +1,239 @@ +/** + * LayoutSection — Sidebar layout selector for Settings > Display + * + * Dropdown to pick from 16 sidebar layouts with an SVG wireframe preview card. + */ + +import type { SidebarLayoutId } from '@shared/types/layout'; +import { SIDEBAR_LAYOUTS } from '@shared/types/layout'; + +import { useLayoutStore } from '@renderer/shared/stores'; + +import { Card, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ui'; + +import { useUpdateSettings } from '../api/useSettings'; + +// ── SVG Preview Data ─────────────────────────────────────── + +interface LayoutPreviewConfig { + sidebarSide: 'left' | 'right' | 'both'; + sidebarWidth: number; + hasSecondSidebar: boolean; + variant: 'default' | 'floating' | 'inset'; + collapsible: 'default' | 'icon' | 'offcanvas'; + sections: number; +} + +const LAYOUT_PREVIEWS: Record = { + 'sidebar-01': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 2 }, + 'sidebar-02': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 2 }, + 'sidebar-03': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 3 }, + 'sidebar-04': { sidebarSide: 'left', sidebarWidth: 55, hasSecondSidebar: false, variant: 'floating', collapsible: 'default', sections: 2 }, + 'sidebar-05': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 3 }, + 'sidebar-06': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 2 }, + 'sidebar-07': { sidebarSide: 'left', sidebarWidth: 20, hasSecondSidebar: false, variant: 'default', collapsible: 'icon', sections: 2 }, + 'sidebar-08': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'inset', collapsible: 'default', sections: 2 }, + 'sidebar-09': { sidebarSide: 'left', sidebarWidth: 80, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 4 }, + 'sidebar-10': { sidebarSide: 'left', sidebarWidth: 20, hasSecondSidebar: false, variant: 'floating', collapsible: 'icon', sections: 2 }, + 'sidebar-11': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 3 }, + 'sidebar-12': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 2 }, + 'sidebar-13': { sidebarSide: 'left', sidebarWidth: 0, hasSecondSidebar: false, variant: 'default', collapsible: 'offcanvas', sections: 2 }, + 'sidebar-14': { sidebarSide: 'right', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 2 }, + 'sidebar-15': { sidebarSide: 'both', sidebarWidth: 50, hasSecondSidebar: true, variant: 'default', collapsible: 'default', sections: 2 }, + 'sidebar-16': { sidebarSide: 'left', sidebarWidth: 60, hasSecondSidebar: false, variant: 'default', collapsible: 'default', sections: 2 }, +}; + +// ── SVG Preview Component ────────────────────────────────── + +function LayoutPreviewSvg({ config }: { config: LayoutPreviewConfig }) { + const w = 200; + const h = 120; + const pad = config.variant === 'floating' ? 4 : 0; + const sidebarW = config.sidebarWidth; + const headerH = 14; + const rightSidebarW = config.hasSecondSidebar ? 35 : 0; + + return ( + + {/* Background */} + + + {/* Content header bar */} + + + {/* Left sidebar */} + {config.sidebarSide !== 'right' && sidebarW > 0 ? ( + + + {/* Section lines */} + {Array.from({ length: config.sections }).map((_, i) => { + const sectionH = (h - pad * 2 - 20) / config.sections; + const y = pad + 20 + i * sectionH; + return ( + + + {Array.from({ length: 3 }).map((_unused, j) => ( + + ))} + + ); + })} + + ) : null} + + {/* Right sidebar (Layout 14 or 15) */} + {config.sidebarSide === 'right' ? ( + + ) : null} + + {/* Second sidebar for dual layout */} + {config.hasSecondSidebar ? ( + + ) : null} + + {/* Content area placeholder lines */} + + {Array.from({ length: 4 }).map((_, i) => { + const contentX = + config.sidebarSide === 'right' ? 10 : sidebarW + 10; + const contentW = + w - sidebarW - rightSidebarW - 20; + return ( + + ); + })} + + + {/* Icon-only indicator for collapsible=icon */} + {config.collapsible === 'icon' ? ( + + {Array.from({ length: 4 }).map((_, i) => ( + + ))} + + ) : null} + + {/* Offcanvas indicator (no visible sidebar) */} + {config.collapsible === 'offcanvas' ? ( + + ) : null} + + ); +} + +// ── Main Section ─────────────────────────────────────────── + +export function LayoutSection() { + const { sidebarLayout, setSidebarLayout } = useLayoutStore(); + const updateSettings = useUpdateSettings(); + + const selectedMeta = SIDEBAR_LAYOUTS.find((l) => l.id === sidebarLayout); + const previewConfig = LAYOUT_PREVIEWS[sidebarLayout]; + + function handleLayoutChange(value: string) { + const layoutId = value as SidebarLayoutId; + setSidebarLayout(layoutId); + updateSettings.mutate({ sidebarLayout: layoutId }); + } + + return ( +
+

+ Sidebar Layout +

+ +
+ {/* Left: Select dropdown */} +
+ + + {selectedMeta ? ( +

{selectedMeta.description}

+ ) : null} +
+ + {/* Right: Preview card */} + + + +
+
+ ); +} diff --git a/src/renderer/features/settings/components/SettingsPage.tsx b/src/renderer/features/settings/components/SettingsPage.tsx index 7eb41e0..b7b28d5 100644 --- a/src/renderer/features/settings/components/SettingsPage.tsx +++ b/src/renderer/features/settings/components/SettingsPage.tsx @@ -26,6 +26,7 @@ import { ColorThemeSection } from './ColorThemeSection'; import { GitHubAuthSettings } from './GitHubAuthSettings'; import { HotkeySettings } from './HotkeySettings'; import { HubSettings } from './HubSettings'; +import { LayoutSection } from './LayoutSection'; import { OAuthProviderSettings } from './OAuthProviderSettings'; import { ProfileSection } from './ProfileSection'; import { StorageManagementSection } from './StorageManagementSection'; @@ -89,6 +90,7 @@ export function SettingsPage() { case 'display': { return ( <> + diff --git a/src/renderer/shared/components/ui/breadcrumb.tsx b/src/renderer/shared/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..58be8e2 --- /dev/null +++ b/src/renderer/shared/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +/** + * Breadcrumb — shadcn breadcrumb components adapted for ADC + * + * Provides composable breadcrumb navigation with separators. + * Used by AppBreadcrumbs in the content header bar. + */ + +import type { ComponentProps } from 'react'; + +import { Slot } from '@radix-ui/react-slot'; +import { ChevronRight, MoreHorizontal } from 'lucide-react'; + +import { cn } from '@renderer/shared/lib/utils'; + + +function Breadcrumb({ ...props }: ComponentProps<'nav'>) { + return