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:
@@ -289,23 +289,37 @@ export async function pluginDockManifests(): Promise<
|
||||
}>
|
||||
> {
|
||||
const { states } = await snapshotPlugins();
|
||||
// One tile per SCREEN. A plugin with two screens gets two tiles behind ONE permission — caldav is
|
||||
// Calendar and Contacts, which are two places to go and one thing to be allowed to reach. Filtering
|
||||
// stays per-plugin for that reason: the permission is the plugin's, and a screen is not a smaller
|
||||
// grant, it is another door into the same room.
|
||||
return states
|
||||
.filter((s) => s.install?.enabled && s.plugin.web)
|
||||
.map(({ plugin }) => {
|
||||
const prefix = mountPrefix(plugin);
|
||||
return {
|
||||
sidecarId: plugin.appName,
|
||||
permission: plugin.manifest.permissions[0]?.key ?? null,
|
||||
name: plugin.manifest.label,
|
||||
// A shipped `assets/icon.png` wins; the lucide NAME is the fallback for a plugin with no
|
||||
// artwork of its own. The dock renders `image` as an <img> and `icon` through `resolveIcon`,
|
||||
// and it already preferred the image — this is the first thing to give it one.
|
||||
...(plugin.icon ? { image: iconUrl(plugin.appName) } : { icon: plugin.manifest.icon }),
|
||||
// Artwork defaults to `bare`, a glyph to `badge`; the manifest overrides either way.
|
||||
tile: plugin.manifest.tile ?? (plugin.icon ? 'bare' : 'badge'),
|
||||
color: plugin.manifest.color,
|
||||
rootRoute: prefix,
|
||||
routes: [prefix],
|
||||
};
|
||||
});
|
||||
.flatMap(({ plugin }) =>
|
||||
plugin.web!.screens.map((screen) => {
|
||||
const route = screen.segment ? `/${screen.segment}` : mountPrefix(plugin);
|
||||
return {
|
||||
sidecarId: plugin.appName,
|
||||
permission: plugin.manifest.permissions[0]?.key ?? null,
|
||||
name: screen.label,
|
||||
// A shipped `assets/icon.png` wins; the lucide NAME is the fallback for a plugin with no
|
||||
// artwork of its own. The dock renders `image` as an <img> and `icon` through `resolveIcon`,
|
||||
// and it already preferred the image — this is the first thing to give it one.
|
||||
//
|
||||
// A plugin with several screens is the one case where the shared artwork is WRONG: two
|
||||
// identical tiles side by side say nothing about which is which. So a screen naming its own
|
||||
// glyph overrides the image, and one that does not falls back to the plugin's.
|
||||
...(screen.icon && plugin.web!.screens.length > 1
|
||||
? { icon: screen.icon }
|
||||
: plugin.icon
|
||||
? { image: iconUrl(plugin.appName) }
|
||||
: { icon: screen.icon ?? plugin.manifest.icon }),
|
||||
// Artwork defaults to `bare`, a glyph to `badge`; the manifest overrides either way.
|
||||
tile: plugin.manifest.tile ?? (plugin.icon && plugin.web!.screens.length === 1 ? 'bare' : 'badge'),
|
||||
color: plugin.manifest.color,
|
||||
rootRoute: route,
|
||||
routes: [route],
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user