diff --git a/frontend/src/components/layout/AISidebar.tsx b/frontend/src/components/layout/AISidebar.tsx index ef2a35f..45ef3e6 100644 --- a/frontend/src/components/layout/AISidebar.tsx +++ b/frontend/src/components/layout/AISidebar.tsx @@ -5,6 +5,7 @@ import { SuggestionList } from '@/components/ai/SuggestionSidebar'; import { ImprovementPanel } from '@/components/ai/ImprovementPanel'; import { createSession, fetchSessions } from '@/api/ai'; import { useUIStore } from '@/store/uiStore'; +import { getContributedTabs } from './sidebarTabs'; import { useTranslation } from 'react-i18next'; import { useUsers, useGroups } from '@/api/hooks'; import { Avatar } from '@/components/ui/Avatar'; @@ -35,7 +36,7 @@ const chevronRightIcon = ( ); interface TabDef { - key: 'proactive' | 'chat' | 'notifications' | 'team' | 'chatroom'; + key: string; label: string; icon: (cls: string) => React.ReactNode; testId: string; @@ -139,6 +140,13 @@ export function AISidebar() { { key: 'notifications', label: t('topbar.notifications'), icon: bellIcon, testId: 'ai-sidebar-tab-notifications' }, { key: 'team', label: 'Team', icon: teamIcon, testId: 'ai-sidebar-tab-team' }, { key: 'chatroom', label: 'Chat', icon: chatBubbleIcon, testId: 'ai-sidebar-tab-chatroom' }, + // Plugin-contributed tabs (Block H / HC-G) + ...getContributedTabs().map((ct) => ({ + key: ct.key, + label: ct.label, + icon: ct.icon, + testId: ct.testId, + })), ]; const activeTab = tabs.find((tab) => tab.key === aiSidebarTab) || tabs[0]; @@ -209,6 +217,12 @@ export function AISidebar() { if (aiSidebarTab === 'chatroom') { return ; } + // Plugin-contributed tabs (Block H / HC-G) + const contributed = getContributedTabs().find((ct) => ct.key === aiSidebarTab); + if (contributed) { + const ContributedComponent = contributed.component; + return ; + } // chat tab if (loading) { return ( diff --git a/frontend/src/components/layout/sidebarTabs.ts b/frontend/src/components/layout/sidebarTabs.ts new file mode 100644 index 0000000..1eeaa54 --- /dev/null +++ b/frontend/src/components/layout/sidebarTabs.ts @@ -0,0 +1,42 @@ +/** + * AI sidebar tab registry. + * + * Core owns the base tabs (chat/proactive/notifications/team/chatroom); + * plugins contribute additional tabs via ``registerSidebarTab`` during their + * frontend activation lifecycle (Block H / HC-G). Contributed tabs render a + * generic container component provided by the plugin. + */ +import type { ComponentType } from 'react'; + +type IconFn = (className: string) => JSX.Element; + +export interface ContributedTab { + key: string; + label: string; + icon: IconFn; + testId: string; + component: ComponentType; +} + +const _tabs = new Map(); + +export function registerSidebarTab(tab: ContributedTab): void { + if (_tabs.has(tab.key)) { + // eslint-disable-next-line no-console + console.warn(`[sidebarTabs] tab '${tab.key}' re-registered — overwriting`); + } + _tabs.set(tab.key, tab); +} + +export function unregisterSidebarTabs(owner: string): void { + for (const [key, tab] of _tabs.entries()) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((tab.component as any).__sidebar_owner__ === owner) { + _tabs.delete(key); + } + } +} + +export function getContributedTabs(): ContributedTab[] { + return Array.from(_tabs.values()); +} diff --git a/frontend/src/store/uiStore.ts b/frontend/src/store/uiStore.ts index 848a8ff..a67094e 100644 --- a/frontend/src/store/uiStore.ts +++ b/frontend/src/store/uiStore.ts @@ -2,7 +2,9 @@ import { create } from 'zustand'; export type Theme = 'light' | 'dark' | 'system'; export type Locale = 'de' | 'en'; -export type AISidebarTab = 'proactive' | 'chat' | 'notifications' | 'team' | 'chatroom'; +// Plugin-contributed tabs add arbitrary keys (Block H / HC-G) — the union +// keeps autocomplete for the base tabs while allowing any string key. +export type AISidebarTab = 'proactive' | 'chat' | 'notifications' | 'team' | 'chatroom' | (string & {}); export interface Toast { id: string;