feat(plugins): resizable collapsible detail sidebar, auto-init on refresh

- Add email-detail-sidebar slot type for right-side plugin panels
- Plugin sidebar inside EmailViewer with ResizeHandle (200-500px, drag/dbl-click reset)
- Collapse/expand toggle with PanelRightClose/PanelRightOpen icons
- Call initializePlugins() on mount so plugins survive page refresh
- Fix plugin loader: call exposePluginExternals() before loading
- Fix CSP: add blob: to script-src for plugin bundle loading
- Fix enablePlugin: wire slot registration bridge before loading
- Fix email-banner PluginSlot: pass email via extraProps
- Fix sidebar-widget PluginSlot: move inside scrollable area
- Emit emailHooks.onEmailOpen/onEmailClose from selectEmail()
This commit is contained in:
Linus Rath
2026-03-25 00:44:05 +01:00
parent 29a222eef4
commit 54981950b0
10 changed files with 104 additions and 10 deletions
+1
View File
@@ -48,6 +48,7 @@ function resetStore() {
'email-footer': [],
'composer-toolbar': [],
'sidebar-widget': [],
'email-detail-sidebar': [],
'settings-section': [],
'context-menu-email': [],
'navigation-rail-bottom': [],
+6
View File
@@ -113,6 +113,7 @@ export interface PluginAPI {
registerSettingsSection: (section: SettingsSection) => Disposable;
registerComposerAction: (action: ComposerAction) => Disposable;
registerSidebarWidget: (widget: SidebarWidget) => Disposable;
registerDetailSidebar: (widget: SidebarWidget) => Disposable;
registerContextMenuItem: (item: ContextMenuItem) => Disposable;
registerNavigationRailItem: (component: React.ComponentType) => Disposable;
};
@@ -556,6 +557,11 @@ export function createPluginAPI(plugin: InstalledPlugin): PluginAPI {
return registerSlot(plugin.id, 'sidebar-widget', widget.render as React.ComponentType<Record<string, unknown>>, widget.order ?? 100);
},
registerDetailSidebar: (widget: SidebarWidget) => {
requirePermission(plugin, 'ui:sidebar-widget');
return registerSlot(plugin.id, 'email-detail-sidebar', widget.render as React.ComponentType<Record<string, unknown>>, widget.order ?? 100);
},
registerContextMenuItem: (item: ContextMenuItem) => {
requirePermission(plugin, 'ui:context-menu');
const Component = () => {
+3
View File
@@ -49,6 +49,9 @@ export async function loadPlugin(plugin: InstalledPlugin): Promise<void> {
return;
}
// Ensure React/ReactDOM are exposed before any plugin module evaluates
exposePluginExternals();
try {
// 1. Read bundle from IndexedDB
const code = await pluginStorage.getCode(plugin.id);
+1
View File
@@ -85,6 +85,7 @@ export type SlotName =
| 'email-footer'
| 'composer-toolbar'
| 'sidebar-widget'
| 'email-detail-sidebar'
| 'settings-section'
| 'context-menu-email'
| 'navigation-rail-bottom';