diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx index 9dda0fe..e262df2 100644 --- a/frontend/src/components/layout/AppShell.tsx +++ b/frontend/src/components/layout/AppShell.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Outlet } from 'react-router-dom'; import { Sidebar } from './Sidebar'; import { TopBar } from './TopBar'; +import { PluginToolbar } from './PluginToolbar'; import { ToastContainer } from '@/components/ui/Toast'; export function AppShell() { @@ -10,6 +11,7 @@ export function AppShell() {
+
+ {item.icon && {item.icon}} + {item.label} + + ); +} + +function ToolbarSearch({ item }: { item: ToolbarItem }) { + return ( + item.onSearch?.(e.target.value)} + className="w-40 px-2 py-1 text-xs border border-secondary-200 rounded + focus:outline-none focus:border-primary-400 focus:ring-1 focus:ring-primary-300 + placeholder:text-secondary-400" + /> + ); +} + +function ToolbarSelect({ item }: { item: ToolbarItem }) { + return ( + + ); +} + +export function PluginToolbar() { + const { items, activePlugin } = usePluginToolbarStore(); + const visibleItems = items.filter((i) => i.plugin === activePlugin); + + if (visibleItems.length === 0) return null; + + // Group items by group field + const groups: Record = {}; + for (const item of visibleItems) { + const g = item.group || 'default'; + if (!groups[g]) groups[g] = []; + groups[g].push(item); + } + const groupNames = Object.keys(groups); + + return ( +
+ {groupNames.map((groupName, gi) => ( + + {gi > 0 &&
} +
+ {groups[groupName].map((item) => { + if (item.type === 'search') return ; + if (item.type === 'select') return ; + return ; + })} +
+ + ))} +
+ ); +} diff --git a/frontend/src/pages/Mail.tsx b/frontend/src/pages/Mail.tsx index ea8966a..faff3b9 100644 --- a/frontend/src/pages/Mail.tsx +++ b/frontend/src/pages/Mail.tsx @@ -15,6 +15,7 @@ import { MailDetail } from '@/components/mail/MailDetail'; import { ComposeModal, type ComposeMode } from '@/components/mail/ComposeModal'; import { SharedMailboxSelector } from '@/components/mail/SharedMailboxSelector'; import { MailSearchBar } from '@/components/mail/MailSearchBar'; +import { usePluginToolbarStore } from '@/store/pluginToolbarStore'; import { fetchAccounts, fetchFolders, @@ -280,6 +281,57 @@ export function MailPage() { setSearchQuery(''); }, []); + // Register toolbar items + const { registerItems, unregisterPlugin } = usePluginToolbarStore(); + useEffect(() => { + registerItems('mail', [ + { + id: 'account-select', + plugin: 'mail', + label: 'Account', + type: 'select', + group: 'account', + selectValue: selectedAccountId, + selectOptions: accounts.map((a) => ({ value: a.id, label: a.email })), + onSelect: handleAccountSwitch, + }, + { + id: 'search', + plugin: 'mail', + label: 'Suchen', + type: 'search', + group: 'search', + searchPlaceholder: 'E-Mails durchsuchen...', + onSearch: handleSearch, + }, + { + id: 'compose', + plugin: 'mail', + label: 'Verfassen', + group: 'actions', + icon: , + onClick: handleCompose, + }, + { + id: 'sync', + plugin: 'mail', + label: 'Sync', + group: 'actions', + icon: , + onClick: () => { /* sync trigger */ }, + }, + { + id: 'settings', + plugin: 'mail', + label: 'Einstellungen', + group: 'actions', + icon: , + onClick: () => window.location.href = '/mail/settings', + }, + ]); + return () => unregisterPlugin('mail'); + }, [registerItems, unregisterPlugin, accounts, selectedAccountId, handleAccountSwitch, handleSearch, handleCompose]); + if (loadingAccounts) { return (
@@ -306,29 +358,6 @@ export function MailPage() { return (
- {/* Top bar */} -
- -
- -
-
- - - {t('mail.settings')} - -
-
- {error && (

{error}

diff --git a/frontend/src/store/pluginToolbarStore.ts b/frontend/src/store/pluginToolbarStore.ts new file mode 100644 index 0000000..8801346 --- /dev/null +++ b/frontend/src/store/pluginToolbarStore.ts @@ -0,0 +1,49 @@ +import { create } from 'zustand'; + +export interface ToolbarItem { + id: string; + plugin: string; + label: string; + icon?: React.ReactNode; + onClick: () => void; + group?: string; + disabled?: boolean; + active?: boolean; + type?: 'button' | 'search' | 'select'; + searchPlaceholder?: string; + onSearch?: (query: string) => void; + selectOptions?: { value: string; label: string }[]; + selectValue?: string; + onSelect?: (value: string) => void; +} + +interface PluginToolbarState { + items: ToolbarItem[]; + activePlugin: string | null; + registerItems: (plugin: string, items: ToolbarItem[]) => void; + unregisterPlugin: (plugin: string) => void; + setActivePlugin: (plugin: string | null) => void; + updateItem: (plugin: string, id: string, updates: Partial) => void; +} + +export const usePluginToolbarStore = create((set) => ({ + items: [], + activePlugin: null, + registerItems: (plugin, newItems) => + set((state) => ({ + items: [...state.items.filter((i) => i.plugin !== plugin), ...newItems], + activePlugin: plugin, + })), + unregisterPlugin: (plugin) => + set((state) => ({ + items: state.items.filter((i) => i.plugin !== plugin), + activePlugin: state.activePlugin === plugin ? null : state.activePlugin, + })), + setActivePlugin: (plugin) => set({ activePlugin: plugin }), + updateItem: (plugin, id, updates) => + set((state) => ({ + items: state.items.map((i) => + i.plugin === plugin && i.id === id ? { ...i, ...updates } : i + ), + })), +}));