the connection form — where fofos is, and the token — was reached through `?config=1` and a link in the detail header, because it was not one of the nine sections. it is now the tenth, called Administration, last in the nav. that removes the overlay and the second route to it. the two could disagree: the form covered whatever section was selected underneath, so the nav highlighted one thing while the panel showed another, and an unconfigured plugin rendered the form with no header at all — which reads as the selected section being broken rather than as one setup step not done yet. unconfigured now goes to Administration and the header says so. `site` and `administration` are deliberately separate and the file says why. Site settings is the CLIENT's — partners, social links, contact details, all of it public copy. Administration is Officer's end: how this plugin reaches fofos, and whatever else it will need to know. nothing in it ever appears on the site, and the people who touch the two are not the same people. `path: null`, alone among the sections: it reads nothing from fofos, which is why it keeps working when fofos is unreachable and the others do not. the header's settings link is replaced by the section's own hint, so the row still says what it is once you are in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
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.
|
|
*
|
|
* `<Link>` rather than `<NavLink>` 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.
|
|
*/
|
|
|
|
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;
|
|
|
|
return (
|
|
<nav className="flex size-full flex-col overflow-auto">
|
|
{SECTIONS.map((section) => {
|
|
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 (
|
|
<Link
|
|
key={section.key}
|
|
to={{ search: `?${search.toString()}` }}
|
|
replace
|
|
className={rowClass(active === section.key)}
|
|
>
|
|
<Icon className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
|
<span className="min-w-0 flex-1">
|
|
<span className="flex items-baseline gap-2">
|
|
<span className={`truncate text-sm ${section.built ? '' : 'text-muted-foreground'}`}>
|
|
{section.label}
|
|
</span>
|
|
{/* 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 && (
|
|
<span className="text-[10px] tracking-wide text-muted-foreground/70 uppercase">soon</span>
|
|
)}
|
|
</span>
|
|
<span className="block truncate text-xs text-muted-foreground">{section.hint}</span>
|
|
</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
};
|