Administration sits at the foot of the panel, not the end of the list
it was last in SECTIONS, so it rendered as the tenth row and then stopped — directly under Messages, with empty panel beneath it. that is the bottom of the LIST. what it wanted is the bottom of the PANEL, the way Update-all sits in the plugins nav. `mt-auto` on a second group, divided by the same border the rows already use. it holds whether there are ten sections or two, and it does not need a spacer element to push it down. the split is a `pinned` flag on the section rather than the nav testing for the key. which sections are the client's site and which are this plugin's own machinery is a fact about the section, and the nav should be reading it, not knowing it. one array still — `sectionFor` and `isSectionKey` have to resolve every key, and a second exported list is a second place to forget one. the row body came out into `renderRow` so both groups draw the same row. no change to what a row looks like. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -35,7 +35,7 @@ import type { PluginManifest } from '@@/plugins/manifest';
|
|||||||
*/
|
*/
|
||||||
export const manifest: PluginManifest = {
|
export const manifest: PluginManifest = {
|
||||||
publisher: 'officerdev',
|
publisher: 'officerdev',
|
||||||
version: '2026.08.29.02',
|
version: '2026.08.29.03',
|
||||||
platform: '>=1.0.0',
|
platform: '>=1.0.0',
|
||||||
|
|
||||||
label: 'Fofos',
|
label: 'Fofos',
|
||||||
|
|||||||
+46
-31
@@ -22,6 +22,15 @@ import { DEFAULT_SECTION, SECTIONS, isSectionKey, type SectionKey } from './sect
|
|||||||
* from the same value the detail panel reads, which is the one place it can come from.
|
* 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) =>
|
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 ${
|
`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'
|
active ? 'bg-muted' : 'hover:bg-muted/50'
|
||||||
@@ -32,40 +41,46 @@ export const NavPanel = () => {
|
|||||||
const raw = params.get('section');
|
const raw = params.get('section');
|
||||||
const active: SectionKey = isSectionKey(raw) ? raw : DEFAULT_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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex size-full flex-col overflow-auto">
|
<nav className="flex size-full flex-col overflow-auto">
|
||||||
{SECTIONS.map((section) => {
|
<div className="flex flex-col">{SECTIONS.filter((section) => !section.pinned).map(renderRow)}</div>
|
||||||
const Icon = section.icon;
|
|
||||||
|
|
||||||
// Everything else in the URL is preserved. This is one screen of a workspace, and the global
|
{/* Separated by the same border the rows use, so it reads as the end of the panel rather than as
|
||||||
// query string carries state that belongs to the shell rather than to this panel.
|
one more page of the site. `border-t` and not `border-t-2`: it is a divider, not a heading. */}
|
||||||
const search = new URLSearchParams(params);
|
<div className="mt-auto flex flex-col border-t border-border">
|
||||||
search.set('section', section.key);
|
{SECTIONS.filter((section) => section.pinned).map(renderRow)}
|
||||||
|
</div>
|
||||||
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>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ export type Section = {
|
|||||||
/** The fofos path this reads, relative to the plugin's mount. Null when nothing is built. */
|
/** The fofos path this reads, relative to the plugin's mount. Null when nothing is built. */
|
||||||
path: string | null;
|
path: string | null;
|
||||||
built: boolean;
|
built: boolean;
|
||||||
|
/**
|
||||||
|
* Pinned to the FOOT of the nav rather than sitting in the list.
|
||||||
|
*
|
||||||
|
* A flag rather than the nav hard-coding a key, because "which sections are the site's and which are
|
||||||
|
* the plugin's own" is a fact about the section. It is also why this stays in one array: `sectionFor`
|
||||||
|
* and `isSectionKey` have to resolve every key, and a second exported list would be a second place to
|
||||||
|
* forget one.
|
||||||
|
*/
|
||||||
|
pinned?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECTIONS: Section[] = [
|
export const SECTIONS: Section[] = [
|
||||||
@@ -135,6 +144,7 @@ export const SECTIONS: Section[] = [
|
|||||||
hint: 'Where fofos is, and the token to reach it',
|
hint: 'Where fofos is, and the token to reach it',
|
||||||
path: null,
|
path: null,
|
||||||
built: true,
|
built: true,
|
||||||
|
pinned: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user