160 lines
7.5 KiB
TypeScript
160 lines
7.5 KiB
TypeScript
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>
|
||
);
|
||
};
|