feat: AI sidebar full height with collapsed icon strip and drag resize
- AISidebar: full height, right of TopBar and Toolbar (all pages except AI Assistant) - AISidebar: collapsed mode shows narrow 48px icon strip with KI button - AISidebar: expanded mode uses ResizablePanel with drag-to-resize (240-600px) - AppShell: moved AISidebar outside inner content container to full-height flex - uiStore: renamed aiSidebarOpen→aiSidebarCollapsed (default: true=collapsed) - TopBar: AI button highlights when sidebar is expanded
This commit is contained in:
@@ -5,7 +5,7 @@ import { createSession, fetchSessions } from '@/api/ai';
|
|||||||
import { useUIStore } from '@/store/uiStore';
|
import { useUIStore } from '@/store/uiStore';
|
||||||
|
|
||||||
export function AISidebar() {
|
export function AISidebar() {
|
||||||
const { setAISidebarOpen } = useUIStore();
|
const { aiSidebarCollapsed, toggleAISidebar } = useUIStore();
|
||||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -27,6 +27,28 @@ export function AISidebar() {
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Collapsed: narrow icon strip
|
||||||
|
if (aiSidebarCollapsed) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 w-12 flex flex-col items-center border-l border-secondary-200 bg-white py-3"
|
||||||
|
data-testid="ai-sidebar-collapsed"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={toggleAISidebar}
|
||||||
|
className="p-2 rounded-md hover:bg-secondary-100 min-h-touch min-w-touch flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||||
|
aria-label="KI Assistent öffnen"
|
||||||
|
title="KI Assistent"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-secondary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expanded: full chat panel with resize handle
|
||||||
return (
|
return (
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
initialWidth={320}
|
initialWidth={320}
|
||||||
@@ -39,11 +61,14 @@ export function AISidebar() {
|
|||||||
<div className="h-12 flex items-center justify-between px-3 border-b border-secondary-200">
|
<div className="h-12 flex items-center justify-between px-3 border-b border-secondary-200">
|
||||||
<span className="text-sm font-medium text-secondary-700">KI Assistent</span>
|
<span className="text-sm font-medium text-secondary-700">KI Assistent</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setAISidebarOpen(false)}
|
onClick={toggleAISidebar}
|
||||||
className="text-secondary-400 hover:text-secondary-600"
|
className="p-1 rounded text-secondary-400 hover:text-secondary-600 hover:bg-secondary-100"
|
||||||
title="Schließen"
|
title="Einklappen"
|
||||||
|
aria-label="KI Assistent einklappen"
|
||||||
>
|
>
|
||||||
✕
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import { ToastContainer } from '@/components/ui/Toast';
|
|||||||
import { useUIStore } from '@/store/uiStore';
|
import { useUIStore } from '@/store/uiStore';
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const { aiSidebarOpen } = useUIStore();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Hide AI sidebar on the AI Assistant page itself
|
// Hide AI sidebar on the AI Assistant page itself
|
||||||
const showAISidebar = aiSidebarOpen && !location.pathname.startsWith('/ai-assistant');
|
const showAISidebar = !location.pathname.startsWith('/ai-assistant');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-secondary-50" data-testid="app-shell">
|
<div className="flex h-screen overflow-hidden bg-secondary-50" data-testid="app-shell">
|
||||||
@@ -30,9 +29,10 @@ export function AppShell() {
|
|||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* AI Sidebar — full height, right of TopBar and Toolbar */}
|
||||||
{showAISidebar && <AISidebar />}
|
{showAISidebar && <AISidebar />}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function TopBar() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
const { toggleSidebar, toggleAISidebar } = useUIStore();
|
const { toggleSidebar, toggleAISidebar, aiSidebarCollapsed } = useUIStore();
|
||||||
const { currentTenant, availableTenants, switchTenant, isSwitching } = useTenant();
|
const { currentTenant, availableTenants, switchTenant, isSwitching } = useTenant();
|
||||||
const logoutMutation = useLogout();
|
const logoutMutation = useLogout();
|
||||||
|
|
||||||
@@ -72,7 +72,10 @@ export function TopBar() {
|
|||||||
{/* AI Assistant toggle */}
|
{/* AI Assistant toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={toggleAISidebar}
|
onClick={toggleAISidebar}
|
||||||
className="p-2 rounded-md hover:bg-secondary-100 min-h-touch min-w-touch flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
className={clsx(
|
||||||
|
'p-2 rounded-md min-h-touch min-w-touch flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
||||||
|
aiSidebarCollapsed ? 'hover:bg-secondary-100' : 'bg-primary-50 text-primary-600 hover:bg-primary-100',
|
||||||
|
)}
|
||||||
aria-label="KI Assistent"
|
aria-label="KI Assistent"
|
||||||
title="KI Assistent"
|
title="KI Assistent"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ export interface UIState {
|
|||||||
theme: Theme;
|
theme: Theme;
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
aiSidebarOpen: boolean;
|
aiSidebarCollapsed: boolean;
|
||||||
toasts: Toast[];
|
toasts: Toast[];
|
||||||
setTheme: (theme: Theme) => void;
|
setTheme: (theme: Theme) => void;
|
||||||
setLocale: (locale: Locale) => void;
|
setLocale: (locale: Locale) => void;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
setSidebarOpen: (open: boolean) => void;
|
setSidebarOpen: (open: boolean) => void;
|
||||||
toggleAISidebar: () => void;
|
toggleAISidebar: () => void;
|
||||||
setAISidebarOpen: (open: boolean) => void;
|
setAISidebarCollapsed: (collapsed: boolean) => void;
|
||||||
addToast: (toast: Omit<Toast, 'id'>) => void;
|
addToast: (toast: Omit<Toast, 'id'>) => void;
|
||||||
removeToast: (id: string) => void;
|
removeToast: (id: string) => void;
|
||||||
clearToasts: () => void;
|
clearToasts: () => void;
|
||||||
@@ -33,7 +33,7 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
theme: (localStorage.getItem('leocrm_theme') as Theme) || 'light',
|
theme: (localStorage.getItem('leocrm_theme') as Theme) || 'light',
|
||||||
locale: (localStorage.getItem('leocrm_lang') as Locale) || 'de',
|
locale: (localStorage.getItem('leocrm_lang') as Locale) || 'de',
|
||||||
sidebarOpen: true,
|
sidebarOpen: true,
|
||||||
aiSidebarOpen: false,
|
aiSidebarCollapsed: true,
|
||||||
toasts: [],
|
toasts: [],
|
||||||
setTheme: (theme) => {
|
setTheme: (theme) => {
|
||||||
localStorage.setItem('leocrm_theme', theme);
|
localStorage.setItem('leocrm_theme', theme);
|
||||||
@@ -45,8 +45,8 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
},
|
},
|
||||||
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
||||||
setSidebarOpen: (open) => set({ sidebarOpen: open }),
|
setSidebarOpen: (open) => set({ sidebarOpen: open }),
|
||||||
toggleAISidebar: () => set((s) => ({ aiSidebarOpen: !s.aiSidebarOpen })),
|
toggleAISidebar: () => set((s) => ({ aiSidebarCollapsed: !s.aiSidebarCollapsed })),
|
||||||
setAISidebarOpen: (open) => set({ aiSidebarOpen: open }),
|
setAISidebarCollapsed: (collapsed) => set({ aiSidebarCollapsed: collapsed }),
|
||||||
addToast: (toast) => {
|
addToast: (toast) => {
|
||||||
const id = `toast-${++toastIdCounter}`;
|
const id = `toast-${++toastIdCounter}`;
|
||||||
set((s) => ({ toasts: [...s.toasts, { ...toast, id }] }));
|
set((s) => ({ toasts: [...s.toasts, { ...toast, id }] }));
|
||||||
|
|||||||
Reference in New Issue
Block a user