import { Link, useSearchParams } from 'react-router'; import { DEFAULT_SECTION, SECTIONS, isSectionKey, type SectionKey } from './sections'; /** * The left column: which part of the fofos site the detail panel is showing. * * ── Selection is in the URL ── * * `?section=…`, per `docs/navigation-audit.md`: what is on screen is addressable, so it survives a * reload, can be linked, and the detail panel reads it directly. The two panels are siblings in a * workspace and never speak to each other. * * ── Real links, not buttons ── * * `AGENTS.md`: "Rows and nav items are real links." `plugins/proxy-manager/web/NavPanel.tsx` uses * buttons calling `setParams`, which keeps the URL correct but gives up anchor semantics — no * cmd-click into a new tab, no middle-click, not link-focusable. That is the older pattern and the * file that documents it says not to copy it. * * `` rather than `` because active state here is a QUERY parameter and NavLink's * `isActive` compares paths — every section would light up at once. So the active class is computed * from the same value the detail panel reads, which is the one place it can come from. */ /** * The nav has a FOOT. * * Everything above it is a part of the client's site, in sitemap order. Below the divider is this * plugin's own machinery, which is not a page and does not belong in a list of them — the same split * the plugins nav makes with Update-all, and the reason it sits at the bottom of the panel rather than * at the bottom of the list. `mt-auto` pins it there whether there are ten sections or two. */ const rowClass = (active: boolean) => `flex w-full items-start gap-2.5 border-b border-border px-3 py-2.5 text-left transition-colors ${ active ? 'bg-muted' : 'hover:bg-muted/50' }`; export const NavPanel = () => { const [params] = useSearchParams(); const raw = params.get('section'); const active: SectionKey = isSectionKey(raw) ? raw : DEFAULT_SECTION; const renderRow = (section: (typeof SECTIONS)[number]) => { const Icon = section.icon; // Everything else in the URL is preserved. This is one screen of a workspace, and the global // query string carries state that belongs to the shell rather than to this panel. const search = new URLSearchParams(params); search.set('section', section.key); return ( {section.label} {/* Dimmed and labelled rather than hidden. A nav that shows only what is finished gives no sense of what the thing is for — see sections.ts. */} {!section.built && ( soon )} {section.hint} ); }; return ( ); };