Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/components/AppShell.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/components/AppShell.tsx |
| Original SHA-256 | 33514212031cf906fc54c09a6b63b70728d9bae73e6e83fe5a4e856ac2ab746a |
| Git blob SHA | 900e80b75883377f91c5cda77cb63ba09129ed8a |
| Reading-copy SHA-256 | 33514212031cf906fc54c09a6b63b70728d9bae73e6e83fe5a4e856ac2ab746a |
Recovered source transcript
Download complete source text ↓377 lines · 16.1 KB
import { useEffect, useState, type ComponentType, type CSSProperties } from 'react'
import { NavLink, Outlet, useLocation } from 'react-router-dom'
import {
BrainCircuit,
Briefcase,
Users2,
FileDown,
TrendingUp,
CheckSquare,
Building2,
ClipboardList,
Crosshair,
Gauge,
Globe,
Inbox,
Layers,
LogOut,
Menu,
X,
Radar,
Scale,
Search,
Settings as SettingsIcon,
ShieldCheck,
Users,
} from 'lucide-react'
import { useAuth } from '../lib/useAuth'
import { hasTool } from '../lib/can'
import { CAP, can } from '../lib/can'
import { orgLogoURL } from '../lib/org'
import { useOrgBranding } from '../lib/orgQueries'
type Icon = ComponentType<{ size?: number; className?: string; style?: CSSProperties }>
type NavItem = { to: string; label: string; icon: Icon }
type Tool = {
key: string
// tool is the server side entitlement name this group belongs to. The navigation is filtered by it, so
// a user without the grant never sees the group. The API refuses independently.
tool: string
name: string
tagline: string
accent: string
basePath: string
icon: Icon
match: (path: string) => boolean
nav: NavItem[]
}
// The three tools under the missionMATCH umbrella. The switcher routes between them; each owns its
// accent and its own nav. missionMATCH (gold) is the home tool and the default after login.
const TOOLS: Tool[] = [
{
key: 'mission',
tool: 'missionmatch',
name: 'missionMATCH',
tagline: 'Command Center',
accent: 'var(--color-gold)',
basePath: '/command',
icon: Radar,
match: (p) => p === '/' || p.startsWith('/command') || p.startsWith('/compliance'),
nav: [
{ to: '/command', label: 'Command Center', icon: Radar },
{ to: '/command/candidates', label: 'Candidates', icon: Users },
{ to: '/command/ask', label: 'Ask the Brain', icon: BrainCircuit },
{ to: '/compliance', label: 'Compliance', icon: Scale },
],
},
{
key: 'screen',
tool: 'screenmatch',
name: 'screenMATCH',
tagline: 'Qualification',
accent: 'var(--color-screen)',
basePath: '/screen',
icon: Crosshair,
match: (p) => p.startsWith('/screen') || p.startsWith('/requirements') || p.startsWith('/evaluate'),
nav: [
{ to: '/screen', label: 'Screen Candidate', icon: Crosshair },
{ to: '/requirements', label: 'Opportunities', icon: ClipboardList },
],
},
{
key: 'slate',
tool: 'slatematch',
name: 'slateMATCH',
tagline: 'Proposal Slates',
accent: 'var(--color-slate-accent)',
basePath: '/slate',
icon: Layers,
match: (p) => p.startsWith('/slate'),
nav: [
{ to: '/slate', label: 'Candidate Slate', icon: Layers },
{ to: '/slate/capture', label: 'Capture', icon: Crosshair },
{ to: '/slate/review', label: 'Capture review', icon: Crosshair },
],
},
{
key: 'recruit',
tool: 'recruitmatch',
name: 'recruitMATCH',
tagline: 'Recruiting Operations',
accent: 'var(--color-recruit)',
basePath: '/recruit',
icon: ClipboardList,
match: (p) => p.startsWith('/recruit'),
nav: [
{ to: '/recruit', label: 'Dashboard', icon: ClipboardList },
{ to: '/recruit/clients', label: 'Clients', icon: Building2 },
{ to: '/recruit/requisitions', label: 'Job orders', icon: Briefcase },
{ to: '/recruit/careers', label: 'Careers and applicants', icon: Globe },
{ to: '/recruit/tasks', label: 'My work', icon: CheckSquare },
{ to: '/recruit/manager', label: 'My desks', icon: Gauge },
{ to: '/recruit/analytics', label: 'Analytics', icon: TrendingUp },
{ to: '/recruit/reports', label: 'Reports', icon: FileDown },
],
},
]
function activeTool(path: string): Tool {
return TOOLS.find((t) => t.match(path)) ?? TOOLS[0]
}
// Shared and legacy routes belong to no tool; give them their own header title so it never reads the
// fallback tool's first nav label (for example the Settings page must not show "Command Center").
const SHARED_TITLES: Record<string, string> = {
'/settings': 'Settings',
'/admin': 'Accounts and access',
'/people': 'Accounts and access',
'/mailbox': 'Your mailbox',
'/bench': 'Find people',
'/bench/duplicates': 'Possible duplicates',
}
function isSharedPath(path: string): boolean {
return path in SHARED_TITLES
}
function headerTitle(tool: Tool, path: string): string {
if (SHARED_TITLES[path]) return SHARED_TITLES[path]
const item = tool.nav.find((n) => (path === n.to ? true : path.startsWith(n.to) && n.to !== tool.basePath))
return item?.label ?? tool.nav[0]?.label ?? tool.name
}
export function AppShell() {
const { pathname } = useLocation()
// THE SIDEBAR IS A DRAWER BELOW THE LARGE BREAKPOINT.
//
// The shell was a fixed two column grid, 248 pixels of navigation and then the content. On a 390 pixel
// phone that is sixty three percent of the screen spent on the menu, and the content ran off the right
// edge: a visual sweep found HORIZONTAL OVERFLOW on all twenty routes at that width, and a screenshot shows
// numbers and headings clipped mid word. The product was unusable on a phone, which is exactly where a
// recruiter checks who is rolling off between meetings.
const [navOpen, setNavOpen] = useState(false)
// Navigating closes it. Without this, tapping a link on a phone leaves the drawer covering the screen it
// just opened.
useEffect(() => setNavOpen(false), [pathname])
const { principal, logout } = useAuth()
const tool = activeTool(pathname)
// On a shared or legacy route no tool is truly active, so no tool chip lights and the header stays
// neutral rather than borrowing the fallback tool's identity.
const shared = isSharedPath(pathname)
// Only the tools this user was granted. Filtered from the same list the switcher and the header read,
// so an unentitled tool cannot appear in one and not the other.
const visibleTools = TOOLS.filter((t) => hasTool(principal, t.tool))
const branding = useOrgBranding()
const orgLogo = orgLogoURL(branding.data)
const orgName = branding.data?.display_name || branding.data?.name || ""
return (
<div className="relative h-full lg:grid lg:grid-cols-[248px_1fr]">
{/* The scrim. It only exists while the drawer is open and only below the breakpoint, and tapping it
closes the drawer, which is the gesture everybody already expects. */}
{navOpen && (
<button
type="button"
aria-label="Close the menu"
onClick={() => setNavOpen(false)}
className="fixed inset-0 z-30 bg-black/60 backdrop-blur-sm lg:hidden"
/>
)}
<aside
className={`fixed inset-y-0 left-0 z-40 flex w-[248px] flex-col border-r border-line/70 bg-navy-950/95 backdrop-blur transition-transform lg:static lg:z-auto lg:w-auto lg:translate-x-0 lg:bg-navy-950/70 ${
navOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center gap-2.5 px-5 py-5">
{orgLogo ? (
// The client's own mark. object-contain so a wide or tall logo is never distorted, and the
// alt text names the organization so the shell stays readable without images.
<span className="grid size-9 place-items-center overflow-hidden rounded-lg bg-navy-900">
<img src={orgLogo} alt={orgName} className="size-9 object-contain" />
</span>
) : (
<span className="grid size-9 place-items-center rounded-lg bg-gradient-to-br from-blue to-navy-700 text-gold">
<ShieldCheck size={20} />
</span>
)}
<div className="leading-tight">
<div className="text-[15px] font-extrabold tracking-tight">
mission<span className="text-gold">MATCH</span>
<span className="align-super text-[9px]">™</span>
</div>
<div className="truncate text-[10px] uppercase tracking-[0.16em] text-faint" title={orgName}>
{orgName || 'Enterprise Mission Intelligence™'}
</div>
</div>
</div>
{/* Tool switcher: one chip per ENTITLED tool, the active tool wears its accent. A tool the user was
not granted is absent rather than disabled, because a disabled chip advertises a product they
cannot use and invites a support request. The API refuses those routes independently. */}
<div className="mx-3 mb-2 space-y-1 rounded-xl border border-line/60 bg-navy-900/50 p-1.5">
{visibleTools.map((t) => {
const isActive = t.key === tool.key && !shared
return (
<NavLink
key={t.key}
to={t.basePath}
className="flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-semibold transition-colors"
style={{
background: isActive ? 'color-mix(in srgb, ' + t.accent + ' 16%, transparent)' : 'transparent',
color: isActive ? 'var(--color-ink)' : 'var(--color-muted)',
boxShadow: isActive ? 'inset 2px 0 0 0 ' + t.accent : 'none',
}}
>
<t.icon size={16} style={{ color: t.accent }} />
{t.name}
</NavLink>
)
})}
</div>
<nav className="mt-1 flex-1 space-y-0.5 px-3">
<div className="px-3 pb-1 pt-2 text-[10px] font-bold uppercase tracking-[0.16em] text-faint">
{tool.tagline}
</div>
{tool.nav.map(({ to, label, icon: ItemIcon }) => (
<NavLink
key={to}
to={to}
end={to === tool.basePath}
className={({ isActive }) =>
`flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive ? 'text-ink' : 'text-muted hover:bg-navy-800/60 hover:text-ink'
}`
}
style={({ isActive }) =>
isActive
? { background: 'color-mix(in srgb, ' + tool.accent + ' 15%, transparent)', boxShadow: 'inset 2px 0 0 0 ' + tool.accent }
: undefined
}
>
<ItemIcon size={17} />
{label}
</NavLink>
))}
</nav>
<NavLink
to="/settings"
className={({ isActive }) =>
`mx-3 mb-2 flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive ? 'bg-navy-800/60 text-ink' : 'text-muted hover:bg-navy-800/60 hover:text-ink'
}`
}
>
<SettingsIcon size={17} /> Settings
</NavLink>
{/* The bench sits above settings and below the tool navigation, because it belongs to no tool and is
reached many times a day. Everybody who can read a candidate gets it: searching the records you
are already allowed to see is not a privileged act, and the server narrows the answer by record
scope regardless of who asks. */}
{can(principal, CAP.candidateRead) && (
<NavLink
to="/bench"
end
className={({ isActive }) =>
`mx-3 mb-1 flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive ? 'bg-navy-800/60 text-ink' : 'text-muted hover:bg-navy-800/60 hover:text-ink'
}`
}
>
<Search size={17} /> Find people
</NavLink>
)}
{/* The personal mailbox, shown to anyone who works candidate records. It sits beside settings rather
than inside a tool because it belongs to the person, not to a product. A viewer never sees it:
there is nothing to log correspondence against on an account that changes no records. */}
{can(principal, CAP.candidateWrite) && (
<NavLink
to="/mailbox"
className={({ isActive }) =>
`mx-3 mb-2 flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive ? 'bg-navy-800/60 text-ink' : 'text-muted hover:bg-navy-800/60 hover:text-ink'
}`
}
>
<Inbox size={17} /> Your mailbox
</NavLink>
)}
{/* The access screen appears only for someone who can actually manage accounts. Hiding it is a
courtesy; the API refuses every action independently regardless of what the nav shows. */}
{can(principal, CAP.userInvite) && (
<NavLink
to="/admin"
className={({ isActive }) =>
`mx-3 mb-2 flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive ? 'bg-navy-800/60 text-ink' : 'text-muted hover:bg-navy-800/60 hover:text-ink'
}`
}
>
<Users2 size={17} /> Accounts and access
</NavLink>
)}
<p className="mt-auto px-5 pb-4 pt-3 text-[10px] leading-relaxed text-faint">
Powered by the Adaptive Intelligence Optimization Engine™
</p>
</aside>
{/* min-w-0 is load bearing on a grid child: without it the column takes its content's intrinsic width
and a wide table pushes the whole layout sideways instead of scrolling inside its own box. */}
<div className="flex h-full min-w-0 flex-col">
<header className="flex items-center justify-between gap-2 border-b border-line/70 bg-navy-950/40 px-4 py-3.5 backdrop-blur sm:px-6">
<div className="flex min-w-0 items-center gap-3">
<button
type="button"
aria-label={navOpen ? 'Close the menu' : 'Open the menu'}
aria-expanded={navOpen}
onClick={() => setNavOpen((v) => !v)}
className="grid size-8 shrink-0 place-items-center rounded-md border border-line/70 text-muted hover:text-ink lg:hidden"
>
{navOpen ? <X size={16} /> : <Menu size={16} />}
</button>
<span className="hidden h-7 w-1.5 shrink-0 rounded-full sm:block" style={{ background: shared ? 'var(--color-line)' : tool.accent }} />
<div className="min-w-0">
<h1 className="truncate text-base font-bold tracking-tight sm:text-lg">{headerTitle(tool, pathname)}</h1>
<p className="truncate text-xs text-faint">{shared ? 'missionMATCH' : `${tool.name} · ${tool.tagline}`}</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-3">
{/* The compliance badge is the first thing to go on a narrow screen: it is reassurance, not a
control, and the sign out button behind it is not. */}
<span className="hidden rounded-full border border-line/70 px-3 py-1 text-[11px] font-semibold text-muted xl:inline">
FIPS 140-3 • RLS isolated
</span>
{/* The signed in identity, linked to the screen that edits it. The name is here because a
rename has to be VISIBLE to be believed: the header is the one place a person looks at on
every screen, so it is where the profile edit proves it propagated. */}
{principal && (
<NavLink
to="/settings"
title="Your profile"
className="hidden items-center gap-2 rounded-full border border-line/70 py-1 pl-3 pr-1 text-[11px] font-semibold text-muted hover:text-ink sm:flex"
>
<span className="max-w-[9rem] truncate lg:max-w-[14rem]">{principal.full_name || 'Your profile'}</span>
<span className="rounded-full bg-blue/20 px-2.5 py-0.5 font-semibold uppercase tracking-wide text-blue-soft">
{principal.role}
</span>
</NavLink>
)}
<button
onClick={() => void logout()}
title="Sign out"
className="grid size-8 place-items-center rounded-md border border-line/70 text-muted hover:border-risk/60 hover:text-risk"
>
<LogOut size={15} />
</button>
</div>
</header>
<main className="min-h-0 min-w-0 flex-1 overflow-y-auto p-4 sm:p-6">
<Outlet />
</main>
</div>
</div>
)
}