fix: make navigation sections reachable
This commit is contained in:
1 parent
88c905c381
commit
4cb61399b1
3 files changed
+161
-42
No files matched your search
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
@@ -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[];
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user