08fb6d45f3
Signed-off-by: Douwe M Osinga <douwe@sidewalklabs.com> Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com> Co-authored-by: Michael Neale <michael.neale@gmail.com>
135 lines
4.6 KiB
TypeScript
135 lines
4.6 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { IpcRendererEvent } from 'electron';
|
|
import { Outlet, useLocation } from 'react-router-dom';
|
|
import { motion } from 'framer-motion';
|
|
import { Menu, PanelLeft } from 'lucide-react';
|
|
import { defineMessages, useIntl } from '../../i18n';
|
|
import { Button } from '../ui/button';
|
|
import ChatSessionsContainer from '../ChatSessionsContainer';
|
|
import { useChatContext } from '../../contexts/ChatContext';
|
|
import { NavigationProvider, useNavigationContext } from './NavigationContext';
|
|
import { Navigation } from './NavigationPanel';
|
|
import { NAV_DIMENSIONS, Z_INDEX } from './constants';
|
|
import { cn } from '../../utils';
|
|
import { UserInput } from '../../types/message';
|
|
|
|
const i18n = defineMessages({
|
|
openNavigation: {
|
|
id: 'appLayout.openNavigation',
|
|
defaultMessage: 'Open navigation',
|
|
},
|
|
collapseNavigation: {
|
|
id: 'appLayout.collapseNavigation',
|
|
defaultMessage: 'Collapse navigation',
|
|
},
|
|
});
|
|
|
|
interface AppLayoutContentProps {
|
|
activeSessions: Array<{
|
|
sessionId: string;
|
|
initialMessage?: UserInput;
|
|
noAutoSubmit?: boolean;
|
|
}>;
|
|
}
|
|
|
|
const AppLayoutContent: React.FC<AppLayoutContentProps> = ({ activeSessions }) => {
|
|
const intl = useIntl();
|
|
const location = useLocation();
|
|
const safeIsMacOS = (window?.electron?.platform || 'darwin') === 'darwin';
|
|
const chatContext = useChatContext();
|
|
const isOnPairRoute = location.pathname === '/pair';
|
|
|
|
const [isFullScreen, setIsFullScreen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!safeIsMacOS) return;
|
|
window.electron
|
|
.getIsFullScreen()
|
|
.then(setIsFullScreen)
|
|
.catch(() => {});
|
|
const handler = (_event: IpcRendererEvent, ...args: unknown[]) => {
|
|
setIsFullScreen(Boolean(args[0]));
|
|
};
|
|
window.electron.on('fullscreen-change', handler);
|
|
return () => window.electron.off('fullscreen-change', handler);
|
|
}, [safeIsMacOS]);
|
|
|
|
const { isNavExpanded, setIsNavExpanded } = useNavigationContext();
|
|
|
|
if (!chatContext) {
|
|
throw new Error('AppLayoutContent must be used within ChatProvider');
|
|
}
|
|
|
|
const { setChat } = chatContext;
|
|
|
|
const needsTrafficLightInset = safeIsMacOS && !isFullScreen;
|
|
const headerPadding = needsTrafficLightInset ? 'pl-[96px]' : 'pl-4';
|
|
const headerTop = needsTrafficLightInset ? 'top-[14px]' : 'top-[11px]';
|
|
const navToggleTitle = intl.formatMessage(
|
|
isNavExpanded ? i18n.collapseNavigation : i18n.openNavigation
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-1 w-full h-full relative animate-fade-in bg-background-primary flex-row">
|
|
<div
|
|
style={{ zIndex: Z_INDEX.HEADER }}
|
|
className={cn('absolute flex items-center gap-1', headerPadding, headerTop, 'ml-1.5')}
|
|
>
|
|
<Button
|
|
onClick={() => setIsNavExpanded(!isNavExpanded)}
|
|
className="no-drag hover:!bg-background-tertiary"
|
|
variant="ghost"
|
|
size="xs"
|
|
title={navToggleTitle}
|
|
aria-label={navToggleTitle}
|
|
>
|
|
{isNavExpanded ? <PanelLeft className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Main content with navigation. Shared white canvas; the sidebar is a
|
|
rounded outlined card floating on it with breathing room. */}
|
|
<div className="flex flex-1 w-full h-full min-h-0 flex-row">
|
|
<motion.div
|
|
key="nav"
|
|
initial={false}
|
|
animate={{ width: isNavExpanded ? NAV_DIMENSIONS.NAV_WIDTH : 0 }}
|
|
transition={{ type: 'spring', stiffness: 400, damping: 40 }}
|
|
style={{ height: '100%' }}
|
|
className="relative flex-shrink-0 overflow-hidden h-full p-2"
|
|
>
|
|
<div className="w-full h-full overflow-hidden rounded-xl border border-border-primary">
|
|
<Navigation />
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Main content — no border / no card; just flows on the canvas. */}
|
|
<div className="flex-1 overflow-hidden min-h-0">
|
|
<Outlet />
|
|
{/* Always render ChatSessionsContainer to keep SSE connections alive.
|
|
When navigating away from /pair, hide it with CSS */}
|
|
<div className={isOnPairRoute ? 'contents' : 'hidden'}>
|
|
<ChatSessionsContainer setChat={setChat} activeSessions={activeSessions} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface AppLayoutProps {
|
|
activeSessions: Array<{
|
|
sessionId: string;
|
|
initialMessage?: UserInput;
|
|
noAutoSubmit?: boolean;
|
|
}>;
|
|
}
|
|
|
|
export const AppLayout: React.FC<AppLayoutProps> = ({ activeSessions }) => {
|
|
return (
|
|
<NavigationProvider>
|
|
<AppLayoutContent activeSessions={activeSessions} />
|
|
</NavigationProvider>
|
|
);
|
|
};
|