floating action button for sessions panel

This commit is contained in:
AI Christianson 2025-03-14 10:08:17 -04:00
parent af16879dd6
commit 8a507f245e
11 changed files with 387 additions and 209 deletions

View File

@ -1,6 +1,7 @@
export * from './button'; export * from './button';
export * from './card'; export * from './card';
export * from './collapsible'; export * from './collapsible';
export * from './floating-action-button';
export * from './input'; export * from './input';
export * from './layout'; export * from './layout';
export * from './sheet'; export * from './sheet';

View File

@ -1,6 +1,7 @@
export * from './button'; export * from './button';
export * from './card'; export * from './card';
export * from './collapsible'; export * from './collapsible';
export * from './floating-action-button';
export * from './input'; export * from './input';
export * from './layout'; export * from './layout';
export * from './sheet'; export * from './sheet';

View File

@ -6,5 +6,6 @@ export * from './components/TimelineStep';
export * from './components/TimelineFeed'; export * from './components/TimelineFeed';
export * from './components/SessionDrawer'; export * from './components/SessionDrawer';
export * from './components/SessionSidebar'; export * from './components/SessionSidebar';
export * from './components/DefaultAgentScreen';
export declare const hello: () => void; export declare const hello: () => void;
export { getSampleAgentSteps, getSampleAgentSessions } from './utils/sample-data'; export { getSampleAgentSteps, getSampleAgentSessions } from './utils/sample-data';

View File

@ -12,6 +12,8 @@ export * from './components/TimelineFeed';
// Export session navigation components // Export session navigation components
export * from './components/SessionDrawer'; export * from './components/SessionDrawer';
export * from './components/SessionSidebar'; export * from './components/SessionSidebar';
// Export the main screen component
export * from './components/DefaultAgentScreen';
// Export the hello function (temporary example) // Export the hello function (temporary example)
export const hello = () => { export const hello = () => {
console.log("Hello from @ra-aid/common"); console.log("Hello from @ra-aid/common");

View File

@ -614,6 +614,9 @@ video {
.bottom-0 { .bottom-0 {
bottom: 0px; bottom: 0px;
} }
.bottom-6 {
bottom: 1.5rem;
}
.left-0 { .left-0 {
left: 0px; left: 0px;
} }
@ -623,6 +626,9 @@ video {
.right-4 { .right-4 {
right: 1rem; right: 1rem;
} }
.right-6 {
right: 1.5rem;
}
.top-0 { .top-0 {
top: 0px; top: 0px;
} }
@ -638,6 +644,9 @@ video {
.z-30 { .z-30 {
z-index: 30; z-index: 30;
} }
.z-50 {
z-index: 50;
}
.col-span-full { .col-span-full {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
@ -654,12 +663,18 @@ video {
.mb-2 { .mb-2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.mb-5 { .mb-5 {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
.mr-1 { .mr-1 {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.mr-2 {
margin-right: 0.5rem;
}
.mr-3 { .mr-3 {
margin-right: 0.75rem; margin-right: 0.75rem;
} }
@ -678,6 +693,9 @@ video {
.block { .block {
display: block; display: block;
} }
.inline-block {
display: inline-block;
}
.flex { .flex {
display: flex; display: flex;
} }
@ -693,6 +711,9 @@ video {
.h-10 { .h-10 {
height: 2.5rem; height: 2.5rem;
} }
.h-14 {
height: 3.5rem;
}
.h-16 { .h-16 {
height: 4rem; height: 4rem;
} }
@ -711,6 +732,9 @@ video {
.h-5 { .h-5 {
height: 1.25rem; height: 1.25rem;
} }
.h-6 {
height: 1.5rem;
}
.h-8 { .h-8 {
height: 2rem; height: 2rem;
} }
@ -729,6 +753,9 @@ video {
.min-h-screen { .min-h-screen {
min-height: 100vh; min-height: 100vh;
} }
.w-14 {
width: 3.5rem;
}
.w-2\.5 { .w-2\.5 {
width: 0.625rem; width: 0.625rem;
} }
@ -744,6 +771,9 @@ video {
.w-4 { .w-4 {
width: 1rem; width: 1rem;
} }
.w-6 {
width: 1.5rem;
}
.w-8 { .w-8 {
width: 2rem; width: 2rem;
} }
@ -809,6 +839,9 @@ video {
.justify-between { .justify-between {
justify-content: space-between; justify-content: space-between;
} }
.gap-2 {
gap: 0.5rem;
}
.gap-4 { .gap-4 {
gap: 1rem; gap: 1rem;
} }
@ -905,6 +938,10 @@ video {
.border-transparent { .border-transparent {
border-color: transparent; border-color: transparent;
} }
.border-white {
--tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
}
.border-l-transparent { .border-l-transparent {
border-left-color: transparent; border-left-color: transparent;
} }
@ -924,6 +961,10 @@ video {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
} }
.bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
}
.bg-border { .bg-border {
background-color: hsl(var(--border)); background-color: hsl(var(--border));
} }
@ -951,6 +992,10 @@ video {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1)); background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
} }
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
}
.bg-secondary { .bg-secondary {
background-color: hsl(var(--secondary)); background-color: hsl(var(--secondary));
} }
@ -961,6 +1006,21 @@ video {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(234 179 8 / var(--tw-bg-opacity, 1)); background-color: rgb(234 179 8 / var(--tw-bg-opacity, 1));
} }
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
.from-blue-400 {
--tw-gradient-from: #60a5fa var(--tw-gradient-from-position);
--tw-gradient-to: rgb(96 165 250 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-purple-500 {
--tw-gradient-to: #a855f7 var(--tw-gradient-to-position);
}
.bg-clip-text {
-webkit-background-clip: text;
background-clip: text;
}
.p-2 { .p-2 {
padding: 0.5rem; padding: 0.5rem;
} }
@ -1023,6 +1083,10 @@ video {
.text-center { .text-center {
text-align: center; text-align: center;
} }
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-lg { .text-lg {
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.75rem; line-height: 1.75rem;
@ -1031,10 +1095,17 @@ video {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
} }
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-xs { .text-xs {
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1rem; line-height: 1rem;
} }
.font-bold {
font-weight: 700;
}
.font-medium { .font-medium {
font-weight: 500; font-weight: 500;
} }
@ -1077,6 +1148,13 @@ video {
.text-secondary-foreground { .text-secondary-foreground {
color: hsl(var(--secondary-foreground)); color: hsl(var(--secondary-foreground));
} }
.text-transparent {
color: transparent;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.underline-offset-4 { .underline-offset-4 {
text-underline-offset: 4px; text-underline-offset: 4px;
} }
@ -1098,6 +1176,11 @@ video {
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.shadow-xl {
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.outline { .outline {
outline-style: solid; outline-style: solid;
} }
@ -1117,6 +1200,9 @@ video {
.ring-offset-background { .ring-offset-background {
--tw-ring-offset-color: hsl(var(--background)); --tw-ring-offset-color: hsl(var(--background));
} }
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.backdrop-blur-sm { .backdrop-blur-sm {
--tw-backdrop-blur: blur(4px); --tw-backdrop-blur: blur(4px);
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
@ -1203,12 +1289,20 @@ video {
.hover\:bg-accent\/50:hover { .hover\:bg-accent\/50:hover {
background-color: hsl(var(--accent) / 0.5); background-color: hsl(var(--accent) / 0.5);
} }
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
}
.hover\:bg-destructive\/90:hover { .hover\:bg-destructive\/90:hover {
background-color: hsl(var(--destructive) / 0.9); background-color: hsl(var(--destructive) / 0.9);
} }
.hover\:bg-primary\/90:hover { .hover\:bg-primary\/90:hover {
background-color: hsl(var(--primary) / 0.9); background-color: hsl(var(--primary) / 0.9);
} }
.hover\:bg-red-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity, 1));
}
.hover\:bg-secondary\/80:hover { .hover\:bg-secondary\/80:hover {
background-color: hsl(var(--secondary) / 0.8); background-color: hsl(var(--secondary) / 0.8);
} }
@ -1381,6 +1475,10 @@ video {
.data-\[state\=open\]\:duration-500[data-state="open"] { .data-\[state\=open\]\:duration-500[data-state="open"] {
animation-duration: 500ms; animation-duration: 500ms;
} }
.dark\:border-gray-800:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(31 41 55 / var(--tw-border-opacity, 1));
}
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:max-w-md { .sm\:max-w-md {
@ -1419,6 +1517,10 @@ video {
display: block; display: block;
} }
.md\:hidden {
display: none;
}
.md\:grid-cols-\[250px_1fr\] { .md\:grid-cols-\[250px_1fr\] {
grid-template-columns: 250px 1fr; grid-template-columns: 250px 1fr;
} }

View File

@ -0,0 +1,215 @@
import React, { useState, useEffect } from 'react';
import { PanelLeft } from 'lucide-react';
import {
Button,
Layout
} from './ui';
import { SessionDrawer } from './SessionDrawer';
import { SessionSidebar } from './SessionSidebar';
import { TimelineFeed } from './TimelineFeed';
import { getSampleAgentSessions, getSampleAgentSteps } from '../utils/sample-data';
/**
* DefaultAgentScreen component
*
* Main application screen for displaying agent sessions and their steps.
* Handles state management, responsive design, and UI interactions.
*/
export const DefaultAgentScreen: React.FC = () => {
// State for drawer open/close
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// State for selected session
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
// State for theme (dark is default)
const [isDarkTheme, setIsDarkTheme] = useState(true);
// Get sample data
const sessions = getSampleAgentSessions();
const allSteps = getSampleAgentSteps();
// Set up theme on component mount
useEffect(() => {
const isDark = setupTheme();
setIsDarkTheme(isDark);
}, []);
// Set initial selected session if none selected
useEffect(() => {
if (!selectedSessionId && sessions.length > 0) {
setSelectedSessionId(sessions[0].id);
}
}, [sessions, selectedSessionId]);
// Filter steps for selected session
const selectedSessionSteps = selectedSessionId
? allSteps.filter(step => sessions.find(s => s.id === selectedSessionId)?.steps.some(s => s.id === step.id))
: [];
// Handle session selection
const handleSessionSelect = (sessionId: string) => {
setSelectedSessionId(sessionId);
setIsDrawerOpen(false); // Close drawer on selection (mobile)
};
// Toggle theme function
const toggleTheme = () => {
const newIsDark = !isDarkTheme;
setIsDarkTheme(newIsDark);
// Update document element class
if (newIsDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Save to localStorage
localStorage.setItem('theme', newIsDark ? 'dark' : 'light');
};
// Render header content
const headerContent = (
<div className="flex justify-between items-center h-full px-4">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 inline-block text-transparent bg-clip-text">
RA-Aid
</h1>
<div className="flex items-center gap-2">
{/* Theme toggle button */}
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label={isDarkTheme ? "Switch to light mode" : "Switch to dark mode"}
className="mr-2"
>
{isDarkTheme ? (
// Sun icon for light mode toggle
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
) : (
// Moon icon for dark mode toggle
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
)}
</Button>
</div>
</div>
);
// Render sidebar content
const sidebarContent = (
<SessionSidebar
sessions={sessions}
currentSessionId={selectedSessionId || undefined}
onSelectSession={handleSessionSelect}
/>
);
// Render drawer
const drawerContent = (
<SessionDrawer
sessions={sessions}
currentSessionId={selectedSessionId || undefined}
onSelectSession={handleSessionSelect}
isOpen={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
/>
);
// Render main content
const mainContent = (
selectedSessionId ? (
<>
<h2 className="text-xl font-semibold mb-4">
Session: {sessions.find(s => s.id === selectedSessionId)?.name || 'Unknown'}
</h2>
<TimelineFeed
steps={selectedSessionSteps}
/>
</>
) : (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Select a session to view details</p>
</div>
)
);
// Create floating action button for mobile sidebar toggle
const floatingAction = (
<Button
variant="default"
size="icon"
onClick={() => setIsDrawerOpen(true)}
aria-label="Toggle sessions panel"
className="h-14 w-14 rounded-full shadow-xl bg-red-600 hover:bg-red-700 text-white flex items-center justify-center border-2 border-white dark:border-gray-800"
>
<PanelLeft className="h-6 w-6" />
</Button>
);
return (
<>
<Layout
header={headerContent}
sidebar={sidebarContent}
drawer={drawerContent}
>
{mainContent}
</Layout>
<div className="fixed bottom-6 right-6 z-50 md:hidden" style={{zIndex: 9999}}>
{floatingAction}
</div>
</>
);
};
// Helper function for theme setup
const setupTheme = () => {
// Check if theme preference is stored in localStorage
const storedTheme = localStorage.getItem('theme');
// Default to dark mode unless explicitly set to light
const isDark = storedTheme ? storedTheme === 'dark' : true;
// Apply theme to document
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
return isDark;
};

View File

@ -0,0 +1,36 @@
import React, { ReactNode } from 'react';
import { Button } from './button';
export interface FloatingActionButtonProps {
icon: ReactNode;
onClick: () => void;
ariaLabel?: string;
className?: string;
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
}
/**
* FloatingActionButton component
*
* A button typically used for primary actions on mobile layouts
* Designed to be used with the Layout component's floatingAction prop
*/
export const FloatingActionButton: React.FC<FloatingActionButtonProps> = ({
icon,
onClick,
ariaLabel = 'Action button',
className = '',
variant = 'default'
}) => {
return (
<Button
variant={variant}
size="icon"
onClick={onClick}
aria-label={ariaLabel}
className={`h-14 w-14 rounded-full shadow-xl bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center border-2 border-white dark:border-gray-800 ${className}`}
>
{icon}
</Button>
);
};

View File

@ -1,6 +1,7 @@
export * from './button'; export * from './button';
export * from './card'; export * from './card';
export * from './collapsible'; export * from './collapsible';
export * from './floating-action-button';
export * from './input'; export * from './input';
export * from './layout'; export * from './layout';
export * from './sheet'; export * from './sheet';

View File

@ -6,17 +6,25 @@ import React from 'react';
* - Sticky header at the top (z-index 30) * - Sticky header at the top (z-index 30)
* - Sidebar on desktop (hidden on mobile) * - Sidebar on desktop (hidden on mobile)
* - Main content area with proper positioning * - Main content area with proper positioning
* - Optional floating action button for mobile navigation
*/ */
export interface LayoutProps { export interface LayoutProps {
header: React.ReactNode; header: React.ReactNode;
sidebar?: React.ReactNode; sidebar?: React.ReactNode;
drawer?: React.ReactNode; drawer?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
floatingAction?: React.ReactNode;
} }
export const Layout: React.FC<LayoutProps> = ({ header, sidebar, drawer, children }) => { export const Layout: React.FC<LayoutProps> = ({
header,
sidebar,
drawer,
children,
floatingAction
}) => {
return ( return (
<div className="grid min-h-screen grid-cols-1 grid-rows-[64px_1fr] md:grid-cols-[250px_1fr] lg:grid-cols-[300px_1fr] bg-background text-foreground"> <div className="grid min-h-screen grid-cols-1 grid-rows-[64px_1fr] md:grid-cols-[250px_1fr] lg:grid-cols-[300px_1fr] bg-background text-foreground relative">
{/* Header - always visible, spans full width */} {/* Header - always visible, spans full width */}
<header className="sticky top-0 z-30 h-16 flex items-center bg-background border-b border-border col-span-full"> <header className="sticky top-0 z-30 h-16 flex items-center bg-background border-b border-border col-span-full">
{header} {header}
@ -36,6 +44,13 @@ export const Layout: React.FC<LayoutProps> = ({ header, sidebar, drawer, childre
{/* Mobile drawer - rendered outside grid */} {/* Mobile drawer - rendered outside grid */}
{drawer} {drawer}
{/* Floating action button for mobile */}
{floatingAction && (
<div className="fixed bottom-6 right-6 z-50 md:hidden">
{floatingAction}
</div>
)}
</div> </div>
); );
}; };

View File

@ -18,6 +18,9 @@ export * from './components/TimelineFeed';
export * from './components/SessionDrawer'; export * from './components/SessionDrawer';
export * from './components/SessionSidebar'; export * from './components/SessionSidebar';
// Export the main screen component
export * from './components/DefaultAgentScreen';
// Export the hello function (temporary example) // Export the hello function (temporary example)
export const hello = (): void => { export const hello = (): void => {
console.log("Hello from @ra-aid/common"); console.log("Hello from @ra-aid/common");

View File

@ -1,215 +1,16 @@
import React, { useState, useEffect } from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { import { DefaultAgentScreen } from '@ra-aid/common';
Button,
Layout,
SessionDrawer,
SessionSidebar,
TimelineFeed,
getSampleAgentSessions,
getSampleAgentSteps
} from '@ra-aid/common';
// The CSS import happens through the common package's index.ts
// Theme management helper function
const setupTheme = () => {
// Check if theme preference is stored in localStorage
const storedTheme = localStorage.getItem('theme');
// Default to dark mode unless explicitly set to light
const isDark = storedTheme ? storedTheme === 'dark' : true;
// Apply theme class to document element (html) for better CSS specificity
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Store the current theme preference
localStorage.setItem('theme', isDark ? 'dark' : 'light');
return isDark;
};
/**
* Main application entry point
* Simply renders the DefaultAgentScreen component from the common package
*/
const App = () => { const App = () => {
// State for drawer open/close return <DefaultAgentScreen />;
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// State for selected session
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
// State for theme (dark is default)
const [isDarkTheme, setIsDarkTheme] = useState(true);
// Get sample data
const sessions = getSampleAgentSessions();
const allSteps = getSampleAgentSteps();
// Set up theme on component mount
useEffect(() => {
const isDark = setupTheme();
setIsDarkTheme(isDark);
}, []);
// Set initial selected session if none selected
useEffect(() => {
if (!selectedSessionId && sessions.length > 0) {
setSelectedSessionId(sessions[0].id);
}
}, [sessions, selectedSessionId]);
// Filter steps for selected session
const selectedSessionSteps = selectedSessionId
? allSteps.filter(step => sessions.find(s => s.id === selectedSessionId)?.steps.some(s => s.id === step.id))
: [];
// Handle session selection
const handleSessionSelect = (sessionId: string) => {
setSelectedSessionId(sessionId);
setIsDrawerOpen(false); // Close drawer on selection (mobile)
};
// Toggle theme function
const toggleTheme = () => {
const newIsDark = !isDarkTheme;
setIsDarkTheme(newIsDark);
// Update document element class
if (newIsDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Save to localStorage
localStorage.setItem('theme', newIsDark ? 'dark' : 'light');
};
// Render header content
const headerContent = (
<div className="flex justify-between items-center h-full px-4">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 inline-block text-transparent bg-clip-text">
RA-Aid
</h1>
<div className="flex items-center gap-2">
{/* Theme toggle button */}
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label={isDarkTheme ? "Switch to light mode" : "Switch to dark mode"}
className="mr-2"
>
{isDarkTheme ? (
// Sun icon for light mode toggle
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
) : (
// Moon icon for dark mode toggle
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
)}
</Button>
{/* Mobile drawer toggle - show only on small screens */}
<div className="md:hidden">
<Button
variant="ghost"
size="icon"
onClick={() => setIsDrawerOpen(true)}
aria-label="Open menu"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
</Button>
</div>
</div>
</div>
);
// Render sidebar content
const sidebarContent = (
<SessionSidebar
sessions={sessions}
currentSessionId={selectedSessionId || undefined}
onSelectSession={handleSessionSelect}
/>
);
// Render drawer
const drawerContent = (
<SessionDrawer
sessions={sessions}
currentSessionId={selectedSessionId || undefined}
onSelectSession={handleSessionSelect}
isOpen={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
/>
);
// Render main content
const mainContent = (
selectedSessionId ? (
<>
<h2 className="text-xl font-semibold mb-4">
Session: {sessions.find(s => s.id === selectedSessionId)?.name || 'Unknown'}
</h2>
<TimelineFeed
steps={selectedSessionSteps}
/>
</>
) : (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Select a session to view details</p>
</div>
)
);
return (
<Layout
header={headerContent}
sidebar={sidebarContent}
drawer={drawerContent}
>
{mainContent}
</Layout>
);
}; };
// Initialize theme before rendering the app // Mount the app to the root element
setupTheme();
const root = ReactDOM.createRoot(document.getElementById('root')!); const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render( root.render(
<React.StrictMode> <React.StrictMode>