krona-front/src/app/layouts/AppShell.tsx

160 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { useDemoStore } from '../../mock/store';
import { Sidebar } from '../shell/Sidebar';
import { Topbar } from '../shell/Topbar';
import { RightCommandPanel } from '../shell/RightCommandPanel';
import { BottomNav } from '../shell/BottomNav';
import { CommandPalette } from '../shell/CommandPalette';
import { QuickActionsModal } from '../shell/QuickActionsModal';
import { NotificationsDrawer } from '../shell/NotificationsDrawer';
import { Drawer } from '../../shared/ui';
import { useResponsive } from '../../shared/lib/useResponsive';
import { useCommunicationsStore } from '../../features/communications/services/communicationsService';
export const AppShell: React.FC = () => {
const {
theme,
viewportMode,
isAiDrawerOpen,
setIsAiDrawerOpen,
getActiveProfile,
activeLocation,
} = useDemoStore();
const { width, isMobile, isTablet, isDesktop } = useResponsive();
const profile = getActiveProfile();
const location = useLocation();
const isCommunicationsPage = location.pathname.startsWith('/communications');
const isMobileChatOpen = useCommunicationsStore((state) => state.isMobileChatOpen);
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
const kronaTheme = {
light: 'parchment-light',
'soft-light': 'noble-green',
dark: 'graphite-forest-dark',
}[theme];
document.documentElement.setAttribute('data-krona-theme', kronaTheme);
document.documentElement.setAttribute('data-omni-theme', kronaTheme);
document.documentElement.setAttribute('data-omni-density', isMobile ? 'touch' : 'workspace');
}, [theme, isMobile]);
// Layout resolution:
// 1. Real screen size is paramount:
// - If actual viewport < 768px -> always Mobile Shell.
// - If actual viewport 768-1199px -> always Tablet Shell.
// 2. Demo emulation frame is only applied when on real desktop (>= 1200px) and user explicitly chose a demo mode.
const isEmulatedOnDesktop = isDesktop && viewportMode !== 'desktop';
const isEffectiveMobile = isMobile || (isDesktop && viewportMode === 'mobile');
const isEffectiveTablet = isTablet || (isDesktop && (viewportMode === 'tablet-portrait' || viewportMode === 'tablet-landscape'));
const getViewportContainerStyles = () => {
if (!isEmulatedOnDesktop) {
return isCommunicationsPage ? 'w-full h-screen max-h-screen overflow-hidden' : 'w-full min-h-screen';
}
switch (viewportMode) {
case 'mobile':
return 'max-w-[420px] h-[880px] my-6 border-8 border-krona-border rounded-[36px] overflow-hidden shadow-2xl relative';
case 'tablet-portrait':
return 'max-w-[820px] h-[1050px] my-6 border-8 border-krona-border rounded-[28px] overflow-hidden shadow-2xl relative';
case 'tablet-landscape':
return 'max-w-[1180px] h-[800px] my-6 border-8 border-krona-border rounded-[24px] overflow-hidden shadow-2xl relative';
default:
return isCommunicationsPage ? 'w-full h-screen max-h-screen overflow-hidden' : 'w-full min-h-screen';
}
};
return (
<div
data-testid="app-shell-root"
className={`${
isCommunicationsPage ? 'h-screen max-h-screen overflow-hidden' : 'min-h-screen'
} bg-krona-app text-krona-primary flex items-center justify-center ${
isEmulatedOnDesktop ? 'p-4' : ''
}`}
>
<div
className={`w-full bg-krona-workspace text-krona-primary flex flex-col transition-all duration-200 ${getViewportContainerStyles()}`}
>
{/* Main Shell Layout */}
<div className="flex-1 flex overflow-hidden w-full h-full relative">
{/* Left Sidebar: hidden on mobile, rendered on tablet & desktop */}
{!isEffectiveMobile && (
<Sidebar
isTabletCompact={isEffectiveTablet || (!isEmulatedOnDesktop && width < 1366)}
/>
)}
{/* Center Work Area */}
<div className="flex-1 flex flex-col min-w-0 overflow-hidden bg-krona-workspace">
{!(isEffectiveMobile && isCommunicationsPage && isMobileChatOpen) && <Topbar />}
{/* Main Content Router Outlet */}
<main
className={`flex-1 min-h-0 ${
isCommunicationsPage
? `overflow-hidden flex flex-col pt-0 px-0 ${isEffectiveMobile ? 'pb-14' : 'pb-0'}`
: `overflow-y-auto ${isEffectiveMobile ? 'pb-24' : 'pb-8'} p-3 sm:p-4 md:p-6`
}`}
>
<Outlet />
</main>
</div>
{/* Right Command Panel (Only on full Desktop, not on communications which has its own full right card) */}
{!isEffectiveMobile && !isEffectiveTablet && width >= 1536 && !isCommunicationsPage && (
<RightCommandPanel />
)}
</div>
{/* Mobile Bottom Navigation (Always present on mobile) */}
{isEffectiveMobile && <BottomNav />}
{/* Global Modals & Drawers */}
<CommandPalette />
<QuickActionsModal />
<NotificationsDrawer />
{/* Standalone AI Drawer */}
<Drawer
isOpen={isAiDrawerOpen}
onClose={() => setIsAiDrawerOpen(false)}
title="✦ Krona AI — Бизнес-оркестратор"
subtitle={`Контекст: ${profile.name} • ${activeLocation}`}
width="md"
>
<div className="space-y-4 text-xs">
<div className="p-3 bg-krona-gold/10 border border-krona-gold/30 rounded-xl leading-relaxed text-krona-gold-text">
<strong>Krona AI:</strong> Интеллектуальный помощник подключен к локальной модели данных бизнеса, остаткам, расписанию смен и фискальным отчетам.
</div>
<div className="space-y-2">
<span className="text-xs font-semibold text-krona-secondary uppercase tracking-wider">
Ролевые агенты команды:
</span>
<div className="grid grid-cols-2 gap-2">
<div className="p-2.5 rounded-xl border border-krona-border bg-krona-surface">
<div className="font-semibold text-krona-primary">Бизнес-аналитик</div>
<div className="text-[11px] text-krona-secondary">Food cost, выручка, чеки</div>
</div>
<div className="p-2.5 rounded-xl border border-krona-border bg-krona-surface">
<div className="font-semibold text-krona-primary">Складской агент</div>
<div className="text-[11px] text-krona-secondary">Остатки, неснижаемый лимит</div>
</div>
<div className="p-2.5 rounded-xl border border-krona-border bg-krona-surface">
<div className="font-semibold text-krona-primary">Маркетолог</div>
<div className="text-[11px] text-krona-secondary">Сегменты, акции, рассылки</div>
</div>
<div className="p-2.5 rounded-xl border border-krona-border bg-krona-surface">
<div className="font-semibold text-krona-primary">HR-ассистент</div>
<div className="text-[11px] text-krona-secondary">График смен, переработки</div>
</div>
</div>
</div>
</div>
</Drawer>
</div>
</div>
);
};