a plugin screen can declare its routes

Every screen got `/<route>` and `/<route>/:section` and nothing deeper, so a
section had to live in the query string. A screen may now name its routes in
the manifest, and give one its own arrangement of the screen's panels.

Absent, the old pair is unchanged — which is every plugin but fofos-admin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 04:57:18 +01:00
co-authored by Claude Opus 5
parent 43f32a7440
commit 1fe767ea77
5 changed files with 200 additions and 16 deletions
@@ -21,6 +21,7 @@ export function PluginScreen({
screenId,
panels,
layout,
layoutKey,
}: {
/**
* This SCREEN's id, not the plugin's — `caldav/calendar`, not `caldav`.
@@ -32,6 +33,14 @@ export function PluginScreen({
screenId: string;
panels: AppRegistryMeta[];
layout: LayoutNode;
/**
* What to persist sizes under, when this route's arrangement is not the screen's default.
*
* A plugin with a route table can give one route a different layout, and two arrangements sharing one
* key would each remember the other's dividers — which reads as a layout that will not stay put. The
* route's own path pattern is the suffix, so it is stable across every URL that matches it.
*/
layoutKey?: string;
}) {
// Per-user and per-plugin, so two plugins never share a layout and a user's arrangement is their own.
//
@@ -39,7 +48,7 @@ export function PluginScreen({
// `layout` on every render, while only the sizes are the user's and persisted. That is what lets a plugin
// ship a new layout and have it reach people who have already opened the screen — and it is what stops
// the `appTypes` repair below from being written back as though the user had chosen it. See the hook.
const workspace = useAuthoredLayout(`screens/plugin/${screenId}`, layout);
const workspace = useAuthoredLayout(`screens/plugin/${layoutKey ?? screenId}`, layout);
const allowed = panels.map((panel) => panel.key);
return (