ui styling

This commit is contained in:
AI Christianson 2025-03-14 08:54:24 -04:00
parent 262c9f7d77
commit f29658fee8
8 changed files with 366 additions and 301 deletions

View File

@ -623,17 +623,24 @@ video {
.top-4 { .top-4 {
top: 1rem; top: 1rem;
} }
.z-40 {
z-index: 40;
}
.z-50 { .z-50 {
z-index: 50; z-index: 50;
} }
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mb-2 { .mb-2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.mb-4 { .mb-5 {
margin-bottom: 1rem; margin-bottom: 1.25rem;
} }
.mr-2 { .mr-1 {
margin-right: 0.5rem; margin-right: 0.25rem;
} }
.mr-3 { .mr-3 {
margin-right: 0.75rem; margin-right: 0.75rem;
@ -644,8 +651,8 @@ video {
.mt-1\.5 { .mt-1\.5 {
margin-top: 0.375rem; margin-top: 0.375rem;
} }
.mt-3 { .mt-4 {
margin-top: 0.75rem; margin-top: 1rem;
} }
.mt-6 { .mt-6 {
margin-top: 1.5rem; margin-top: 1.5rem;
@ -659,9 +666,6 @@ video {
.inline-flex { .inline-flex {
display: inline-flex; display: inline-flex;
} }
.hidden {
display: none;
}
.h-10 { .h-10 {
height: 2.5rem; height: 2.5rem;
} }
@ -671,6 +675,9 @@ video {
.h-3 { .h-3 {
height: 0.75rem; height: 0.75rem;
} }
.h-3\.5 {
height: 0.875rem;
}
.h-4 { .h-4 {
height: 1rem; height: 1rem;
} }
@ -689,30 +696,27 @@ video {
.h-full { .h-full {
height: 100%; height: 100%;
} }
.h-screen {
height: 100vh;
}
.w-2\.5 { .w-2\.5 {
width: 0.625rem; width: 0.625rem;
} }
.w-3 { .w-3 {
width: 0.75rem; width: 0.75rem;
} }
.w-3\.5 {
width: 0.875rem;
}
.w-3\/4 { .w-3\/4 {
width: 75%; width: 75%;
} }
.w-4 { .w-4 {
width: 1rem; width: 1rem;
} }
.w-5 { .w-8 {
width: 1.25rem; width: 2rem;
} }
.w-9 { .w-9 {
width: 2.25rem; width: 2.25rem;
} }
.w-\[250px\] {
width: 250px;
}
.w-\[85\%\] { .w-\[85\%\] {
width: 85%; width: 85%;
} }
@ -722,8 +726,8 @@ video {
.min-w-0 { .min-w-0 {
min-width: 0px; min-width: 0px;
} }
.max-w-xs { .max-w-md {
max-width: 20rem; max-width: 28rem;
} }
.flex-1 { .flex-1 {
flex: 1 1 0%; flex: 1 1 0%;
@ -766,12 +770,14 @@ 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;
} }
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) { .space-y-1\.5 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse))); margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse)));
@ -787,12 +793,17 @@ video {
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse)); margin-bottom: calc(1rem * var(--tw-space-y-reverse));
} }
.space-y-5 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1.25rem * var(--tw-space-y-reverse));
}
.overflow-auto {
overflow: auto;
}
.overflow-hidden { .overflow-hidden {
overflow: hidden; overflow: hidden;
} }
.overflow-x-auto {
overflow-x: auto;
}
.truncate { .truncate {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -804,9 +815,6 @@ video {
.whitespace-pre-wrap { .whitespace-pre-wrap {
white-space: pre-wrap; white-space: pre-wrap;
} }
.rounded {
border-radius: 0.25rem;
}
.rounded-\[inherit\] { .rounded-\[inherit\] {
border-radius: inherit; border-radius: inherit;
} }
@ -840,9 +848,15 @@ video {
.border-t { .border-t {
border-top-width: 1px; border-top-width: 1px;
} }
.border-dashed {
border-style: dashed;
}
.border-border { .border-border {
border-color: hsl(var(--border)); border-color: hsl(var(--border));
} }
.border-border\/50 {
border-color: hsl(var(--border) / 0.5);
}
.border-input { .border-input {
border-color: hsl(var(--input)); border-color: hsl(var(--input));
} }
@ -874,6 +888,9 @@ video {
.bg-card { .bg-card {
background-color: hsl(var(--card)); background-color: hsl(var(--card));
} }
.bg-card\/50 {
background-color: hsl(var(--card) / 0.5);
}
.bg-destructive { .bg-destructive {
background-color: hsl(var(--destructive)); background-color: hsl(var(--destructive));
} }
@ -908,6 +925,9 @@ video {
.p-4 { .p-4 {
padding: 1rem; padding: 1rem;
} }
.p-5 {
padding: 1.25rem;
}
.p-6 { .p-6 {
padding: 1.5rem; padding: 1.5rem;
} }
@ -930,21 +950,22 @@ video {
padding-left: 2rem; padding-left: 2rem;
padding-right: 2rem; padding-right: 2rem;
} }
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-1 { .py-1 {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
} }
.py-12 {
padding-top: 3rem;
padding-bottom: 3rem;
}
.py-2 { .py-2 {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.pt-0 { .pt-0 {
padding-top: 0px; padding-top: 0px;
} }
@ -981,6 +1002,9 @@ video {
.leading-none { .leading-none {
line-height: 1; line-height: 1;
} }
.leading-relaxed {
line-height: 1.625;
}
.tracking-tight { .tracking-tight {
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
@ -996,6 +1020,9 @@ video {
.text-muted-foreground { .text-muted-foreground {
color: hsl(var(--muted-foreground)); color: hsl(var(--muted-foreground));
} }
.text-muted-foreground\/50 {
color: hsl(var(--muted-foreground) / 0.5);
}
.text-primary { .text-primary {
color: hsl(var(--primary)); color: hsl(var(--primary));
} }
@ -1034,12 +1061,17 @@ video {
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
} }
.ring-1 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-ring\/20 {
--tw-ring-color: hsl(var(--ring) / 0.2);
}
.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);
@ -1120,6 +1152,9 @@ video {
.hover\:bg-accent:hover { .hover\:bg-accent:hover {
background-color: hsl(var(--accent)); background-color: hsl(var(--accent));
} }
.hover\:bg-accent\/30:hover {
background-color: hsl(var(--accent) / 0.3);
}
.hover\:bg-accent\/50:hover { .hover\:bg-accent\/50:hover {
background-color: hsl(var(--accent) / 0.5); background-color: hsl(var(--accent) / 0.5);
} }
@ -1141,6 +1176,11 @@ video {
.hover\:opacity-100:hover { .hover\:opacity-100:hover {
opacity: 1; opacity: 1;
} }
.hover\:shadow-md:hover {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.focus\:outline-none:focus { .focus\:outline-none:focus {
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
@ -1188,6 +1228,11 @@ video {
.disabled\:opacity-50:disabled { .disabled\:opacity-50:disabled {
opacity: 0.5; opacity: 0.5;
} }
.group:hover .group-hover\:scale-110 {
--tw-scale-x: 1.1;
--tw-scale-y: 1.1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.data-\[state\=checked\]\:translate-x-4[data-state="checked"] { .data-\[state\=checked\]\:translate-x-4[data-state="checked"] {
--tw-translate-x: 1rem; --tw-translate-x: 1rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@ -1319,19 +1364,3 @@ video {
text-align: left; text-align: left;
} }
} }
@media (min-width: 768px) {
.md\:block {
display: block;
}
.md\:hidden {
display: none;
}
}
@media (min-width: 1024px) {
.lg\:w-\[300px\] {
width: 300px;
}
}

View File

@ -1,14 +1,11 @@
import React from 'react'; import React from 'react';
import { Menu } from 'lucide-react';
import { import {
Sheet, Sheet,
SheetTrigger,
SheetContent, SheetContent,
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
SheetClose SheetClose
} from './ui/sheet'; } from './ui/sheet';
import { Button } from './ui/button';
import { ScrollArea } from './ui/scroll-area'; import { ScrollArea } from './ui/scroll-area';
import { AgentSession, getSampleAgentSessions } from '../utils/sample-data'; import { AgentSession, getSampleAgentSessions } from '../utils/sample-data';
@ -16,12 +13,16 @@ interface SessionDrawerProps {
onSelectSession?: (sessionId: string) => void; onSelectSession?: (sessionId: string) => void;
currentSessionId?: string; currentSessionId?: string;
sessions?: AgentSession[]; sessions?: AgentSession[];
isOpen?: boolean;
onClose?: () => void;
} }
export const SessionDrawer: React.FC<SessionDrawerProps> = ({ export const SessionDrawer: React.FC<SessionDrawerProps> = ({
onSelectSession, onSelectSession,
currentSessionId, currentSessionId,
sessions = getSampleAgentSessions() sessions = getSampleAgentSessions(),
isOpen = false,
onClose
}) => { }) => {
// Get status color // Get status color
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
@ -48,19 +49,16 @@ export const SessionDrawer: React.FC<SessionDrawerProps> = ({
}; };
return ( return (
<Sheet> <Sheet open={isOpen} onOpenChange={onClose}>
<SheetTrigger asChild> <SheetContent
<Button variant="ghost" size="icon" className="md:hidden"> side="left"
<Menu className="h-5 w-5" /> className="w-[85%] sm:max-w-md border-r border-border"
<span className="sr-only">Toggle navigation</span> >
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[85%] sm:max-w-md">
<SheetHeader> <SheetHeader>
<SheetTitle>Sessions</SheetTitle> <SheetTitle>Sessions</SheetTitle>
</SheetHeader> </SheetHeader>
<ScrollArea className="h-[calc(100vh-5rem)] mt-6"> <ScrollArea className="h-[calc(100vh-9rem)] mt-6">
<div className="space-y-4"> <div className="space-y-4 p-2">
{sessions.map((session) => ( {sessions.map((session) => (
<SheetClose key={session.id} asChild> <SheetClose key={session.id} asChild>
<button <button

View File

@ -40,11 +40,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}; };
return ( return (
<div className={`hidden md:block w-[250px] lg:w-[300px] h-screen border-r border-border ${className}`}> <div className={`flex flex-col h-full ${className}`}>
<div className="p-4 border-b border-border"> <div className="p-4 border-b border-border">
<h3 className="font-medium text-lg">Sessions</h3> <h3 className="font-medium text-lg">Sessions</h3>
</div> </div>
<ScrollArea className="h-[calc(100vh-5rem)]"> <ScrollArea className="flex-1">
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
{sessions.map((session) => ( {sessions.map((session) => (
<button <button

View File

@ -1,123 +1,45 @@
import React, { useState } from 'react'; import React, { useMemo } from 'react';
import { ScrollArea } from './ui/scroll-area';
import { TimelineStep } from './TimelineStep'; import { TimelineStep } from './TimelineStep';
import { AgentStep } from '../utils/sample-data'; import { AgentStep } from '../utils/sample-data';
interface TimelineFeedProps { interface TimelineFeedProps {
steps: AgentStep[]; steps: AgentStep[];
maxHeight?: string; maxHeight?: string;
filter?: {
types?: string[];
status?: string[];
};
sortOrder?: 'asc' | 'desc';
} }
export const TimelineFeed: React.FC<TimelineFeedProps> = ({ export const TimelineFeed: React.FC<TimelineFeedProps> = ({
steps, steps,
maxHeight = '500px', maxHeight
filter,
sortOrder = 'desc'
}) => { }) => {
// State for filtered and sorted steps // Always use 'desc' (newest first) sort order
const [activeFilter, setActiveFilter] = useState(filter); const sortOrder = 'desc';
const [activeSortOrder, setActiveSortOrder] = useState<'asc' | 'desc'>(sortOrder);
// Apply filters and sorting // Sort steps with newest first (desc order)
const filteredSteps = steps.filter(step => { const sortedSteps = useMemo(() => {
if (!activeFilter) return true; return [...steps].sort((a, b) => {
const typeMatch = !activeFilter.types || activeFilter.types.length === 0 ||
activeFilter.types.includes(step.type);
const statusMatch = !activeFilter.status || activeFilter.status.length === 0 ||
activeFilter.status.includes(step.status);
return typeMatch && statusMatch;
});
// Sort steps
const sortedSteps = [...filteredSteps].sort((a, b) => {
if (activeSortOrder === 'asc') {
return a.timestamp.getTime() - b.timestamp.getTime();
} else {
return b.timestamp.getTime() - a.timestamp.getTime(); return b.timestamp.getTime() - a.timestamp.getTime();
}
}); });
}, [steps]);
// Toggle sort order
const toggleSortOrder = () => {
setActiveSortOrder(prevOrder => prevOrder === 'asc' ? 'desc' : 'asc');
};
// Filter by type
const filterTypes = [
'all',
'tool-execution',
'thinking',
'planning',
'implementation',
'user-input'
];
const handleFilterChange = (type: string) => {
if (type === 'all') {
setActiveFilter({
...activeFilter,
types: []
});
} else {
setActiveFilter({
...activeFilter,
types: [type]
});
}
};
return ( return (
<div className="w-full border border-border rounded-md bg-background"> <div className="w-full rounded-md bg-background">
<div className="p-3 border-b border-border"> <div
<div className="flex justify-between items-center mb-2"> className="p-4 space-y-5 overflow-auto"
<h3 className="font-medium">Timeline Feed</h3> style={{ maxHeight: maxHeight || undefined }}
<button
onClick={toggleSortOrder}
className="text-xs bg-secondary hover:bg-secondary/80 text-secondary-foreground px-2 py-1 rounded"
> >
{activeSortOrder === 'asc' ? '⬆️ Oldest first' : '⬇️ Newest first'}
</button>
</div>
<div className="flex gap-2 overflow-x-auto pb-2 text-xs">
{filterTypes.map(type => (
<button
key={type}
onClick={() => handleFilterChange(type)}
className={`px-2 py-1 rounded whitespace-nowrap ${
type === 'all' && (!activeFilter?.types || activeFilter.types.length === 0) ||
activeFilter?.types?.includes(type)
? 'bg-primary text-primary-foreground'
: 'bg-secondary/50 text-secondary-foreground hover:bg-secondary/80'
}`}
>
{type === 'all' ? 'All types' : type}
</button>
))}
</div>
</div>
<ScrollArea className="h-full" style={{ maxHeight }}>
<div className="p-3">
{sortedSteps.length > 0 ? ( {sortedSteps.length > 0 ? (
sortedSteps.map((step) => ( sortedSteps.map((step) => (
<TimelineStep key={step.id} step={step} /> <TimelineStep key={step.id} step={step} />
)) ))
) : ( ) : (
<div className="text-center text-muted-foreground py-8"> <div className="text-center text-muted-foreground py-12 border border-dashed border-border rounded-md">
No steps to display <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 mx-auto mb-2 text-muted-foreground/50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p>No steps to display</p>
</div> </div>
)} )}
</div> </div>
</ScrollArea>
</div> </div>
); );
}; }

View File

@ -47,34 +47,47 @@ export const TimelineStep: React.FC<TimelineStepProps> = ({ step }) => {
}; };
return ( return (
<Collapsible className="w-full mb-4 border border-border rounded-md overflow-hidden transition-all duration-200"> <Collapsible className="w-full mb-5 border border-border rounded-md overflow-hidden shadow-sm hover:shadow-md transition-all duration-200">
<CollapsibleTrigger className="w-full flex items-center justify-between p-3 text-left hover:bg-accent/50 cursor-pointer"> <CollapsibleTrigger className="w-full flex items-center justify-between p-4 text-left hover:bg-accent/30 cursor-pointer group">
<div className="flex items-center"> <div className="flex items-center space-x-3">
<div className={`w-3 h-3 rounded-full ${getStatusColor(step.status)} mr-3`} /> <div className={`w-3 h-3 rounded-full ${getStatusColor(step.status)} ring-1 ring-ring/20`} />
<div className="mr-2">{getTypeIcon(step.type)}</div> <div className="text-lg group-hover:scale-110 transition-transform">{getTypeIcon(step.type)}</div>
<div> <div>
<div className="font-medium">{step.title}</div> <div className="font-medium text-foreground">{step.title}</div>
<div className="text-sm text-muted-foreground truncate max-w-xs"> <div className="text-sm text-muted-foreground truncate max-w-md">
{step.type === 'tool-execution' ? 'Run tool' : step.content.substring(0, 60)} {step.type === 'tool-execution' ? 'Run tool' : step.content.substring(0, 60)}
{step.content.length > 60 ? '...' : ''} {step.content.length > 60 ? '...' : ''}
</div> </div>
</div> </div>
</div> </div>
<div className="text-xs text-muted-foreground flex flex-col items-end"> <div className="text-xs text-muted-foreground flex flex-col items-end">
<span>{formatTime(step.timestamp)}</span> <span className="font-medium">{formatTime(step.timestamp)}</span>
{step.duration && ( {step.duration && (
<span className="mt-1">{(step.duration / 1000).toFixed(1)}s</span> <span className="mt-1 px-2 py-0.5 bg-secondary/50 rounded-full">
{(step.duration / 1000).toFixed(1)}s
</span>
)} )}
</div> </div>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="p-4 bg-card border-t border-border"> <div className="p-5 bg-card/50 border-t border-border">
<div className="text-sm whitespace-pre-wrap"> <div className="text-sm whitespace-pre-wrap text-foreground leading-relaxed">
{step.content} {step.content}
</div> </div>
{step.duration && ( {step.duration && (
<div className="mt-3 pt-3 border-t border-border"> <div className="mt-4 pt-3 border-t border-border/50">
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-3.5 w-3.5 mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
Duration: {(step.duration / 1000).toFixed(1)} seconds Duration: {(step.duration / 1000).toFixed(1)} seconds
</div> </div>
</div> </div>

View File

@ -19,7 +19,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-70 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className
)} )}
{...props} {...props}
@ -29,7 +29,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva( const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "fixed z-70 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{ {
variants: { variants: {
side: { side: {

View File

@ -0,0 +1,99 @@
import React, { ReactNode } from 'react';
interface LayoutProps {
header: ReactNode;
sidebar?: ReactNode;
drawer?: ReactNode;
children: ReactNode;
}
/**
* Layout component using CSS Grid with named areas
* This component creates a responsive layout with:
* - Sticky header at the top (z-index 30)
* - Sidebar on desktop (hidden on mobile)
* - Main content area with proper positioning
*/
export const Layout: React.FC<LayoutProps> = ({ header, sidebar, drawer, children }) => {
return (
<>
<style>{`
.layout-grid {
display: grid;
min-height: 100vh;
grid-template-areas:
"header"
"main";
grid-template-rows: 64px 1fr;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.layout-grid {
grid-template-areas:
"header header"
"sidebar main";
grid-template-columns: 250px 1fr;
grid-template-rows: 64px 1fr;
}
}
@media (min-width: 1024px) {
.layout-grid {
grid-template-columns: 300px 1fr;
}
}
.layout-header {
grid-area: header;
position: sticky;
top: 0;
z-index: 30;
height: 64px;
display: flex;
align-items: center;
}
.layout-sidebar {
grid-area: sidebar;
position: sticky;
top: 64px;
height: calc(100vh - 64px);
overflow-y: auto;
z-index: 20;
display: none;
}
@media (min-width: 768px) {
.layout-sidebar {
display: block;
}
}
.layout-main {
grid-area: main;
overflow-y: auto;
}
`}</style>
<div className="layout-grid bg-background text-foreground">
<header className="layout-header bg-background border-b border-border">
{header}
</header>
{sidebar && (
<aside className="layout-sidebar bg-background border-r border-border">
{sidebar}
</aside>
)}
{/* Mobile drawer - rendered outside grid */}
{drawer}
<main className="layout-main p-4">
{children}
</main>
</div>
</>
);
};

View File

@ -8,6 +8,7 @@ import {
getSampleAgentSessions, getSampleAgentSessions,
getSampleAgentSteps getSampleAgentSteps
} from '@ra-aid/common'; } from '@ra-aid/common';
import { Layout } from './components/Layout';
// The CSS import happens through the common package's index.ts // The CSS import happens through the common package's index.ts
// Theme management helper function // Theme management helper function
@ -85,11 +86,9 @@ const App = () => {
localStorage.setItem('theme', newIsDark ? 'dark' : 'light'); localStorage.setItem('theme', newIsDark ? 'dark' : 'light');
}; };
return ( // Render header content
<div className="min-h-screen bg-background text-foreground flex flex-col"> const headerContent = (
{/* Header */} <div className="flex justify-between items-center h-full px-4">
<header className="border-b border-border py-4 px-4 sticky top-0 z-10 bg-background">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 inline-block text-transparent bg-clip-text"> <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 RA-Aid
</h1> </h1>
@ -157,49 +156,54 @@ const App = () => {
</div> </div>
</div> </div>
</div> </div>
</header> );
{/* Main content */} // Render sidebar content
<div className="flex flex-1 overflow-hidden"> const sidebarContent = (
{/* Desktop sidebar - hidden on mobile */}
<SessionSidebar <SessionSidebar
sessions={sessions} sessions={sessions}
currentSessionId={selectedSessionId || undefined} currentSessionId={selectedSessionId || undefined}
onSelectSession={handleSessionSelect} onSelectSession={handleSessionSelect}
className="shrink-0"
/> />
);
{/* Mobile drawer */} // Render drawer
const drawerContent = (
<SessionDrawer <SessionDrawer
sessions={sessions} sessions={sessions}
currentSessionId={selectedSessionId || undefined} currentSessionId={selectedSessionId || undefined}
onSelectSession={handleSessionSelect} onSelectSession={handleSessionSelect}
isOpen={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
/> />
);
{/* Main content area */} // Render main content
<main className="flex-1 overflow-auto p-4"> const mainContent = (
{selectedSessionId ? ( selectedSessionId ? (
<> <>
<h2 className="text-xl font-semibold mb-4"> <h2 className="text-xl font-semibold mb-4">
Session: {sessions.find(s => s.id === selectedSessionId)?.name || 'Unknown'} Session: {sessions.find(s => s.id === selectedSessionId)?.name || 'Unknown'}
</h2> </h2>
<TimelineFeed <TimelineFeed
steps={selectedSessionSteps} steps={selectedSessionSteps}
maxHeight="calc(100vh - 14rem)"
/> />
</> </>
) : ( ) : (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Select a session to view details</p> <p className="text-muted-foreground">Select a session to view details</p>
</div> </div>
)} )
</main> );
</div>
<footer className="border-t border-border py-4 px-4 text-center text-muted-foreground text-sm"> return (
<p>Built with shadcn/ui components from the RA-Aid common package</p> <Layout
</footer> header={headerContent}
</div> sidebar={sidebarContent}
drawer={drawerContent}
>
{mainContent}
</Layout>
); );
}; };