From 4cb61399b118aca0912fd990a624974157805dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 23 Apr 2026 22:02:23 -0400 Subject: [PATCH] fix: make navigation sections reachable --- src/renderer/hooks/useUIController.ts | 55 +++--------- src/renderer/navigation.ts | 119 ++++++++++++++++++++++++++ tests/unit/navigation-model.test.ts | 29 +++++++ 3 files changed, 161 insertions(+), 42 deletions(-) create mode 100644 src/renderer/navigation.ts create mode 100644 tests/unit/navigation-model.test.ts diff --git a/src/renderer/hooks/useUIController.ts b/src/renderer/hooks/useUIController.ts index 391d9a24..d1570e28 100644 --- a/src/renderer/hooks/useUIController.ts +++ b/src/renderer/hooks/useUIController.ts @@ -5,51 +5,26 @@ import type { AgentRecord, GoalRecord, ProjectRecord, RunRecord, SectionId, Task import { createTranslator } from "../i18n"; import { useAppStore } from "../state/store"; import { useFocusTrap, useReducedMotion } from "../components/ui"; +import { SquaresFour } from "../components/ui/icons"; import { - BookOpen, - Gear, - Tray, - SquaresFour, - UsersThree, - ListChecks, - ChatCircle, - PuzzlePiece, - FileText, - ShieldCheck, - Megaphone, - FolderOpen, -} from "../components/ui/icons"; + SETTINGS_NAV_ITEM_DEF, + SIDEBAR_NAV_GROUP_DEFS, + type NavItemDef, +} from "../navigation"; import type { AppLocale } from "@shared/locale"; -type NavItemDef = { id: SectionId; labelKey: string; icon: typeof SquaresFour }; export type NavItem = { id: SectionId; label: string; icon: typeof SquaresFour }; export type NavGroup = { items: NavItem[] }; export type PaletteAction = { key: string; section: SectionId; entityId?: string }; -const sidebarCoreDefs: NavItemDef[] = [ - { id: "overview", labelKey: "nav.overview", icon: SquaresFour }, - { id: "inbox", labelKey: "nav.inbox", icon: Tray }, - { id: "communication", labelKey: "nav.communication", icon: ChatCircle }, -]; - -const sidebarCollaborateDefs: NavItemDef[] = [ - { id: "tasks", labelKey: "nav.work", icon: ListChecks }, - { id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck }, - { id: "agents", labelKey: "nav.agents", icon: UsersThree }, - { id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen }, - { id: "social", labelKey: "nav.social", icon: Megaphone }, - { id: "documents", labelKey: "nav.documents", icon: FileText }, -]; - -const paletteOnlyNavDefs: NavItemDef[] = [ - { id: "skills", labelKey: "nav.skills", icon: PuzzlePiece }, - { id: "knowledge", labelKey: "nav.knowledge", icon: BookOpen }, -]; - const NO_MOTION = { duration: 0 } as const; const PAGE_TRANSITION = { duration: 0.32, ease: [0.16, 1, 0.3, 1] as const } as const; const PANEL_TRANSITION = { duration: 0.28, ease: [0.16, 1, 0.3, 1] as const } as const; +function toNavItem(item: NavItemDef, t: (key: string) => string): NavItem { + return { id: item.id, label: t(item.labelKey), icon: item.icon }; +} + export function useUIController() { const reducedMotion = useReducedMotion(); const pageTransition = reducedMotion ? NO_MOTION : PAGE_TRANSITION; @@ -59,10 +34,9 @@ export function useUIController() { const t = useMemo(() => createTranslator(locale), [locale]); const sidebarGroups: NavGroup[] = useMemo( - () => [ - { items: sidebarCoreDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })) }, - { items: sidebarCollaborateDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })) }, - ], + () => SIDEBAR_NAV_GROUP_DEFS.map((group) => ({ + items: group.items.map((item) => toNavItem(item, t)), + })), [t], ); const sidebarNav: NavItem[] = useMemo( @@ -70,10 +44,7 @@ export function useUIController() { [sidebarGroups], ); const navItems: NavItem[] = useMemo( - () => - sidebarNav - .concat(paletteOnlyNavDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon }))) - .concat({ id: "settings", label: t("nav.settings"), icon: Gear }), + () => sidebarNav.concat(toNavItem(SETTINGS_NAV_ITEM_DEF, t)), [sidebarNav, t], ); diff --git a/src/renderer/navigation.ts b/src/renderer/navigation.ts new file mode 100644 index 00000000..b8db7806 --- /dev/null +++ b/src/renderer/navigation.ts @@ -0,0 +1,119 @@ +import type { SectionId } from "@shared/types"; +import { + BookOpen, + Briefcase, + CalendarBlank, + ChartLine, + ChatCircle, + ClockCounterClockwise, + CurrencyCircleDollar, + FileText, + FolderOpen, + Gear, + Kanban, + ListChecks, + Megaphone, + MonitorPlay, + Plugs, + PuzzlePiece, + ShieldCheck, + SquaresFour, + Target, + Timer, + Tray, + UserPlus, + UsersThree, +} from "./components/ui/icons"; + +export type NavItemDef = { id: SectionId; labelKey: string; icon: typeof SquaresFour }; +export type NavGroupDef = { id: string; items: NavItemDef[] }; +type SectionNavItemMap = { [K in SectionId]: NavItemDef & { id: K } }; + +export const SECTION_NAV_ITEM_DEFS = { + overview: { id: "overview", labelKey: "nav.overview", icon: SquaresFour }, + inbox: { id: "inbox", labelKey: "nav.inbox", icon: Tray }, + standup: { id: "standup", labelKey: "nav.standup", icon: Timer }, + performance: { id: "performance", labelKey: "nav.performance", icon: ChartLine }, + runs: { id: "runs", labelKey: "nav.runs", icon: MonitorPlay }, + connectors: { id: "connectors", labelKey: "nav.connectors", icon: Plugs }, + agents: { id: "agents", labelKey: "nav.agents", icon: UsersThree }, + hiring: { id: "hiring", labelKey: "nav.hiring", icon: UserPlus }, + goals: { id: "goals", labelKey: "nav.goals", icon: Target }, + projects: { id: "projects", labelKey: "nav.projects", icon: Kanban }, + tasks: { id: "tasks", labelKey: "nav.work", icon: ListChecks }, + approvals: { id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck }, + costs: { id: "costs", labelKey: "nav.costs", icon: CurrencyCircleDollar }, + activity: { id: "activity", labelKey: "nav.activity", icon: ClockCounterClockwise }, + workspaces: { id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen }, + social: { id: "social", labelKey: "nav.social", icon: Megaphone }, + skills: { id: "skills", labelKey: "nav.skills", icon: PuzzlePiece }, + automation: { id: "automation", labelKey: "nav.automation", icon: Briefcase }, + communication: { id: "communication", labelKey: "nav.communication", icon: ChatCircle }, + documents: { id: "documents", labelKey: "nav.documents", icon: FileText }, + knowledge: { id: "knowledge", labelKey: "nav.knowledge", icon: BookOpen }, + meetings: { id: "meetings", labelKey: "nav.meetings", icon: CalendarBlank }, + sprints: { id: "sprints", labelKey: "nav.sprints", icon: Kanban }, + settings: { id: "settings", labelKey: "nav.settings", icon: Gear }, +} as const satisfies SectionNavItemMap; + +export const SECTION_NAV_IDS = Object.keys(SECTION_NAV_ITEM_DEFS) as SectionId[]; + +export const SIDEBAR_NAV_GROUP_DEFS = [ + { + id: "company", + items: [ + SECTION_NAV_ITEM_DEFS.overview, + SECTION_NAV_ITEM_DEFS.inbox, + SECTION_NAV_ITEM_DEFS.standup, + SECTION_NAV_ITEM_DEFS.performance, + ], + }, + { + id: "organization", + items: [ + SECTION_NAV_ITEM_DEFS.agents, + SECTION_NAV_ITEM_DEFS.hiring, + SECTION_NAV_ITEM_DEFS.goals, + SECTION_NAV_ITEM_DEFS.projects, + ], + }, + { + id: "execution", + items: [ + SECTION_NAV_ITEM_DEFS.tasks, + SECTION_NAV_ITEM_DEFS.approvals, + SECTION_NAV_ITEM_DEFS.runs, + SECTION_NAV_ITEM_DEFS.costs, + SECTION_NAV_ITEM_DEFS.activity, + ], + }, + { + id: "operations", + items: [ + SECTION_NAV_ITEM_DEFS.workspaces, + SECTION_NAV_ITEM_DEFS.documents, + SECTION_NAV_ITEM_DEFS.knowledge, + SECTION_NAV_ITEM_DEFS.automation, + SECTION_NAV_ITEM_DEFS.connectors, + ], + }, + { + id: "collaboration", + items: [ + SECTION_NAV_ITEM_DEFS.communication, + SECTION_NAV_ITEM_DEFS.meetings, + SECTION_NAV_ITEM_DEFS.sprints, + SECTION_NAV_ITEM_DEFS.social, + SECTION_NAV_ITEM_DEFS.skills, + ], + }, +] as const satisfies readonly NavGroupDef[]; + +export const SETTINGS_NAV_ITEM_DEF = SECTION_NAV_ITEM_DEFS.settings; + +export const DASHBOARD_NAV_TARGETS = [ + "approvals", + "runs", + "communication", + "costs", +] as const satisfies readonly SectionId[]; diff --git a/tests/unit/navigation-model.test.ts b/tests/unit/navigation-model.test.ts new file mode 100644 index 00000000..34587a81 --- /dev/null +++ b/tests/unit/navigation-model.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import type { SectionId } from "@shared/types"; +import { + DASHBOARD_NAV_TARGETS, + SECTION_NAV_IDS, + SETTINGS_NAV_ITEM_DEF, + SIDEBAR_NAV_GROUP_DEFS, +} from "../../src/renderer/navigation"; + +function sidebarSectionIds(): SectionId[] { + return SIDEBAR_NAV_GROUP_DEFS.flatMap((group) => group.items.map((item) => item.id)); +} + +describe("renderer navigation model", () => { + it("gives every section a stable visible entry", () => { + const visibleIds = [...sidebarSectionIds(), SETTINGS_NAV_ITEM_DEF.id]; + + expect(new Set(visibleIds).size).toBe(SECTION_NAV_IDS.length); + expect(visibleIds).toEqual(expect.arrayContaining(SECTION_NAV_IDS)); + }); + + it("keeps Dashboard targets reachable from the stable navigation model", () => { + const visibleIds = new Set([...sidebarSectionIds(), SETTINGS_NAV_ITEM_DEF.id]); + + for (const sectionId of DASHBOARD_NAV_TARGETS) { + expect(visibleIds.has(sectionId)).toBe(true); + } + }); +});