refactor(nexus): dry shared destinations into frame module
IconRail (desktop) and MobileTabBar (mobile) each carried hand- synced copies of the 4-destination nav config — labels, href builders, isActive regex predicates, and Lucide icons. Phase 15 explicitly deferred the DRY to Phase 16 cleanup. Phase 16a extracts the shared config into `frame/destinations.ts` and replaces both consumers' inline definitions with a single import. The Projects umbrella regex list (issues, agents, routines, goals, approvals, costs, activity, inbox, execution-workspaces) lives in the shared module so both surfaces stay in lockstep. Net line change for the two consumers: - IconRail.tsx: -67 lines (inline config) - MobileTabBar.tsx: -63 lines (inline config) - +82 lines (new destinations.ts with shared types + regex list) Test sweep: src/components/frame/ — 53 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5957290073
commit
a3102351d8
3 changed files with 89 additions and 128 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import { MessageCircle, Sparkles, FolderKanban, Settings } from "lucide-react";
|
||||
import { Link, useLocation } from "@/lib/router";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCompany } from "../../context/CompanyContext";
|
||||
import { useGateIndicator } from "../../hooks/useGateIndicator";
|
||||
import { DESTINATIONS, type Destination } from "./destinations";
|
||||
|
||||
interface IconRailProps {
|
||||
/**
|
||||
|
|
@ -13,73 +13,6 @@ interface IconRailProps {
|
|||
companyPrefix: string | null;
|
||||
}
|
||||
|
||||
type DestinationKey = "assistant" | "studio" | "projects" | "settings";
|
||||
|
||||
interface Destination {
|
||||
key: DestinationKey;
|
||||
label: string;
|
||||
/**
|
||||
* Given a company prefix (possibly null), return the destination URL.
|
||||
*/
|
||||
href: (prefix: string | null) => string;
|
||||
/**
|
||||
* Return true if the destination should be marked active for the given pathname.
|
||||
*/
|
||||
isActive: (pathname: string) => boolean;
|
||||
icon: typeof MessageCircle;
|
||||
}
|
||||
|
||||
const DESTINATIONS: Destination[] = [
|
||||
{
|
||||
key: "assistant",
|
||||
label: "Assistant",
|
||||
href: (prefix) => (prefix ? `/${prefix}/assistant` : "/"),
|
||||
isActive: (pathname) => /\/assistant(\/|$)/.test(pathname),
|
||||
icon: MessageCircle,
|
||||
},
|
||||
{
|
||||
key: "studio",
|
||||
label: "Studio",
|
||||
// Studio is a future unified route (Phase 10). For Phase 8 we route it
|
||||
// to the existing content-studio page. The Studio icon also highlights
|
||||
// when the user is on /convert because Convert folds into Studio in Phase 10.
|
||||
href: (prefix) => (prefix ? `/${prefix}/content-studio` : "/"),
|
||||
isActive: (pathname) =>
|
||||
/\/content-studio(\/|$)/.test(pathname) ||
|
||||
/\/studio(\/|$)/.test(pathname) ||
|
||||
/\/convert(\/|$)/.test(pathname),
|
||||
icon: Sparkles,
|
||||
},
|
||||
{
|
||||
key: "projects",
|
||||
label: "Projects",
|
||||
href: (prefix) => (prefix ? `/${prefix}/projects` : "/"),
|
||||
// The Projects icon acts as the umbrella for every route that Phase 11
|
||||
// will eventually demote to a per-project tab.
|
||||
isActive: (pathname) =>
|
||||
/\/projects(\/|$)/.test(pathname) ||
|
||||
/\/issues(\/|$)/.test(pathname) ||
|
||||
/\/agents(\/|$)/.test(pathname) ||
|
||||
/\/routines(\/|$)/.test(pathname) ||
|
||||
/\/goals(\/|$)/.test(pathname) ||
|
||||
/\/approvals(\/|$)/.test(pathname) ||
|
||||
/\/costs(\/|$)/.test(pathname) ||
|
||||
/\/activity(\/|$)/.test(pathname) ||
|
||||
/\/inbox(\/|$)/.test(pathname) ||
|
||||
/\/execution-workspaces(\/|$)/.test(pathname),
|
||||
icon: FolderKanban,
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
label: "Settings",
|
||||
// Instance settings is a global (non-company-prefixed) route in Paperclip.
|
||||
// Phase 13 may change this; for Phase 8 we preserve the current URL.
|
||||
href: () => "/instance/settings/general",
|
||||
isActive: (pathname) => pathname.startsWith("/instance/settings"),
|
||||
icon: Settings,
|
||||
},
|
||||
];
|
||||
|
||||
export function IconRail({ companyPrefix }: IconRailProps) {
|
||||
const { pathname } = useLocation();
|
||||
// Phase 11 integration: pending gates → volt dot on the Assistant destination.
|
||||
|
|
|
|||
|
|
@ -6,16 +6,14 @@
|
|||
// Projects, Settings. Silver default, volt active, with a 2px volt bar
|
||||
// above the active icon and safe-area aware bottom padding.
|
||||
//
|
||||
// The destination-matching regexes are intentionally duplicated from
|
||||
// IconRail rather than shared via a module: Phase 8 owns IconRail, and
|
||||
// touching it inside Phase 15 is out of scope. Phase 16 cleanup can DRY
|
||||
// the helpers into a shared `frame/destinations.ts`.
|
||||
// Phase 16a DRY'd the destination config into `./destinations.ts`; both
|
||||
// this bar and the desktop IconRail now import the same list.
|
||||
//
|
||||
// Spec: docs/specs/2026-04-11-nexus-layout-overhaul.md §9 (Mobile).
|
||||
import { MessageCircle, Sparkles, FolderKanban, Settings } from "lucide-react";
|
||||
import { Link, useLocation } from "@/lib/router";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCompany } from "../../context/CompanyContext";
|
||||
import { DESTINATIONS, type Destination } from "./destinations";
|
||||
|
||||
interface MobileTabBarProps {
|
||||
/**
|
||||
|
|
@ -26,61 +24,6 @@ interface MobileTabBarProps {
|
|||
visible?: boolean;
|
||||
}
|
||||
|
||||
type DestinationKey = "assistant" | "studio" | "projects" | "settings";
|
||||
|
||||
interface Destination {
|
||||
key: DestinationKey;
|
||||
label: string;
|
||||
href: (prefix: string | null) => string;
|
||||
isActive: (pathname: string) => boolean;
|
||||
icon: typeof MessageCircle;
|
||||
}
|
||||
|
||||
// Regexes duplicated from IconRail intentionally — see file header.
|
||||
const DESTINATIONS: Destination[] = [
|
||||
{
|
||||
key: "assistant",
|
||||
label: "Assistant",
|
||||
href: (prefix) => (prefix ? `/${prefix}/assistant` : "/"),
|
||||
isActive: (pathname) => /\/assistant(\/|$)/.test(pathname),
|
||||
icon: MessageCircle,
|
||||
},
|
||||
{
|
||||
key: "studio",
|
||||
label: "Studio",
|
||||
href: (prefix) => (prefix ? `/${prefix}/content-studio` : "/"),
|
||||
isActive: (pathname) =>
|
||||
/\/content-studio(\/|$)/.test(pathname) ||
|
||||
/\/studio(\/|$)/.test(pathname) ||
|
||||
/\/convert(\/|$)/.test(pathname),
|
||||
icon: Sparkles,
|
||||
},
|
||||
{
|
||||
key: "projects",
|
||||
label: "Projects",
|
||||
href: (prefix) => (prefix ? `/${prefix}/projects` : "/"),
|
||||
isActive: (pathname) =>
|
||||
/\/projects(\/|$)/.test(pathname) ||
|
||||
/\/issues(\/|$)/.test(pathname) ||
|
||||
/\/agents(\/|$)/.test(pathname) ||
|
||||
/\/routines(\/|$)/.test(pathname) ||
|
||||
/\/goals(\/|$)/.test(pathname) ||
|
||||
/\/approvals(\/|$)/.test(pathname) ||
|
||||
/\/costs(\/|$)/.test(pathname) ||
|
||||
/\/activity(\/|$)/.test(pathname) ||
|
||||
/\/inbox(\/|$)/.test(pathname) ||
|
||||
/\/execution-workspaces(\/|$)/.test(pathname),
|
||||
icon: FolderKanban,
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
label: "Settings",
|
||||
href: () => "/instance/settings/general",
|
||||
isActive: (pathname) => pathname.startsWith("/instance/settings"),
|
||||
icon: Settings,
|
||||
},
|
||||
];
|
||||
|
||||
export function MobileTabBar({ visible = true }: MobileTabBarProps) {
|
||||
const { pathname } = useLocation();
|
||||
// Company-prefix scoping mirrors IconRail: the first three destinations
|
||||
|
|
|
|||
85
ui/src/components/frame/destinations.ts
Normal file
85
ui/src/components/frame/destinations.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// [nexus] Phase 16a — shared destination config for the primary nav.
|
||||
//
|
||||
// Previously IconRail (desktop) and MobileTabBar (mobile) each carried
|
||||
// hand-synced copies of the 4-destination nav config (labels, href
|
||||
// builders, isActive regex predicates, Lucide icons). Phase 16a DRYs
|
||||
// them into this module so the two surfaces always agree.
|
||||
//
|
||||
// The Projects destination acts as the umbrella for every route that
|
||||
// Phase 11 demoted into a per-project tab (issues, agents, routines,
|
||||
// etc.); the regex list lives here so both rail and tab bar stay in
|
||||
// lockstep.
|
||||
//
|
||||
// Spec: docs/specs/2026-04-11-nexus-layout-overhaul.md §4 (IconRail),
|
||||
// §9 (MobileTabBar).
|
||||
import { MessageCircle, Sparkles, FolderKanban, Settings, type LucideIcon } from "lucide-react";
|
||||
|
||||
export type DestinationKey = "assistant" | "studio" | "projects" | "settings";
|
||||
|
||||
export interface Destination {
|
||||
key: DestinationKey;
|
||||
label: string;
|
||||
/**
|
||||
* Given a company prefix (possibly null), return the destination URL.
|
||||
*/
|
||||
href: (companyPrefix: string | null) => string;
|
||||
/**
|
||||
* Return true if the destination should be marked active for the
|
||||
* given pathname.
|
||||
*/
|
||||
isActive: (pathname: string) => boolean;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
export const DESTINATIONS: Destination[] = [
|
||||
{
|
||||
key: "assistant",
|
||||
label: "Assistant",
|
||||
href: (prefix) => (prefix ? `/${prefix}/assistant` : "/"),
|
||||
isActive: (pathname) => /\/assistant(\/|$)/.test(pathname),
|
||||
icon: MessageCircle,
|
||||
},
|
||||
{
|
||||
key: "studio",
|
||||
label: "Studio",
|
||||
// Studio is a future unified route (Phase 10). For Phase 8 we route it
|
||||
// to the existing content-studio page. The Studio icon also highlights
|
||||
// when the user is on /convert because Convert folds into Studio in
|
||||
// Phase 10.
|
||||
href: (prefix) => (prefix ? `/${prefix}/content-studio` : "/"),
|
||||
isActive: (pathname) =>
|
||||
/\/content-studio(\/|$)/.test(pathname) ||
|
||||
/\/studio(\/|$)/.test(pathname) ||
|
||||
/\/convert(\/|$)/.test(pathname),
|
||||
icon: Sparkles,
|
||||
},
|
||||
{
|
||||
key: "projects",
|
||||
label: "Projects",
|
||||
href: (prefix) => (prefix ? `/${prefix}/projects` : "/"),
|
||||
// The Projects icon acts as the umbrella for every route that Phase 11
|
||||
// will eventually demote to a per-project tab.
|
||||
isActive: (pathname) =>
|
||||
/\/projects(\/|$)/.test(pathname) ||
|
||||
/\/issues(\/|$)/.test(pathname) ||
|
||||
/\/agents(\/|$)/.test(pathname) ||
|
||||
/\/routines(\/|$)/.test(pathname) ||
|
||||
/\/goals(\/|$)/.test(pathname) ||
|
||||
/\/approvals(\/|$)/.test(pathname) ||
|
||||
/\/costs(\/|$)/.test(pathname) ||
|
||||
/\/activity(\/|$)/.test(pathname) ||
|
||||
/\/inbox(\/|$)/.test(pathname) ||
|
||||
/\/execution-workspaces(\/|$)/.test(pathname),
|
||||
icon: FolderKanban,
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
label: "Settings",
|
||||
// Instance settings is a global (non-company-prefixed) route in
|
||||
// Paperclip. Phase 13 consolidated the settings into this single
|
||||
// surface; the rail and tab bar both point to `/general`.
|
||||
href: () => "/instance/settings/general",
|
||||
isActive: (pathname) => pathname.startsWith("/instance/settings"),
|
||||
icon: Settings,
|
||||
},
|
||||
];
|
||||
Loading…
Add table
Reference in a new issue