a plugin can be more than one screen

a plugin was one screen because the two things it declares — a mount prefix and
a frontend — happened to be the same string. caldav is where that stops holding:
one sidecar, one permission, one install, and two screens that have nothing to
do with each other on screen. calendar and contacts are not sections of one
page, and forcing them into one would be a worse product than the core screens
they replace.

nothing else moved. all eighteen plugins with a frontend use flat
web/panels.ts, which is now "one screen, at the mount prefix, labelled by the
manifest" — the same file, the same route, the same persisted layout key. the
nested form is opt-in and caldav is its only user.

the split of who declares what follows the rule this file already had. the
DIRECTORY declares that a screen exists (web/<segment>/panels.ts); the MANIFEST
says what to call it and which glyph to draw, because a directory listing cannot
say either and those are a human's choice. the two are cross-checked and a
mismatch is refused rather than reconciled: a segment with no directory renders
nothing, a directory with no entry has no tile and no label, and both are the
quiet kind of broken.

the part that needed the most care is that a segment is a URL the plugin CHOSE
— /calendar, not /caldav/calendar. mountPrefix is what makes it impossible for a
plugin to shadow a core route, and a chosen segment steps straight out of it. so
this is first-party only, refused at discovery, and tested: without that check
this field is a way for any installed plugin to take over /files.

screenId, not appName, is now the layout key and the panel allow-list's owner.
two screens sharing one would persist over each other's sizes and each would
accept the other's panels. existing plugins get screenId === appName, so no
stored layout is orphaned — verified by regenerating.
This commit is contained in:
2026-08-27 04:15:51 +00:00
parent 131af603ff
commit 6b5c87da10
7 changed files with 316 additions and 47 deletions
+54 -1
View File
@@ -238,6 +238,30 @@ export type PluginManifest = {
permissions: PluginPermission[];
/**
* The frontend screens this plugin contributes, when it contributes more than one.
*
* Omit it and the plugin has exactly one screen at its mount prefix, labelled `label` — which is every
* plugin but caldav, and the reason nothing else had to change when this arrived.
*
* ── Why the label is HERE and the existence is on disk ──
*
* Presence is the declaration everywhere else in this file, and it still is: `web/<segment>/panels.ts`
* is what makes a screen exist. What a directory listing cannot say is what to CALL it and which glyph
* to draw, and those are exactly what this file is for — a human choice, not a structural fact.
*
* The two are cross-checked at discovery and a mismatch is REFUSED, not reconciled: a segment here with
* no directory, or a directory with no entry here, names a screen that would render nothing or a screen
* with no tile. Both are the quiet kind of broken.
*
* ── The segment is a URL a first-party plugin chooses ──
*
* `/calendar` and `/contacts`, not `/caldav/calendar`. That is only sound because `mountPrefix` confines
* third parties to `/p/<publisher>/` — so a third-party plugin declaring `screens` is refused. Without
* that check this field would be a way for any installed plugin to shadow a core route.
*/
screens?: { segment: string; label: string; icon?: string }[];
/**
* Paths under this plugin's own mount that are served ABOVE the account gate.
*
@@ -478,7 +502,36 @@ export type DiscoveredPlugin = {
* Both files are required when `web/` exists at all. Missing either is a broken plugin, not a plugin
* with a partial frontend.
*/
web: { panels: string; layout: string } | null;
web: { screens: DiscoveredScreen[] } | null;
};
/**
* One frontend screen: a route, the panels on it, and how they are arranged.
*
* ── Why a plugin can have more than one ──
*
* A plugin was one screen because the two things it declares — a mount prefix and a frontend — happened to
* be the same string. Caldav is where that stops holding: one sidecar, one permission, one install, and
* two screens that have nothing to do with each other on screen. Calendar and Contacts are not sections of
* one page and forcing them into one would be a worse product than the core screens they replace.
*
* ── segment ──
*
* `null` for the ordinary plugin, which means "mount at the prefix" and is every plugin but this one. A
* named segment is a first-party plugin choosing its own URL — `/calendar`, not `/caldav/calendar` — which
* is only safe because `mountPrefix` still confines third parties to `/p/<publisher>/`. A third-party
* plugin naming a segment is refused at discovery, and that check is what keeps this from becoming a way
* to shadow a core route.
*/
export type DiscoveredScreen = {
/** Directory name under `web/`, or null for the flat single-screen form. */
segment: string | null;
/** Dock tile label. The manifest's own `label` for the flat form. */
label: string;
/** Lucide icon NAME for this screen's tile, falling back to the plugin's. */
icon?: string;
panels: string;
layout: string;
};
/**