fix: make navigation sections reachable

This commit is contained in:
ZacharyZhang-NY committed 2026-04-23 22:02:23 -04:00
1 parent 88c905c381
commit 4cb61399b1
3 files changed
+161 -42

No files matched your search

+13 -42
View File
@@ -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],
);
+119
View File
@@ -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[];
+29
View File
@@ -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);
}
});
});