style: enhance UI components with improved styling and layout

- Updated global CSS to include new status colors for better visual feedback.
- Refined button, badge, card, and input components with enhanced styles and transitions for a more polished user experience.
- Adjusted sidebar and dialog components for improved aesthetics and usability.
- Implemented gradient backgrounds and shadow effects across various sections to elevate the overall design.
- Enhanced keyboard shortcuts and settings views with consistent styling and layout adjustments for better accessibility.
This commit is contained in:
SuperComboGamer
2025-12-14 19:21:20 -05:00
parent f6c50ce336
commit e378704c63
23 changed files with 1312 additions and 885 deletions

View File

@@ -79,6 +79,19 @@
--color-running-indicator: var(--running-indicator); --color-running-indicator: var(--running-indicator);
--color-running-indicator-text: var(--running-indicator-text); --color-running-indicator-text: var(--running-indicator-text);
/* Status colors */
--color-status-success: var(--status-success);
--color-status-success-bg: var(--status-success-bg);
--color-status-warning: var(--status-warning);
--color-status-warning-bg: var(--status-warning-bg);
--color-status-error: var(--status-error);
--color-status-error-bg: var(--status-error-bg);
--color-status-info: var(--status-info);
--color-status-info-bg: var(--status-info-bg);
--color-status-backlog: var(--status-backlog);
--color-status-in-progress: var(--status-in-progress);
--color-status-waiting: var(--status-waiting);
/* Border radius */ /* Border radius */
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
@@ -142,6 +155,31 @@
/* Running indicator - Purple */ /* Running indicator - Purple */
--running-indicator: oklch(0.55 0.25 265); --running-indicator: oklch(0.55 0.25 265);
--running-indicator-text: oklch(0.6 0.22 265); --running-indicator-text: oklch(0.6 0.22 265);
/* Status colors - Light mode */
--status-success: oklch(0.55 0.2 140);
--status-success-bg: oklch(0.55 0.2 140 / 0.15);
--status-warning: oklch(0.7 0.15 70);
--status-warning-bg: oklch(0.7 0.15 70 / 0.15);
--status-error: oklch(0.55 0.22 25);
--status-error-bg: oklch(0.55 0.22 25 / 0.15);
--status-info: oklch(0.55 0.2 230);
--status-info-bg: oklch(0.55 0.2 230 / 0.15);
--status-backlog: oklch(0.5 0 0);
--status-in-progress: oklch(0.7 0.15 70);
--status-waiting: oklch(0.65 0.18 50);
/* Shadow tokens */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* Transition tokens */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
--transition-slow: 300ms ease-out;
} }
/* Apply dark mode immediately based on system preference (before JS runs) */ /* Apply dark mode immediately based on system preference (before JS runs) */
@@ -215,6 +253,26 @@
/* Running indicator - Purple */ /* Running indicator - Purple */
--running-indicator: oklch(0.6 0.25 265); --running-indicator: oklch(0.6 0.25 265);
--running-indicator-text: oklch(0.65 0.22 265); --running-indicator-text: oklch(0.65 0.22 265);
/* Status colors - Dark mode */
--status-success: oklch(0.65 0.2 140);
--status-success-bg: oklch(0.65 0.2 140 / 0.2);
--status-warning: oklch(0.75 0.15 70);
--status-warning-bg: oklch(0.75 0.15 70 / 0.2);
--status-error: oklch(0.65 0.22 25);
--status-error-bg: oklch(0.65 0.22 25 / 0.2);
--status-info: oklch(0.65 0.2 230);
--status-info-bg: oklch(0.65 0.2 230 / 0.2);
--status-backlog: oklch(0.6 0 0);
--status-in-progress: oklch(0.75 0.15 70);
--status-waiting: oklch(0.7 0.18 50);
/* Shadow tokens - darker for dark mode */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.3);
} }
} }
@@ -344,6 +402,26 @@
/* Running indicator - Purple */ /* Running indicator - Purple */
--running-indicator: oklch(0.6 0.25 265); --running-indicator: oklch(0.6 0.25 265);
--running-indicator-text: oklch(0.65 0.22 265); --running-indicator-text: oklch(0.65 0.22 265);
/* Status colors - Dark mode */
--status-success: oklch(0.65 0.2 140);
--status-success-bg: oklch(0.65 0.2 140 / 0.2);
--status-warning: oklch(0.75 0.15 70);
--status-warning-bg: oklch(0.75 0.15 70 / 0.2);
--status-error: oklch(0.65 0.22 25);
--status-error-bg: oklch(0.65 0.22 25 / 0.2);
--status-info: oklch(0.65 0.2 230);
--status-info-bg: oklch(0.65 0.2 230 / 0.2);
--status-backlog: oklch(0.6 0 0);
--status-in-progress: oklch(0.75 0.15 70);
--status-waiting: oklch(0.7 0.18 50);
/* Shadow tokens - darker for dark mode */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.3);
} }
.retro { .retro {

View File

@@ -146,9 +146,10 @@ function SortableProjectItem({
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className={cn( className={cn(
"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer text-muted-foreground hover:text-foreground hover:bg-accent", "flex items-center gap-2 px-2.5 py-2 rounded-lg cursor-pointer transition-all duration-200",
isDragging && "bg-accent shadow-lg", "text-muted-foreground hover:text-foreground hover:bg-accent/80",
isHighlighted && "bg-brand-500/10 text-foreground" isDragging && "bg-accent shadow-lg scale-[1.02]",
isHighlighted && "bg-brand-500/10 text-foreground ring-1 ring-brand-500/20"
)} )}
data-testid={`project-option-${project.id}`} data-testid={`project-option-${project.id}`}
> >
@@ -156,20 +157,20 @@ function SortableProjectItem({
<button <button
{...attributes} {...attributes}
{...listeners} {...listeners}
className="p-0.5 rounded hover:bg-sidebar-accent/20 cursor-grab active:cursor-grabbing" className="p-0.5 rounded-md hover:bg-accent/50 cursor-grab active:cursor-grabbing transition-colors"
data-testid={`project-drag-handle-${project.id}`} data-testid={`project-drag-handle-${project.id}`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<GripVertical className="h-3.5 w-3.5 text-muted-foreground" /> <GripVertical className="h-3.5 w-3.5 text-muted-foreground/60" />
</button> </button>
{/* Project content - clickable area */} {/* Project content - clickable area */}
<div <div
className="flex items-center gap-2 flex-1 min-w-0" className="flex items-center gap-2.5 flex-1 min-w-0"
onClick={() => onSelect(project)} onClick={() => onSelect(project)}
> >
<Folder className="h-4 w-4 shrink-0" /> <Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate text-sm">{project.name}</span> <span className="flex-1 truncate text-sm font-medium">{project.name}</span>
{currentProjectId === project.id && ( {currentProjectId === project.id && (
<Check className="h-4 w-4 text-brand-500 shrink-0" /> <Check className="h-4 w-4 text-brand-500 shrink-0" />
)} )}
@@ -1161,7 +1162,13 @@ export function Sidebar() {
return ( return (
<aside <aside
className={cn( className={cn(
"flex-shrink-0 border-r border-sidebar-border bg-sidebar backdrop-blur-md flex flex-col z-30 transition-all duration-300 relative", "flex-shrink-0 flex flex-col z-30 relative",
// Glass morphism background with gradient
"bg-gradient-to-b from-sidebar/95 via-sidebar/85 to-sidebar/90 backdrop-blur-2xl",
// Premium border with subtle glow
"border-r border-border/60 shadow-[1px_0_20px_-5px_rgba(0,0,0,0.1)]",
// Smooth width transition
"transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]",
sidebarOpen ? "w-16 lg:w-72" : "w-16" sidebarOpen ? "w-16 lg:w-72" : "w-16"
)} )}
data-testid="sidebar" data-testid="sidebar"
@@ -1169,22 +1176,40 @@ export function Sidebar() {
{/* Floating Collapse Toggle Button - Desktop only - At border intersection */} {/* Floating Collapse Toggle Button - Desktop only - At border intersection */}
<button <button
onClick={toggleSidebar} onClick={toggleSidebar}
className="hidden lg:flex absolute top-[68px] -right-3 z-9999 group/toggle items-center justify-center w-6 h-6 rounded-full bg-sidebar-accent border border-border text-muted-foreground hover:text-foreground hover:bg-accent hover:border-border transition-all shadow-lg titlebar-no-drag" className={cn(
"hidden lg:flex absolute top-[68px] -right-3 z-9999",
"group/toggle items-center justify-center w-7 h-7 rounded-full",
// Glass morphism button
"bg-card/95 backdrop-blur-sm border border-border/80",
// Premium shadow with glow on hover
"shadow-lg shadow-black/5 hover:shadow-xl hover:shadow-brand-500/10",
"text-muted-foreground hover:text-brand-500 hover:bg-accent/80",
"hover:border-brand-500/30",
"transition-all duration-200 ease-out titlebar-no-drag",
"hover:scale-110 active:scale-90"
)}
data-testid="sidebar-collapse-button" data-testid="sidebar-collapse-button"
> >
{sidebarOpen ? ( {sidebarOpen ? (
<PanelLeftClose className="w-3.5 h-3.5 pointer-events-none" /> <PanelLeftClose className="w-3.5 h-3.5 pointer-events-none transition-transform duration-200" />
) : ( ) : (
<PanelLeft className="w-3.5 h-3.5 pointer-events-none" /> <PanelLeft className="w-3.5 h-3.5 pointer-events-none transition-transform duration-200" />
)} )}
{/* Tooltip */} {/* Tooltip */}
<div <div
className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover/toggle:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border pointer-events-none" className={cn(
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
"bg-popover text-popover-foreground text-xs font-medium",
"border border-border shadow-lg",
"opacity-0 group-hover/toggle:opacity-100 transition-all duration-200",
"whitespace-nowrap z-50 pointer-events-none",
"translate-x-1 group-hover/toggle:translate-x-0"
)}
data-testid="sidebar-toggle-tooltip" data-testid="sidebar-toggle-tooltip"
> >
{sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}{" "} {sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}{" "}
<span <span
className="ml-1 px-1 py-0.5 bg-brand-500/10 border border-brand-500/30 rounded text-[10px] font-mono text-brand-400/70" className="ml-1.5 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground"
data-testid="sidebar-toggle-shortcut" data-testid="sidebar-toggle-shortcut"
> >
{formatShortcut(shortcuts.toggleSidebar, true)} {formatShortcut(shortcuts.toggleSidebar, true)}
@@ -1196,9 +1221,13 @@ export function Sidebar() {
{/* Logo */} {/* Logo */}
<div <div
className={cn( className={cn(
"h-20 border-b border-sidebar-border shrink-0 titlebar-drag-region", "h-20 shrink-0 titlebar-drag-region",
// Subtle bottom border with gradient fade
"border-b border-border/40",
// Background gradient for depth
"bg-gradient-to-b from-transparent to-background/5",
sidebarOpen sidebarOpen
? "pt-8 px-3 lg:px-6 flex items-center justify-between" ? "pt-8 px-3 lg:px-5 flex items-center justify-between"
: "pt-2 pb-2 px-3 flex flex-col items-center justify-center gap-2" : "pt-2 pb-2 px-3 flex flex-col items-center justify-center gap-2"
)} )}
> >
@@ -1215,20 +1244,20 @@ export function Sidebar() {
<img <img
src="/logo.png" src="/logo.png"
alt="Automaker Logo" alt="Automaker Logo"
className="size-8 group-hover:rotate-12 transition-transform" className="size-8 group-hover:rotate-12 transition-transform duration-300 ease-out"
/> />
</div> </div>
) : ( ) : (
<span <span
className={cn( className={cn(
"flex items-center font-bold text-sidebar-foreground text-base tracking-tight", "flex items-center font-bold text-foreground text-base tracking-tight",
"hidden lg:flex" "hidden lg:flex"
)} )}
> >
<img <img
src="/logo.png" src="/logo.png"
alt="A" alt="A"
className="h-[1.8em] w-auto inline-block align-middle group-hover:rotate-12 transition-transform" className="h-[1.8em] w-auto inline-block align-middle group-hover:rotate-12 transition-transform duration-300 ease-out"
/> />
<span className="-ml-0.5"> <span className="-ml-0.5">
uto<span className="text-brand-500">maker</span> uto<span className="text-brand-500">maker</span>
@@ -1244,7 +1273,12 @@ export function Sidebar() {
"https://github.com/AutoMaker-Org/automaker/issues" "https://github.com/AutoMaker-Org/automaker/issues"
); );
}} }}
className="titlebar-no-drag p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 transition-all" className={cn(
"titlebar-no-drag p-1.5 rounded-lg",
"text-muted-foreground hover:text-foreground hover:bg-accent/80",
"transition-all duration-200 ease-out",
"hover:scale-105 active:scale-95"
)}
title="Report Bug / Feature Request" title="Report Bug / Feature Request"
data-testid="bug-report-link" data-testid="bug-report-link"
> >
@@ -1254,38 +1288,69 @@ export function Sidebar() {
{/* Project Actions - Moved above project selector */} {/* Project Actions - Moved above project selector */}
{sidebarOpen && ( {sidebarOpen && (
<div className="flex items-center gap-2 titlebar-no-drag px-2 mt-3"> <div className="flex items-center gap-2.5 titlebar-no-drag px-3 mt-4">
<button <button
onClick={() => setShowNewProjectModal(true)} onClick={() => setShowNewProjectModal(true)}
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border" className={cn(
"group flex items-center justify-center flex-1 px-3 py-2.5 rounded-xl",
"relative overflow-hidden",
"text-muted-foreground hover:text-foreground",
// Glass background with gradient on hover
"bg-accent/20 hover:bg-gradient-to-br hover:from-brand-500/15 hover:to-brand-600/10",
"border border-border/40 hover:border-brand-500/30",
// Premium shadow
"shadow-sm hover:shadow-md hover:shadow-brand-500/5",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.97]"
)}
title="New Project" title="New Project"
data-testid="new-project-button" data-testid="new-project-button"
> >
<Plus className="w-4 h-4 shrink-0" /> <Plus className="w-4 h-4 shrink-0 transition-transform duration-200 group-hover:rotate-90 group-hover:text-brand-500" />
<span className="ml-2 text-sm font-medium hidden lg:block whitespace-nowrap"> <span className="ml-2 text-sm font-medium hidden lg:block whitespace-nowrap">
New New
</span> </span>
</button> </button>
<button <button
onClick={handleOpenFolder} onClick={handleOpenFolder}
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border" className={cn(
"group flex items-center justify-center flex-1 px-3 py-2.5 rounded-xl",
"relative overflow-hidden",
"text-muted-foreground hover:text-foreground",
// Glass background
"bg-accent/20 hover:bg-accent/40",
"border border-border/40 hover:border-border/60",
"shadow-sm hover:shadow-md",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.97]"
)}
title={`Open Folder (${shortcuts.openProject})`} title={`Open Folder (${shortcuts.openProject})`}
data-testid="open-project-button" data-testid="open-project-button"
> >
<FolderOpen className="w-4 h-4 shrink-0" /> <FolderOpen className="w-4 h-4 shrink-0 transition-transform duration-200 group-hover:scale-110" />
<span className="hidden lg:flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70 ml-2"> <span className="hidden lg:flex items-center justify-center min-w-5 h-5 px-1.5 text-[10px] font-mono rounded-md bg-muted/80 text-muted-foreground ml-2">
{formatShortcut(shortcuts.openProject, true)} {formatShortcut(shortcuts.openProject, true)}
</span> </span>
</button> </button>
<button <button
onClick={() => setShowTrashDialog(true)} onClick={() => setShowTrashDialog(true)}
className="group flex items-center justify-center px-3 h-[42px] rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-primary hover:bg-destructive/10 border border-sidebar-border" className={cn(
"group flex items-center justify-center px-3 h-[42px] rounded-xl",
"relative overflow-hidden",
"text-muted-foreground hover:text-destructive",
// Subtle background that turns red on hover
"bg-accent/20 hover:bg-destructive/15",
"border border-border/40 hover:border-destructive/40",
"shadow-sm hover:shadow-md hover:shadow-destructive/10",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.97]"
)}
title="Recycle Bin" title="Recycle Bin"
data-testid="trash-button" data-testid="trash-button"
> >
<Recycle className="size-4 shrink-0" /> <Recycle className="size-4 shrink-0 transition-transform duration-200 group-hover:rotate-12" />
{trashedProjects.length > 0 && ( {trashedProjects.length > 0 && (
<span className="absolute -top-[2px] -right-[2px] flex items-center justify-center w-5 h-5 text-[10px] font-medium rounded-full text-brand-500"> <span className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-4 h-4 px-1 text-[9px] font-bold rounded-full bg-destructive text-destructive-foreground shadow-sm">
{trashedProjects.length > 9 ? "9+" : trashedProjects.length} {trashedProjects.length > 9 ? "9+" : trashedProjects.length}
</span> </span>
)} )}
@@ -1295,56 +1360,77 @@ export function Sidebar() {
{/* Project Selector with Cycle Buttons */} {/* Project Selector with Cycle Buttons */}
{sidebarOpen && projects.length > 0 && ( {sidebarOpen && projects.length > 0 && (
<div className="px-2 mt-3 flex items-center gap-1.5"> <div className="px-3 mt-3 flex items-center gap-2.5">
<DropdownMenu <DropdownMenu
open={isProjectPickerOpen} open={isProjectPickerOpen}
onOpenChange={setIsProjectPickerOpen} onOpenChange={setIsProjectPickerOpen}
> >
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
className="flex-1 flex items-center justify-between px-3 py-2.5 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-all text-foreground titlebar-no-drag min-w-0" className={cn(
"flex-1 flex items-center justify-between px-3.5 py-3 rounded-xl",
// Premium glass background
"bg-gradient-to-br from-accent/40 to-accent/20",
"hover:from-accent/50 hover:to-accent/30",
"border border-border/50 hover:border-border/70",
// Subtle inner shadow
"shadow-sm shadow-black/5",
"text-foreground titlebar-no-drag min-w-0",
"transition-all duration-200 ease-out",
"hover:scale-[1.01] active:scale-[0.99]",
isProjectPickerOpen && "from-brand-500/10 to-brand-600/5 border-brand-500/30 ring-2 ring-brand-500/20 shadow-lg shadow-brand-500/5"
)}
data-testid="project-selector" data-testid="project-selector"
> >
<div className="flex items-center gap-2 flex-1 min-w-0"> <div className="flex items-center gap-2.5 flex-1 min-w-0">
<Folder className="h-4 w-4 text-brand-500 shrink-0" /> <Folder className="h-4 w-4 text-brand-500 shrink-0" />
<span className="text-sm font-medium truncate"> <span className="text-sm font-medium truncate">
{currentProject?.name || "Select Project"} {currentProject?.name || "Select Project"}
</span> </span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1.5">
<span <span
className="hidden lg:flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70" className="hidden lg:flex items-center justify-center min-w-5 h-5 px-1.5 text-[10px] font-mono rounded-md bg-muted text-muted-foreground"
data-testid="project-picker-shortcut" data-testid="project-picker-shortcut"
> >
{formatShortcut(shortcuts.projectPicker, true)} {formatShortcut(shortcuts.projectPicker, true)}
</span> </span>
<ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" /> <ChevronDown className={cn(
"h-4 w-4 text-muted-foreground shrink-0 transition-transform duration-200",
isProjectPickerOpen && "rotate-180"
)} />
</div> </div>
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
className="w-64 bg-popover border-border p-1" className="w-72 bg-popover/95 backdrop-blur-xl border-border shadow-xl p-1.5"
align="start" align="start"
data-testid="project-picker-dropdown" data-testid="project-picker-dropdown"
> >
{/* Search input for type-ahead filtering */} {/* Search input for type-ahead filtering */}
<div className="px-2 pb-2"> <div className="px-1 pb-2">
<div className="relative"> <div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<input <input
ref={projectSearchInputRef} ref={projectSearchInputRef}
type="text" type="text"
placeholder="Search projects..." placeholder="Search projects..."
value={projectSearchQuery} value={projectSearchQuery}
onChange={(e) => setProjectSearchQuery(e.target.value)} onChange={(e) => setProjectSearchQuery(e.target.value)}
className="w-full h-8 pl-7 pr-2 text-sm rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0" className={cn(
"w-full h-9 pl-8 pr-3 text-sm rounded-lg",
"border border-border bg-background/50",
"text-foreground placeholder:text-muted-foreground",
"focus:outline-none focus:ring-2 focus:ring-brand-500/30 focus:border-brand-500/50",
"transition-all duration-200"
)}
data-testid="project-search-input" data-testid="project-search-input"
/> />
</div> </div>
</div> </div>
{filteredProjects.length === 0 ? ( {filteredProjects.length === 0 ? (
<div className="px-2 py-4 text-center text-sm text-muted-foreground"> <div className="px-2 py-6 text-center text-sm text-muted-foreground">
No projects found No projects found
</div> </div>
) : ( ) : (
@@ -1357,26 +1443,28 @@ export function Sidebar() {
items={filteredProjects.map((p) => p.id)} items={filteredProjects.map((p) => p.id)}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
{filteredProjects.map((project, index) => ( <div className="space-y-0.5 max-h-64 overflow-y-auto">
<SortableProjectItem {filteredProjects.map((project, index) => (
key={project.id} <SortableProjectItem
project={project} key={project.id}
currentProjectId={currentProject?.id} project={project}
isHighlighted={index === selectedProjectIndex} currentProjectId={currentProject?.id}
onSelect={(p) => { isHighlighted={index === selectedProjectIndex}
setCurrentProject(p); onSelect={(p) => {
setIsProjectPickerOpen(false); setCurrentProject(p);
}} setIsProjectPickerOpen(false);
/> }}
))} />
))}
</div>
</SortableContext> </SortableContext>
</DndContext> </DndContext>
)} )}
{/* Keyboard hint */} {/* Keyboard hint */}
<div className="px-2 pt-2 mt-1 border-t border-border"> <div className="px-2 pt-2 mt-1.5 border-t border-border/50">
<p className="text-[10px] text-muted-foreground text-center"> <p className="text-[10px] text-muted-foreground text-center tracking-wide">
navigate Enter select Esc close <span className="text-foreground/60">arrow</span> navigate <span className="mx-1 text-foreground/30">|</span> <span className="text-foreground/60">enter</span> select <span className="mx-1 text-foreground/30">|</span> <span className="text-foreground/60">esc</span> close
</p> </p>
</div> </div>
</DropdownMenuContent> </DropdownMenuContent>
@@ -1394,14 +1482,21 @@ export function Sidebar() {
> >
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
className="hidden lg:flex items-center justify-center w-8 h-[42px] rounded-lg text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border transition-all titlebar-no-drag" className={cn(
"hidden lg:flex items-center justify-center w-[42px] h-[42px] rounded-lg",
"text-muted-foreground hover:text-foreground",
"bg-transparent hover:bg-accent/60",
"border border-border/50 hover:border-border",
"transition-all duration-200 ease-out titlebar-no-drag",
"hover:scale-[1.02] active:scale-[0.98]"
)}
title="Project options" title="Project options"
data-testid="project-options-menu" data-testid="project-options-menu"
> >
<MoreVertical className="w-4 h-4" /> <MoreVertical className="w-4 h-4" />
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56"> <DropdownMenuContent align="end" className="w-56 bg-popover/95 backdrop-blur-xl">
{/* Project Theme Submenu */} {/* Project Theme Submenu */}
<DropdownMenuSub> <DropdownMenuSub>
<DropdownMenuSubTrigger data-testid="project-theme-trigger"> <DropdownMenuSubTrigger data-testid="project-theme-trigger">
@@ -1414,7 +1509,7 @@ export function Sidebar() {
)} )}
</DropdownMenuSubTrigger> </DropdownMenuSubTrigger>
<DropdownMenuSubContent <DropdownMenuSubContent
className="w-56" className="w-56 bg-popover/95 backdrop-blur-xl"
data-testid="project-theme-menu" data-testid="project-theme-menu"
onPointerLeave={() => { onPointerLeave={() => {
// Clear preview theme when leaving the dropdown // Clear preview theme when leaving the dropdown
@@ -1555,7 +1650,7 @@ export function Sidebar() {
)} )}
{/* Nav Items - Scrollable */} {/* Nav Items - Scrollable */}
<nav className="flex-1 overflow-y-auto px-2 mt-4 pb-2"> <nav className="flex-1 overflow-y-auto px-3 mt-5 pb-2">
{!currentProject && sidebarOpen ? ( {!currentProject && sidebarOpen ? (
// Placeholder when no project is selected (only in expanded state) // Placeholder when no project is selected (only in expanded state)
<div className="flex items-center justify-center h-full px-4"> <div className="flex items-center justify-center h-full px-4">
@@ -1571,18 +1666,18 @@ export function Sidebar() {
<div key={sectionIdx} className={sectionIdx > 0 ? "mt-6" : ""}> <div key={sectionIdx} className={sectionIdx > 0 ? "mt-6" : ""}>
{/* Section Label */} {/* Section Label */}
{section.label && sidebarOpen && ( {section.label && sidebarOpen && (
<div className="hidden lg:block px-4 mb-2"> <div className="hidden lg:block px-3 mb-2">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider"> <span className="text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-widest">
{section.label} {section.label}
</span> </span>
</div> </div>
)} )}
{section.label && !sidebarOpen && ( {section.label && !sidebarOpen && (
<div className="h-px bg-sidebar-border mx-2 mb-2"></div> <div className="h-px bg-border/30 mx-2 mb-3"></div>
)} )}
{/* Nav Items */} {/* Nav Items */}
<div className="space-y-1"> <div className="space-y-1.5">
{section.items.map((item) => { {section.items.map((item) => {
const isActive = isActiveRoute(item.id); const isActive = isActiveRoute(item.id);
const Icon = item.icon; const Icon = item.icon;
@@ -1592,29 +1687,43 @@ export function Sidebar() {
key={item.id} key={item.id}
onClick={() => setCurrentView(item.id as any)} onClick={() => setCurrentView(item.id as any)}
className={cn( className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag", "group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
isActive isActive
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border" ? [
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50", // Active: Premium gradient with glow
sidebarOpen ? "justify-start" : "justify-center" "bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10",
"text-foreground font-medium",
"border border-brand-500/30",
"shadow-md shadow-brand-500/10",
]
: [
// Inactive: Subtle hover state
"text-muted-foreground hover:text-foreground",
"hover:bg-accent/50",
"border border-transparent hover:border-border/40",
"hover:shadow-sm",
],
sidebarOpen ? "justify-start" : "justify-center",
"hover:scale-[1.02] active:scale-[0.97]"
)} )}
title={!sidebarOpen ? item.label : undefined} title={!sidebarOpen ? item.label : undefined}
data-testid={`nav-${item.id}`} data-testid={`nav-${item.id}`}
> >
{isActive && ( {isActive && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div> <div className="absolute inset-y-0 left-0 w-1 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full shadow-sm shadow-brand-500/50"></div>
)} )}
<Icon <Icon
className={cn( className={cn(
"w-4 h-4 shrink-0 transition-colors", "w-[18px] h-[18px] shrink-0 transition-all duration-200",
isActive isActive
? "text-brand-500" ? "text-brand-500 drop-shadow-sm"
: "group-hover:text-brand-400" : "group-hover:text-brand-400 group-hover:scale-110"
)} )}
/> />
<span <span
className={cn( className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left", "ml-3 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden" sidebarOpen ? "hidden lg:block" : "hidden"
)} )}
> >
@@ -1623,9 +1732,10 @@ export function Sidebar() {
{item.shortcut && sidebarOpen && ( {item.shortcut && sidebarOpen && (
<span <span
className={cn( className={cn(
"hidden lg:flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70", "hidden lg:flex items-center justify-center min-w-5 h-5 px-1.5 text-[10px] font-mono rounded-md transition-all duration-200",
isActive && isActive
"bg-brand-500/20 border-brand-500/50 text-brand-400" ? "bg-brand-500/20 text-brand-400"
: "bg-muted text-muted-foreground group-hover:bg-accent"
)} )}
data-testid={`shortcut-${item.id}`} data-testid={`shortcut-${item.id}`}
> >
@@ -1635,10 +1745,22 @@ export function Sidebar() {
{/* Tooltip for collapsed state */} {/* Tooltip for collapsed state */}
{!sidebarOpen && ( {!sidebarOpen && (
<span <span
className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700" className={cn(
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
"bg-popover text-popover-foreground text-xs font-medium",
"border border-border shadow-lg",
"opacity-0 group-hover:opacity-100",
"transition-all duration-200 whitespace-nowrap z-50",
"translate-x-1 group-hover:translate-x-0"
)}
data-testid={`sidebar-tooltip-${item.label.toLowerCase()}`} data-testid={`sidebar-tooltip-${item.label.toLowerCase()}`}
> >
{item.label} {item.label}
{item.shortcut && (
<span className="ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
{formatShortcut(item.shortcut, true)}
</span>
)}
</span> </span>
)} )}
</button> </button>
@@ -1652,7 +1774,13 @@ export function Sidebar() {
</div> </div>
{/* Bottom Section - Running Agents / Bug Report / Settings */} {/* Bottom Section - Running Agents / Bug Report / Settings */}
<div className="border-t border-sidebar-border bg-sidebar-accent/10 shrink-0"> <div className={cn(
"shrink-0",
// Top border with gradient fade
"border-t border-border/40",
// Elevated background for visual separation
"bg-gradient-to-t from-background/10 via-sidebar/50 to-transparent"
)}>
{/* Course Promo Badge */} {/* Course Promo Badge */}
<CoursePromoBadge sidebarOpen={sidebarOpen} /> <CoursePromoBadge sidebarOpen={sidebarOpen} />
{/* Wiki Link */} {/* Wiki Link */}
@@ -1661,36 +1789,55 @@ export function Sidebar() {
<button <button
onClick={() => setCurrentView("wiki")} onClick={() => setCurrentView("wiki")}
className={cn( className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag", "group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
isActiveRoute("wiki") isActiveRoute("wiki")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border" ? [
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50", "bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10",
sidebarOpen ? "justify-start" : "justify-center" "text-foreground font-medium",
"border border-brand-500/30",
"shadow-md shadow-brand-500/10",
]
: [
"text-muted-foreground hover:text-foreground",
"hover:bg-accent/50",
"border border-transparent hover:border-border/40",
"hover:shadow-sm",
],
sidebarOpen ? "justify-start" : "justify-center",
"hover:scale-[1.02] active:scale-[0.97]"
)} )}
title={!sidebarOpen ? "Wiki" : undefined} title={!sidebarOpen ? "Wiki" : undefined}
data-testid="wiki-link" data-testid="wiki-link"
> >
{isActiveRoute("wiki") && ( {isActiveRoute("wiki") && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div> <div className="absolute inset-y-0 left-0 w-1 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full shadow-sm shadow-brand-500/50"></div>
)} )}
<BookOpen <BookOpen
className={cn( className={cn(
"w-4 h-4 shrink-0 transition-colors", "w-[18px] h-[18px] shrink-0 transition-all duration-200",
isActiveRoute("wiki") isActiveRoute("wiki")
? "text-brand-500" ? "text-brand-500 drop-shadow-sm"
: "group-hover:text-brand-400" : "group-hover:text-brand-400 group-hover:scale-110"
)} )}
/> />
<span <span
className={cn( className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left", "ml-3 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden" sidebarOpen ? "hidden lg:block" : "hidden"
)} )}
> >
Wiki Wiki
</span> </span>
{!sidebarOpen && ( {!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border"> <span className={cn(
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
"bg-popover text-popover-foreground text-xs font-medium",
"border border-border shadow-lg",
"opacity-0 group-hover:opacity-100",
"transition-all duration-200 whitespace-nowrap z-50",
"translate-x-1 group-hover:translate-x-0"
)}>
Wiki Wiki
</span> </span>
)} )}
@@ -1703,31 +1850,48 @@ export function Sidebar() {
<button <button
onClick={() => setCurrentView("running-agents")} onClick={() => setCurrentView("running-agents")}
className={cn( className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag", "group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
isActiveRoute("running-agents") isActiveRoute("running-agents")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border" ? [
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50", "bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10",
sidebarOpen ? "justify-start" : "justify-center" "text-foreground font-medium",
"border border-brand-500/30",
"shadow-md shadow-brand-500/10",
]
: [
"text-muted-foreground hover:text-foreground",
"hover:bg-accent/50",
"border border-transparent hover:border-border/40",
"hover:shadow-sm",
],
sidebarOpen ? "justify-start" : "justify-center",
"hover:scale-[1.02] active:scale-[0.97]"
)} )}
title={!sidebarOpen ? "Running Agents" : undefined} title={!sidebarOpen ? "Running Agents" : undefined}
data-testid="running-agents-link" data-testid="running-agents-link"
> >
{isActiveRoute("running-agents") && ( {isActiveRoute("running-agents") && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div> <div className="absolute inset-y-0 left-0 w-1 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full shadow-sm shadow-brand-500/50"></div>
)} )}
<div className="relative"> <div className="relative">
<Activity <Activity
className={cn( className={cn(
"w-4 h-4 shrink-0 transition-colors", "w-[18px] h-[18px] shrink-0 transition-all duration-200",
isActiveRoute("running-agents") isActiveRoute("running-agents")
? "text-brand-500" ? "text-brand-500 drop-shadow-sm"
: "group-hover:text-brand-400" : "group-hover:text-brand-400 group-hover:scale-110"
)} )}
/> />
{/* Running agents count badge - shown in collapsed state */} {/* Running agents count badge - shown in collapsed state */}
{!sidebarOpen && runningAgentsCount > 0 && ( {!sidebarOpen && runningAgentsCount > 0 && (
<span <span
className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-semibold rounded-full bg-brand-500 text-white" className={cn(
"absolute -top-1.5 -right-1.5 flex items-center justify-center",
"min-w-4 h-4 px-1 text-[9px] font-bold rounded-full",
"bg-brand-500 text-white shadow-sm",
"animate-in fade-in zoom-in duration-200"
)}
data-testid="running-agents-count-collapsed" data-testid="running-agents-count-collapsed"
> >
{runningAgentsCount > 99 ? "99" : runningAgentsCount} {runningAgentsCount > 99 ? "99" : runningAgentsCount}
@@ -1736,7 +1900,7 @@ export function Sidebar() {
</div> </div>
<span <span
className={cn( className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left", "ml-3 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden" sidebarOpen ? "hidden lg:block" : "hidden"
)} )}
> >
@@ -1746,7 +1910,10 @@ export function Sidebar() {
{sidebarOpen && runningAgentsCount > 0 && ( {sidebarOpen && runningAgentsCount > 0 && (
<span <span
className={cn( className={cn(
"hidden lg:flex items-center justify-center min-w-6 h-6 px-1.5 text-xs font-semibold rounded-full bg-brand-500 text-white", "hidden lg:flex items-center justify-center",
"min-w-6 h-6 px-1.5 text-xs font-semibold rounded-full",
"bg-brand-500 text-white shadow-sm",
"animate-in fade-in zoom-in duration-200",
isActiveRoute("running-agents") && "bg-brand-600" isActiveRoute("running-agents") && "bg-brand-600"
)} )}
data-testid="running-agents-count" data-testid="running-agents-count"
@@ -1755,8 +1922,20 @@ export function Sidebar() {
</span> </span>
)} )}
{!sidebarOpen && ( {!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border"> <span className={cn(
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
"bg-popover text-popover-foreground text-xs font-medium",
"border border-border shadow-lg",
"opacity-0 group-hover:opacity-100",
"transition-all duration-200 whitespace-nowrap z-50",
"translate-x-1 group-hover:translate-x-0"
)}>
Running Agents Running Agents
{runningAgentsCount > 0 && (
<span className="ml-2 px-1.5 py-0.5 bg-brand-500 text-white rounded-full text-[10px] font-semibold">
{runningAgentsCount}
</span>
)}
</span> </span>
)} )}
</button> </button>
@@ -1767,29 +1946,41 @@ export function Sidebar() {
<button <button
onClick={() => setCurrentView("settings")} onClick={() => setCurrentView("settings")}
className={cn( className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag", "group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
isActiveRoute("settings") isActiveRoute("settings")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border" ? [
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50", "bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10",
sidebarOpen ? "justify-start" : "justify-center" "text-foreground font-medium",
"border border-brand-500/30",
"shadow-md shadow-brand-500/10",
]
: [
"text-muted-foreground hover:text-foreground",
"hover:bg-accent/50",
"border border-transparent hover:border-border/40",
"hover:shadow-sm",
],
sidebarOpen ? "justify-start" : "justify-center",
"hover:scale-[1.02] active:scale-[0.97]"
)} )}
title={!sidebarOpen ? "Settings" : undefined} title={!sidebarOpen ? "Settings" : undefined}
data-testid="settings-button" data-testid="settings-button"
> >
{isActiveRoute("settings") && ( {isActiveRoute("settings") && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div> <div className="absolute inset-y-0 left-0 w-1 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full shadow-sm shadow-brand-500/50"></div>
)} )}
<Settings <Settings
className={cn( className={cn(
"w-4 h-4 shrink-0 transition-colors", "w-[18px] h-[18px] shrink-0 transition-all duration-200",
isActiveRoute("settings") isActiveRoute("settings")
? "text-brand-500" ? "text-brand-500 drop-shadow-sm"
: "group-hover:text-brand-400" : "group-hover:text-brand-400 group-hover:rotate-90 group-hover:scale-110"
)} )}
/> />
<span <span
className={cn( className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left", "ml-3 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden" sidebarOpen ? "hidden lg:block" : "hidden"
)} )}
> >
@@ -1798,9 +1989,10 @@ export function Sidebar() {
{sidebarOpen && ( {sidebarOpen && (
<span <span
className={cn( className={cn(
"hidden lg:flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70", "hidden lg:flex items-center justify-center min-w-5 h-5 px-1.5 text-[10px] font-mono rounded-md transition-all duration-200",
isActiveRoute("settings") && isActiveRoute("settings")
"bg-brand-500/20 border-brand-500/50 text-brand-400" ? "bg-brand-500/20 text-brand-400"
: "bg-muted text-muted-foreground group-hover:bg-accent"
)} )}
data-testid="shortcut-settings" data-testid="shortcut-settings"
> >
@@ -1808,15 +2000,25 @@ export function Sidebar() {
</span> </span>
)} )}
{!sidebarOpen && ( {!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border"> <span className={cn(
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
"bg-popover text-popover-foreground text-xs font-medium",
"border border-border shadow-lg",
"opacity-0 group-hover:opacity-100",
"transition-all duration-200 whitespace-nowrap z-50",
"translate-x-1 group-hover:translate-x-0"
)}>
Settings Settings
<span className="ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
{formatShortcut(shortcuts.settings, true)}
</span>
</span> </span>
)} )}
</button> </button>
</div> </div>
</div> </div>
<Dialog open={showTrashDialog} onOpenChange={setShowTrashDialog}> <Dialog open={showTrashDialog} onOpenChange={setShowTrashDialog}>
<DialogContent className="bg-popover border-border max-w-2xl"> <DialogContent className="bg-popover/95 backdrop-blur-xl border-border max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Recycle Bin</DialogTitle> <DialogTitle>Recycle Bin</DialogTitle>
<DialogDescription className="text-muted-foreground"> <DialogDescription className="text-muted-foreground">
@@ -1834,7 +2036,7 @@ export function Sidebar() {
{trashedProjects.map((project) => ( {trashedProjects.map((project) => (
<div <div
key={project.id} key={project.id}
className="flex items-start justify-between gap-3 rounded-md border border-sidebar-border bg-sidebar-accent/20 p-3" className="flex items-start justify-between gap-3 rounded-lg border border-border bg-card/50 p-4"
> >
<div className="space-y-1 min-w-0"> <div className="space-y-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate"> <p className="text-sm font-medium text-foreground truncate">
@@ -1912,7 +2114,7 @@ export function Sidebar() {
} }
}} }}
> >
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl bg-popover/95 backdrop-blur-xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Set Up Your Project</DialogTitle> <DialogTitle>Set Up Your Project</DialogTitle>
<DialogDescription className="text-muted-foreground"> <DialogDescription className="text-muted-foreground">
@@ -1932,7 +2134,7 @@ export function Sidebar() {
better specification. better specification.
</p> </p>
<textarea <textarea
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring" className="w-full h-48 p-3 rounded-lg border border-border bg-background/50 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-brand-500/30 focus:border-brand-500/50 transition-all"
value={projectOverview} value={projectOverview}
onChange={(e) => setProjectOverview(e.target.value)} onChange={(e) => setProjectOverview(e.target.value)}
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..." placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
@@ -1970,6 +2172,7 @@ export function Sidebar() {
<Button <Button
onClick={handleCreateInitialSpec} onClick={handleCreateInitialSpec}
disabled={!projectOverview.trim()} disabled={!projectOverview.trim()}
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
> >
<Sparkles className="w-4 h-4 mr-2" /> <Sparkles className="w-4 h-4 mr-2" />
Generate Spec Generate Spec
@@ -1987,7 +2190,7 @@ export function Sidebar() {
} }
}} }}
> >
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl bg-popover/95 backdrop-blur-xl">
<DialogHeader> <DialogHeader>
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-brand-500/10 border border-brand-500/20"> <div className="flex items-center justify-center w-12 h-12 rounded-full bg-brand-500/10 border border-brand-500/20">
@@ -2016,7 +2219,7 @@ export function Sidebar() {
</div> </div>
{/* Benefits list */} {/* Benefits list */}
<div className="space-y-3 rounded-lg bg-muted/50 border border-border p-4"> <div className="space-y-3 rounded-xl bg-muted/30 border border-border/50 p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" /> <CheckCircle2 className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
<div> <div>
@@ -2056,9 +2259,9 @@ export function Sidebar() {
</div> </div>
{/* Info box */} {/* Info box */}
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-3"> <div className="rounded-xl bg-brand-500/5 border border-brand-500/10 p-3">
<p className="text-xs text-blue-400 leading-relaxed"> <p className="text-xs text-muted-foreground leading-relaxed">
<strong className="text-blue-300">Tip:</strong> You can always <strong className="text-foreground">Tip:</strong> You can always
generate or edit your app_spec.txt later from the Spec Editor in generate or edit your app_spec.txt later from the Spec Editor in
the sidebar. the sidebar.
</p> </p>

View File

@@ -4,21 +4,42 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 shadow-sm",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80", "border-transparent bg-primary text-primary-foreground hover:bg-primary/90",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", "border-transparent bg-destructive text-white hover:bg-destructive/90",
outline: "text-foreground", outline:
"text-foreground border-border bg-background/50 backdrop-blur-sm",
// Semantic status variants using CSS variables
success:
"border-transparent bg-[var(--status-success-bg)] text-[var(--status-success)] border border-[var(--status-success)]/30",
warning:
"border-transparent bg-[var(--status-warning-bg)] text-[var(--status-warning)] border border-[var(--status-warning)]/30",
error:
"border-transparent bg-[var(--status-error-bg)] text-[var(--status-error)] border border-[var(--status-error)]/30",
info:
"border-transparent bg-[var(--status-info-bg)] text-[var(--status-info)] border border-[var(--status-info)]/30",
// Muted variants for subtle indication
muted:
"border-border/50 bg-muted/50 text-muted-foreground",
brand:
"border-transparent bg-brand-500/15 text-brand-500 border border-brand-500/30",
},
size: {
default: "px-2.5 py-0.5 text-xs",
sm: "px-2 py-0.5 text-[10px]",
lg: "px-3 py-1 text-sm",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default",
}, },
} }
); );
@@ -27,9 +48,9 @@ export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, size, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props} /> <div className={cn(badgeVariants({ variant, size }), className)} {...props} />
); );
} }

View File

@@ -1,24 +1,26 @@
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.98]",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default:
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow-md hover:shadow-primary/25",
destructive: destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white shadow-sm hover:bg-destructive/90 hover:shadow-md hover:shadow-destructive/25 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline active:scale-100",
"animated-outline": "animated-outline":
"relative overflow-hidden rounded-xl hover:bg-transparent shadow-none", "relative overflow-hidden rounded-xl hover:bg-transparent shadow-none",
}, },
@@ -38,17 +40,32 @@ const buttonVariants = cva(
} }
); );
// Loading spinner component
function ButtonSpinner({ className }: { className?: string }) {
return (
<Loader2
className={cn("size-4 animate-spin", className)}
aria-hidden="true"
/>
);
}
function Button({ function Button({
className, className,
variant, variant,
size, size,
asChild = false, asChild = false,
loading = false,
disabled,
children, children,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean; asChild?: boolean;
loading?: boolean;
}) { }) {
const isDisabled = disabled || loading;
// Special handling for animated-outline variant // Special handling for animated-outline variant
if (variant === "animated-outline" && !asChild) { if (variant === "animated-outline" && !asChild) {
return ( return (
@@ -59,20 +76,22 @@ function Button({
className className
)} )}
data-slot="button" data-slot="button"
disabled={isDisabled}
{...props} {...props}
> >
{/* Animated rotating gradient border */} {/* Animated rotating gradient border - smoother animation */}
<span className="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] animated-outline-gradient" /> <span className="absolute inset-[-1000%] animate-[spin_3s_linear_infinite] animated-outline-gradient opacity-75 transition-opacity duration-300 group-hover:opacity-100" />
{/* Inner content container */} {/* Inner content container */}
<span <span
className={cn( className={cn(
"animated-outline-inner inline-flex h-full w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] px-4 py-1 text-sm font-medium backdrop-blur-3xl transition-all", "animated-outline-inner inline-flex h-full w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] px-4 py-1 text-sm font-medium backdrop-blur-3xl transition-all duration-200",
size === "sm" && "px-3 text-xs gap-1.5", size === "sm" && "px-3 text-xs gap-1.5",
size === "lg" && "px-8", size === "lg" && "px-8",
size === "icon" && "p-0 gap-0" size === "icon" && "p-0 gap-0"
)} )}
> >
{loading && <ButtonSpinner />}
{children} {children}
</span> </span>
</button> </button>
@@ -85,8 +104,10 @@ function Button({
<Comp <Comp
data-slot="button" data-slot="button"
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
disabled={isDisabled}
{...props} {...props}
> >
{loading && <ButtonSpinner />}
{children} {children}
</Comp> </Comp>
); );

View File

@@ -2,12 +2,20 @@ import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) { interface CardProps extends React.ComponentProps<"div"> {
gradient?: boolean;
}
function Card({ className, gradient = false, ...props }: CardProps) {
return ( return (
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-white/10 backdrop-blur-sm py-6 shadow-sm", "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-white/10 backdrop-blur-md py-6",
// Premium layered shadow
"shadow-[0_1px_2px_rgba(0,0,0,0.05),0_4px_6px_rgba(0,0,0,0.05),0_10px_20px_rgba(0,0,0,0.04)]",
// Gradient border option
gradient && "relative before:absolute before:inset-0 before:rounded-xl before:p-[1px] before:bg-gradient-to-br before:from-white/20 before:to-transparent before:pointer-events-none before:-z-10",
className className
)} )}
{...props} {...props}
@@ -20,7 +28,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className className
)} )}
{...props} {...props}
@@ -32,7 +40,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-title" data-slot="card-title"
className={cn("leading-none font-semibold", className)} className={cn("leading-none font-semibold tracking-tight", className)}
{...props} {...props}
/> />
); );
@@ -42,7 +50,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-description" data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm leading-relaxed", className)}
{...props} {...props}
/> />
); );
@@ -75,7 +83,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-footer" data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn("flex items-center gap-3 px-6 [.border-t]:pt-6", className)}
{...props} {...props}
/> />
); );

View File

@@ -38,7 +38,10 @@ function DialogOverlay({
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"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-50 bg-black/50", "fixed inset-0 z-50 bg-black/60 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",
"duration-200",
className className
)} )}
{...props} {...props}
@@ -66,7 +69,17 @@ function DialogContent({
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-4rem)]", "fixed top-[50%] left-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
"flex flex-col w-full max-w-[calc(100%-2rem)] max-h-[calc(100vh-4rem)]",
"bg-card border border-border rounded-xl shadow-2xl",
// Premium shadow
"shadow-[0_25px_50px_-12px_rgba(0,0,0,0.25)]",
// Animations - smoother with scale
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"data-[state=closed]:slide-out-to-top-[2%] data-[state=open]:slide-in-from-top-[2%]",
"duration-200",
compact compact
? "max-w-4xl p-4" ? "max-w-4xl p-4"
: !hasCustomMaxWidth : !hasCustomMaxWidth
@@ -81,8 +94,13 @@ function DialogContent({
<DialogPrimitive.Close <DialogPrimitive.Close
data-slot="dialog-close" data-slot="dialog-close"
className={cn( className={cn(
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity cursor-pointer hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "absolute rounded-lg opacity-60 transition-all duration-200 cursor-pointer",
compact ? "top-2 right-3" : "top-3 right-5" "hover:opacity-100 hover:bg-muted",
"focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-none",
"disabled:pointer-events-none disabled:cursor-not-allowed",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4",
"p-1.5",
compact ? "top-2 right-3" : "top-4 right-4"
)} )}
> >
<XIcon /> <XIcon />
@@ -109,7 +127,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="dialog-footer" data-slot="dialog-footer"
className={cn( className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end mt-6",
className className
)} )}
{...props} {...props}
@@ -124,7 +142,7 @@ function DialogTitle({
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)} className={cn("text-lg leading-none font-semibold tracking-tight", className)}
{...props} {...props}
/> />
); );
@@ -137,7 +155,7 @@ function DialogDescription({
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm leading-relaxed", className)}
{...props} {...props}
/> />
); );

View File

@@ -2,20 +2,64 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) { interface InputProps extends React.ComponentProps<"input"> {
return ( startAddon?: React.ReactNode;
endAddon?: React.ReactNode;
}
function Input({ className, type, startAddon, endAddon, ...props }: InputProps) {
const hasAddons = startAddon || endAddon;
const inputElement = (
<input <input
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-input border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "file:text-foreground placeholder:text-muted-foreground/60 selection:bg-primary selection:text-primary-foreground bg-input border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
// Inner shadow for depth
"shadow-[inset_0_1px_2px_rgba(0,0,0,0.05)]",
// Animated focus ring
"transition-[color,box-shadow,border-color] duration-200 ease-out",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
// Adjust padding for addons
startAddon && "pl-0",
endAddon && "pr-0",
hasAddons && "border-0 shadow-none focus-visible:ring-0",
className className
)} )}
{...props} {...props}
/> />
) );
if (!hasAddons) {
return inputElement;
}
return (
<div
className={cn(
"flex items-center h-9 w-full rounded-md border border-input bg-input shadow-xs",
"shadow-[inset_0_1px_2px_rgba(0,0,0,0.05)]",
"transition-[box-shadow,border-color] duration-200 ease-out",
"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
"has-[input:disabled]:opacity-50 has-[input:disabled]:cursor-not-allowed",
"has-[input[aria-invalid]]:ring-destructive/20 has-[input[aria-invalid]]:border-destructive"
)}
>
{startAddon && (
<span className="flex items-center justify-center px-3 text-muted-foreground text-sm">
{startAddon}
</span>
)}
{inputElement}
{endAddon && (
<span className="flex items-center justify-center px-3 text-muted-foreground text-sm">
{endAddon}
</span>
)}
</div>
);
} }
export { Input } export { Input }

View File

@@ -7,7 +7,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea <textarea
data-slot="textarea" data-slot="textarea"
className={cn( className={cn(
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-h-[80px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-none", "placeholder:text-muted-foreground/60 selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-h-[80px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-none",
// Inner shadow for depth
"shadow-[inset_0_1px_2px_rgba(0,0,0,0.05)]",
// Animated focus ring
"transition-[color,box-shadow,border-color] duration-200 ease-out",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className

View File

@@ -14,13 +14,23 @@ const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 6, ...props }, ref) => (
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 overflow-hidden rounded-lg border border-border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground",
// Premium shadow
"shadow-lg shadow-black/10",
// Faster, snappier animations
"animate-in fade-in-0 zoom-in-95 duration-150",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:duration-100",
// Slide from edge
"data-[side=bottom]:slide-in-from-top-1",
"data-[side=left]:slide-in-from-right-1",
"data-[side=right]:slide-in-from-left-1",
"data-[side=top]:slide-in-from-bottom-1",
className className
)} )}
{...props} {...props}

View File

@@ -2,7 +2,6 @@
import { useState, useCallback, useRef, useEffect, useMemo } from "react"; import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import { useAppStore } from "@/store/app-store"; import { useAppStore } from "@/store/app-store";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ImageDropZone } from "@/components/ui/image-drop-zone"; import { ImageDropZone } from "@/components/ui/image-drop-zone";
@@ -222,11 +221,6 @@ export function AgentView() {
e.stopPropagation(); e.stopPropagation();
if (isProcessing || !isConnected) return; if (isProcessing || !isConnected) return;
console.log(
"[agent-view] Drag enter types:",
Array.from(e.dataTransfer.types)
);
// Check if dragged items contain files // Check if dragged items contain files
if (e.dataTransfer.types.includes("Files")) { if (e.dataTransfer.types.includes("Files")) {
setIsDragOver(true); setIsDragOver(true);
@@ -262,39 +256,21 @@ export function AgentView() {
if (isProcessing || !isConnected) return; if (isProcessing || !isConnected) return;
console.log("[agent-view] Drop event:", {
filesCount: e.dataTransfer.files.length,
itemsCount: e.dataTransfer.items.length,
types: Array.from(e.dataTransfer.types),
});
// Check if we have files // Check if we have files
const files = e.dataTransfer.files; const files = e.dataTransfer.files;
if (files && files.length > 0) { if (files && files.length > 0) {
console.log("[agent-view] Processing files from dataTransfer.files");
processDroppedFiles(files); processDroppedFiles(files);
return; return;
} }
// Handle file paths (from screenshots or other sources) // Handle file paths (from screenshots or other sources)
// This is common on macOS when dragging screenshots
const items = e.dataTransfer.items; const items = e.dataTransfer.items;
if (items && items.length > 0) { if (items && items.length > 0) {
console.log("[agent-view] Processing items");
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
console.log(`[agent-view] Item ${i}:`, {
kind: item.kind,
type: item.type,
});
if (item.kind === "file") { if (item.kind === "file") {
const file = item.getAsFile(); const file = item.getAsFile();
if (file) { if (file) {
console.log("[agent-view] Got file from item:", {
name: file.name,
type: file.type,
size: file.size,
});
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();
dataTransfer.items.add(file); dataTransfer.items.add(file);
processDroppedFiles(dataTransfer.files); processDroppedFiles(dataTransfer.files);
@@ -315,10 +291,6 @@ export function AgentView() {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
console.log("[agent-view] Paste item:", {
kind: item.kind,
type: item.type,
});
if (item.kind === "file") { if (item.kind === "file") {
const file = item.getAsFile(); const file = item.getAsFile();
@@ -330,10 +302,6 @@ export function AgentView() {
} }
if (files.length > 0) { if (files.length > 0) {
console.log(
"[agent-view] Processing pasted image files:",
files.length
);
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();
files.forEach((file) => dataTransfer.items.add(file)); files.forEach((file) => dataTransfer.items.add(file));
await processDroppedFiles(dataTransfer.files); await processDroppedFiles(dataTransfer.files);
@@ -442,13 +410,15 @@ export function AgentView() {
if (!currentProject) { if (!currentProject) {
return ( return (
<div <div
className="flex-1 flex items-center justify-center" className="flex-1 flex items-center justify-center bg-background"
data-testid="agent-view-no-project" data-testid="agent-view-no-project"
> >
<div className="text-center"> <div className="text-center max-w-md">
<Sparkles className="w-12 h-12 text-muted-foreground mx-auto mb-4" /> <div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto mb-6">
<h2 className="text-xl font-semibold mb-2">No Project Selected</h2> <Sparkles className="w-8 h-8 text-primary" />
<p className="text-muted-foreground"> </div>
<h2 className="text-xl font-semibold mb-3 text-foreground">No Project Selected</h2>
<p className="text-muted-foreground leading-relaxed">
Open or create a project to start working with the AI agent. Open or create a project to start working with the AI agent.
</p> </p>
</div> </div>
@@ -472,12 +442,12 @@ export function AgentView() {
return ( return (
<div <div
className="flex-1 flex overflow-hidden content-bg" className="flex-1 flex overflow-hidden bg-background"
data-testid="agent-view" data-testid="agent-view"
> >
{/* Session Manager Sidebar */} {/* Session Manager Sidebar */}
{showSessionManager && currentProject && ( {showSessionManager && currentProject && (
<div className="w-80 border-r flex-shrink-0"> <div className="w-80 border-r border-border flex-shrink-0 bg-card/50">
<SessionManager <SessionManager
currentSessionId={currentSessionId} currentSessionId={currentSessionId}
onSelectSession={handleSelectSession} onSelectSession={handleSelectSession}
@@ -491,13 +461,13 @@ export function AgentView() {
{/* Chat Area */} {/* Chat Area */}
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md"> <div className="flex items-center justify-between px-6 py-4 border-b border-border bg-card/50 backdrop-blur-sm">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setShowSessionManager(!showSessionManager)} onClick={() => setShowSessionManager(!showSessionManager)}
className="h-8 w-8 p-0" className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
> >
{showSessionManager ? ( {showSessionManager ? (
<PanelLeftClose className="w-4 h-4" /> <PanelLeftClose className="w-4 h-4" />
@@ -505,26 +475,28 @@ export function AgentView() {
<PanelLeft className="w-4 h-4" /> <PanelLeft className="w-4 h-4" />
)} )}
</Button> </Button>
<Bot className="w-5 h-5 text-primary" /> <div className="w-9 h-9 rounded-xl bg-primary/10 flex items-center justify-center">
<Bot className="w-5 h-5 text-primary" />
</div>
<div> <div>
<h1 className="text-xl font-bold">AI Agent</h1> <h1 className="text-lg font-semibold text-foreground">AI Agent</h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{currentProject.name} {currentProject.name}
{currentSessionId && !isConnected && " · Connecting..."} {currentSessionId && !isConnected && " - Connecting..."}
</p> </p>
</div> </div>
</div> </div>
{/* Status indicators & actions */} {/* Status indicators & actions */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
{currentTool && ( {currentTool && (
<div className="flex items-center gap-1 text-xs text-muted-foreground bg-muted px-2 py-1 rounded"> <div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 px-3 py-1.5 rounded-full border border-border">
<Wrench className="w-3 h-3" /> <Wrench className="w-3 h-3 text-primary" />
<span>{currentTool}</span> <span className="font-medium">{currentTool}</span>
</div> </div>
)} )}
{agentError && ( {agentError && (
<span className="text-xs text-destructive">{agentError}</span> <span className="text-xs text-destructive font-medium">{agentError}</span>
)} )}
{currentSessionId && messages.length > 0 && ( {currentSessionId && messages.length > 0 && (
<Button <Button
@@ -532,8 +504,9 @@ export function AgentView() {
size="sm" size="sm"
onClick={handleClearChat} onClick={handleClearChat}
disabled={isProcessing} disabled={isProcessing}
className="text-muted-foreground hover:text-foreground"
> >
<Trash2 className="w-4 h-4 mr-1" /> <Trash2 className="w-4 h-4 mr-2" />
Clear Clear
</Button> </Button>
)} )}
@@ -543,22 +516,25 @@ export function AgentView() {
{/* Messages */} {/* Messages */}
{!currentSessionId ? ( {!currentSessionId ? (
<div <div
className="flex-1 flex items-center justify-center" className="flex-1 flex items-center justify-center bg-background"
data-testid="no-session-placeholder" data-testid="no-session-placeholder"
> >
<div className="text-center"> <div className="text-center max-w-md">
<Bot className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-50" /> <div className="w-16 h-16 rounded-2xl bg-muted/50 flex items-center justify-center mx-auto mb-6">
<h2 className="text-lg font-semibold mb-2"> <Bot className="w-8 h-8 text-muted-foreground" />
</div>
<h2 className="text-lg font-semibold mb-3 text-foreground">
No Session Selected No Session Selected
</h2> </h2>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-6 leading-relaxed">
Create or select a session to start chatting Create or select a session to start chatting with the AI agent
</p> </p>
<Button <Button
onClick={() => setShowSessionManager(true)} onClick={() => setShowSessionManager(true)}
variant="outline" variant="outline"
className="gap-2"
> >
<PanelLeft className="w-4 h-4 mr-2" /> <PanelLeft className="w-4 h-4" />
{showSessionManager ? "View" : "Show"} Sessions {showSessionManager ? "View" : "Show"} Sessions
</Button> </Button>
</div> </div>
@@ -566,7 +542,7 @@ export function AgentView() {
) : ( ) : (
<div <div
ref={messagesContainerRef} ref={messagesContainerRef}
className="flex-1 overflow-y-auto p-4 space-y-4" className="flex-1 overflow-y-auto px-6 py-6 space-y-6 scroll-smooth"
data-testid="message-list" data-testid="message-list"
onScroll={handleScroll} onScroll={handleScroll}
> >
@@ -574,95 +550,156 @@ export function AgentView() {
<div <div
key={message.id} key={message.id}
className={cn( className={cn(
"flex gap-3", "flex gap-4 max-w-4xl",
message.role === "user" && "flex-row-reverse" message.role === "user" ? "flex-row-reverse ml-auto" : ""
)} )}
> >
{/* Avatar */}
<div <div
className={cn( className={cn(
"w-8 h-8 rounded-full flex items-center justify-center shrink-0", "w-9 h-9 rounded-xl flex items-center justify-center shrink-0 shadow-sm",
message.role === "assistant" ? "bg-primary/10" : "bg-muted" message.role === "assistant"
? "bg-primary/10 ring-1 ring-primary/20"
: "bg-muted ring-1 ring-border"
)} )}
> >
{message.role === "assistant" ? ( {message.role === "assistant" ? (
<Bot className="w-4 h-4 text-primary" /> <Bot className="w-4 h-4 text-primary" />
) : ( ) : (
<User className="w-4 h-4" /> <User className="w-4 h-4 text-muted-foreground" />
)} )}
</div> </div>
<Card
{/* Message Bubble */}
<div
className={cn( className={cn(
"max-w-[80%] py-0", "flex-1 max-w-[85%] rounded-2xl px-4 py-3 shadow-sm",
message.role === "user" message.role === "user"
? "bg-transparent border border-primary text-foreground" ? "bg-primary text-primary-foreground"
: "border-l-4 border-primary bg-card" : "bg-card border border-border"
)} )}
> >
<CardContent className="px-3 py-2"> {message.role === "assistant" ? (
{message.role === "assistant" ? ( <Markdown className="text-sm text-foreground prose-p:leading-relaxed prose-headings:text-foreground prose-strong:text-foreground prose-code:text-primary prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded">
<Markdown className="text-sm text-primary prose-headings:text-primary prose-strong:text-primary prose-code:text-primary"> {message.content}
{message.content} </Markdown>
</Markdown> ) : (
) : ( <p className="text-sm whitespace-pre-wrap leading-relaxed">
<p className="text-sm whitespace-pre-wrap"> {message.content}
{message.content}
</p>
)}
<p
className={cn(
"text-xs mt-1",
message.role === "user"
? "text-muted-foreground"
: "text-primary/70"
)}
>
{new Date(message.timestamp).toLocaleTimeString()}
</p> </p>
</CardContent> )}
</Card> <p
className={cn(
"text-[11px] mt-2 font-medium",
message.role === "user"
? "text-primary-foreground/70"
: "text-muted-foreground"
)}
>
{new Date(message.timestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
</div> </div>
))} ))}
{/* Thinking Indicator */}
{isProcessing && ( {isProcessing && (
<div className="flex gap-3"> <div className="flex gap-4 max-w-4xl">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center"> <div className="w-9 h-9 rounded-xl bg-primary/10 ring-1 ring-primary/20 flex items-center justify-center shrink-0 shadow-sm">
<Bot className="w-4 h-4 text-primary" /> <Bot className="w-4 h-4 text-primary" />
</div> </div>
<Card className="border-l-4 border-primary bg-card py-0"> <div className="bg-card border border-border rounded-2xl px-4 py-3 shadow-sm">
<CardContent className="p-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
<Loader2 className="w-4 h-4 animate-spin text-primary" /> <span className="w-2 h-2 rounded-full bg-primary animate-pulse" style={{ animationDelay: "0ms" }} />
<span className="text-sm text-primary"> <span className="w-2 h-2 rounded-full bg-primary animate-pulse" style={{ animationDelay: "150ms" }} />
Thinking... <span className="w-2 h-2 rounded-full bg-primary animate-pulse" style={{ animationDelay: "300ms" }} />
</span>
</div> </div>
</CardContent> <span className="text-sm text-muted-foreground">
</Card> Thinking...
</span>
</div>
</div>
</div> </div>
)} )}
</div> </div>
)} )}
{/* Input */} {/* Input Area */}
{currentSessionId && ( {currentSessionId && (
<div className="border-t border-border p-4 space-y-3 bg-background"> <div className="border-t border-border p-4 bg-card/50 backdrop-blur-sm">
{/* Image Drop Zone (when visible) */} {/* Image Drop Zone (when visible) */}
{showImageDropZone && ( {showImageDropZone && (
<ImageDropZone <ImageDropZone
onImagesSelected={handleImagesSelected} onImagesSelected={handleImagesSelected}
images={selectedImages} images={selectedImages}
maxFiles={5} maxFiles={5}
className="mb-3" className="mb-4"
disabled={isProcessing || !isConnected} disabled={isProcessing || !isConnected}
/> />
)} )}
{/* Text Input and Controls - with drag and drop support */} {/* Selected Images Preview */}
{selectedImages.length > 0 && (
<div className="mb-4 space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-foreground">
{selectedImages.length} image
{selectedImages.length > 1 ? "s" : ""} attached
</p>
<button
onClick={() => setSelectedImages([])}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
disabled={isProcessing}
>
Clear all
</button>
</div>
<div className="flex flex-wrap gap-2">
{selectedImages.map((image) => (
<div
key={image.id}
className="group relative rounded-lg border border-border bg-muted/30 p-2 flex items-center gap-2 hover:border-primary/30 transition-colors"
>
{/* Image thumbnail */}
<div className="w-8 h-8 rounded-md overflow-hidden bg-muted flex-shrink-0">
<img
src={image.data}
alt={image.filename}
className="w-full h-full object-cover"
/>
</div>
{/* Image info */}
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-foreground truncate max-w-24">
{image.filename}
</p>
<p className="text-[10px] text-muted-foreground">
{formatFileSize(image.size)}
</p>
</div>
{/* Remove button */}
<button
onClick={() => removeImage(image.id)}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded-full hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
disabled={isProcessing}
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
</div>
)}
{/* Text Input and Controls */}
<div <div
className={cn( className={cn(
"flex gap-2 transition-all duration-200 rounded-lg", "flex gap-2 transition-all duration-200 rounded-xl p-1",
isDragOver && isDragOver && "bg-primary/5 ring-2 ring-primary/30"
"bg-primary/10 ring-2 ring-primary ring-offset-2 ring-offset-background"
)} )}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
@@ -684,21 +721,19 @@ export function AgentView() {
disabled={isProcessing || !isConnected} disabled={isProcessing || !isConnected}
data-testid="agent-input" data-testid="agent-input"
className={cn( className={cn(
"bg-input border-border", "h-11 bg-background border-border rounded-xl pl-4 pr-20 text-sm transition-all",
selectedImages.length > 0 && "focus:ring-2 focus:ring-primary/20 focus:border-primary/50",
"border-primary/50 bg-primary/5", selectedImages.length > 0 && "border-primary/30",
isDragOver && isDragOver && "border-primary bg-primary/5"
"border-primary bg-primary/10"
)} )}
/> />
{selectedImages.length > 0 && !isDragOver && ( {selectedImages.length > 0 && !isDragOver && (
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-primary-foreground bg-primary px-2 py-1 rounded"> <div className="absolute right-3 top-1/2 -translate-y-1/2 text-xs bg-primary text-primary-foreground px-2 py-0.5 rounded-full font-medium">
{selectedImages.length} image {selectedImages.length} image{selectedImages.length > 1 ? "s" : ""}
{selectedImages.length > 1 ? "s" : ""}
</div> </div>
)} )}
{isDragOver && ( {isDragOver && (
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-primary-foreground bg-primary px-2 py-1 rounded flex items-center gap-1"> <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1.5 text-xs text-primary font-medium">
<Paperclip className="w-3 h-3" /> <Paperclip className="w-3 h-3" />
Drop here Drop here
</div> </div>
@@ -708,13 +743,13 @@ export function AgentView() {
{/* Image Attachment Button */} {/* Image Attachment Button */}
<Button <Button
variant="outline" variant="outline"
size="default" size="icon"
onClick={toggleImageDropZone} onClick={toggleImageDropZone}
disabled={isProcessing || !isConnected} disabled={isProcessing || !isConnected}
className={cn( className={cn(
showImageDropZone && "h-11 w-11 rounded-xl border-border",
"bg-primary/20 text-primary border-primary", showImageDropZone && "bg-primary/10 text-primary border-primary/30",
selectedImages.length > 0 && "border-primary" selectedImages.length > 0 && "border-primary/30 text-primary"
)} )}
title="Attach images" title="Attach images"
> >
@@ -729,64 +764,17 @@ export function AgentView() {
isProcessing || isProcessing ||
!isConnected !isConnected
} }
className="h-11 px-4 rounded-xl"
data-testid="send-message" data-testid="send-message"
> >
<Send className="w-4 h-4" /> <Send className="w-4 h-4" />
</Button> </Button>
</div> </div>
{/* Selected Images Preview */} {/* Keyboard hint */}
{selectedImages.length > 0 && ( <p className="text-[11px] text-muted-foreground mt-2 text-center">
<div className="space-y-2"> Press <kbd className="px-1.5 py-0.5 bg-muted rounded text-[10px] font-medium">Enter</kbd> to send
<div className="flex items-center justify-between"> </p>
<p className="text-xs font-medium text-foreground">
{selectedImages.length} image
{selectedImages.length > 1 ? "s" : ""} attached
</p>
<button
onClick={() => setSelectedImages([])}
className="text-xs text-muted-foreground hover:text-foreground"
disabled={isProcessing}
>
Clear all
</button>
</div>
<div className="flex flex-wrap gap-2">
{selectedImages.map((image) => (
<div
key={image.id}
className="relative group rounded-md border border-muted bg-muted/50 p-2 flex items-center space-x-2"
>
{/* Image thumbnail */}
<div className="w-8 h-8 rounded overflow-hidden bg-muted flex-shrink-0">
<img
src={image.data}
alt={image.filename}
className="w-full h-full object-cover"
/>
</div>
{/* Image info */}
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-foreground truncate">
{image.filename}
</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(image.size)}
</p>
</div>
{/* Remove button */}
<button
onClick={() => removeImage(image.id)}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded-full hover:bg-destructive hover:text-destructive-foreground text-muted-foreground"
disabled={isProcessing}
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -110,11 +110,11 @@ import { useWindowState } from "@/hooks/use-window-state";
type ColumnId = Feature["status"]; type ColumnId = Feature["status"];
const COLUMNS: { id: ColumnId; title: string; color: string }[] = [ const COLUMNS: { id: ColumnId; title: string; colorClass: string }[] = [
{ id: "backlog", title: "Backlog", color: "bg-zinc-500" }, { id: "backlog", title: "Backlog", colorClass: "bg-[var(--status-backlog)]" },
{ id: "in_progress", title: "In Progress", color: "bg-yellow-500" }, { id: "in_progress", title: "In Progress", colorClass: "bg-[var(--status-in-progress)]" },
{ id: "waiting_approval", title: "Waiting Approval", color: "bg-orange-500" }, { id: "waiting_approval", title: "Waiting Approval", colorClass: "bg-[var(--status-waiting)]" },
{ id: "verified", title: "Verified", color: "bg-green-500" }, { id: "verified", title: "Verified", colorClass: "bg-[var(--status-success)]" },
]; ];
type ModelOption = { type ModelOption = {
@@ -2008,7 +2008,7 @@ export function BoardView() {
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<div className="flex gap-4 h-full min-w-max"> <div className="flex gap-5 h-full min-w-max py-1">
{COLUMNS.map((column) => { {COLUMNS.map((column) => {
const columnFeatures = getColumnFeatures(column.id); const columnFeatures = getColumnFeatures(column.id);
return ( return (
@@ -2016,7 +2016,7 @@ export function BoardView() {
key={column.id} key={column.id}
id={column.id} id={column.id}
title={column.title} title={column.title}
color={column.color} colorClass={column.colorClass}
count={columnFeatures.length} count={columnFeatures.length}
opacity={backgroundSettings.columnOpacity} opacity={backgroundSettings.columnOpacity}
showBorder={backgroundSettings.columnBorderEnabled} showBorder={backgroundSettings.columnBorderEnabled}
@@ -2131,14 +2131,17 @@ export function BoardView() {
})} })}
</div> </div>
<DragOverlay> <DragOverlay dropAnimation={{
duration: 200,
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
}}>
{activeFeature && ( {activeFeature && (
<Card className="w-72 opacity-90 rotate-3 shadow-xl"> <Card className="w-72 rotate-2 shadow-2xl shadow-black/25 border-primary/50 bg-card/95 backdrop-blur-sm transition-transform">
<CardHeader className="p-3"> <CardHeader className="p-3">
<CardTitle className="text-sm"> <CardTitle className="text-sm font-medium line-clamp-2">
{activeFeature.description} {activeFeature.description}
</CardTitle> </CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs text-muted-foreground">
{activeFeature.category} {activeFeature.category}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -57,7 +57,6 @@ import {
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
Brain, Brain,
Flag,
} from "lucide-react"; } from "lucide-react";
import { CountUpTimer } from "@/components/ui/count-up-timer"; import { CountUpTimer } from "@/components/ui/count-up-timer";
import { getElectronAPI } from "@/lib/electron"; import { getElectronAPI } from "@/lib/electron";
@@ -90,33 +89,6 @@ function formatThinkingLevel(level: ThinkingLevel | undefined): string {
return labels[level]; return labels[level];
} }
/**
* Formats priority for display
*/
function formatPriority(priority: number | undefined): string | null {
if (!priority) return null;
const labels: Record<number, string> = {
1: "High",
2: "Medium",
3: "Low",
};
return labels[priority] || null;
}
/**
* Gets priority badge color classes
*/
function getPriorityBadgeClasses(priority: number | undefined): string {
if (priority === 1) {
return "bg-red-500/20 border border-red-500/50 text-red-400";
} else if (priority === 2) {
return "bg-yellow-500/20 border border-yellow-500/50 text-yellow-400";
} else if (priority === 3) {
return "bg-blue-500/20 border border-blue-500/50 text-blue-400";
}
return "";
}
interface KanbanCardProps { interface KanbanCardProps {
feature: Feature; feature: Feature;
onEdit: () => void; onEdit: () => void;
@@ -134,17 +106,11 @@ interface KanbanCardProps {
hasContext?: boolean; hasContext?: boolean;
isCurrentAutoTask?: boolean; isCurrentAutoTask?: boolean;
shortcutKey?: string; shortcutKey?: string;
/** Context content for extracting progress info */
contextContent?: string; contextContent?: string;
/** Feature summary from agent completion */
summary?: string; summary?: string;
/** Opacity percentage (0-100) */
opacity?: number; opacity?: number;
/** Whether to use glassmorphism (backdrop-blur) effect */
glassmorphism?: boolean; glassmorphism?: boolean;
/** Whether to show card borders */
cardBorderEnabled?: boolean; cardBorderEnabled?: boolean;
/** Card border opacity percentage (0-100) */
cardBorderOpacity?: number; cardBorderOpacity?: number;
} }
@@ -180,16 +146,13 @@ export const KanbanCard = memo(function KanbanCard({
const [currentTime, setCurrentTime] = useState(() => Date.now()); const [currentTime, setCurrentTime] = useState(() => Date.now());
const { kanbanCardDetailLevel } = useAppStore(); const { kanbanCardDetailLevel } = useAppStore();
// Check if feature has worktree
const hasWorktree = !!feature.branchName; const hasWorktree = !!feature.branchName;
// Helper functions to check what should be shown based on detail level
const showSteps = const showSteps =
kanbanCardDetailLevel === "standard" || kanbanCardDetailLevel === "standard" ||
kanbanCardDetailLevel === "detailed"; kanbanCardDetailLevel === "detailed";
const showAgentInfo = kanbanCardDetailLevel === "detailed"; const showAgentInfo = kanbanCardDetailLevel === "detailed";
// Helper to check if "just finished" badge should be shown (within 2 minutes)
const isJustFinished = useMemo(() => { const isJustFinished = useMemo(() => {
if ( if (
!feature.justFinishedAt || !feature.justFinishedAt ||
@@ -199,26 +162,23 @@ export const KanbanCard = memo(function KanbanCard({
return false; return false;
} }
const finishedTime = new Date(feature.justFinishedAt).getTime(); const finishedTime = new Date(feature.justFinishedAt).getTime();
const twoMinutes = 2 * 60 * 1000; // 2 minutes in milliseconds const twoMinutes = 2 * 60 * 1000;
return currentTime - finishedTime < twoMinutes; return currentTime - finishedTime < twoMinutes;
}, [feature.justFinishedAt, feature.status, feature.error, currentTime]); }, [feature.justFinishedAt, feature.status, feature.error, currentTime]);
// Update current time periodically to check if badge should be hidden
useEffect(() => { useEffect(() => {
if (!feature.justFinishedAt || feature.status !== "waiting_approval") { if (!feature.justFinishedAt || feature.status !== "waiting_approval") {
return; return;
} }
const finishedTime = new Date(feature.justFinishedAt).getTime(); const finishedTime = new Date(feature.justFinishedAt).getTime();
const twoMinutes = 2 * 60 * 1000; // 2 minutes in milliseconds const twoMinutes = 2 * 60 * 1000;
const timeRemaining = twoMinutes - (currentTime - finishedTime); const timeRemaining = twoMinutes - (currentTime - finishedTime);
if (timeRemaining <= 0) { if (timeRemaining <= 0) {
// Already past 2 minutes
return; return;
} }
// Update time every second to check if 2 minutes have passed
const interval = setInterval(() => { const interval = setInterval(() => {
setCurrentTime(Date.now()); setCurrentTime(Date.now());
}, 1000); }, 1000);
@@ -226,45 +186,14 @@ export const KanbanCard = memo(function KanbanCard({
return () => clearInterval(interval); return () => clearInterval(interval);
}, [feature.justFinishedAt, feature.status, currentTime]); }, [feature.justFinishedAt, feature.status, currentTime]);
// Calculate priority badge position
const priorityLabel = formatPriority(feature.priority);
const hasPriority = !!priorityLabel;
// Calculate top position for badges (stacking vertically)
const getBadgeTopPosition = (badgeIndex: number) => {
return badgeIndex === 0
? "top-2"
: badgeIndex === 1
? "top-8"
: badgeIndex === 2
? "top-14"
: "top-20";
};
// Determine badge positions (must be after isJustFinished is defined)
let badgeIndex = 0;
const priorityBadgeIndex = hasPriority ? badgeIndex++ : -1;
const skipTestsBadgeIndex =
feature.skipTests && !feature.error ? badgeIndex++ : -1;
const errorBadgeIndex = feature.error ? badgeIndex++ : -1;
const justFinishedBadgeIndex = isJustFinished ? badgeIndex++ : -1;
const branchBadgeIndex =
hasWorktree && !isCurrentAutoTask ? badgeIndex++ : -1;
// Total number of badges displayed
const totalBadgeCount = badgeIndex;
// Load context file for in_progress, waiting_approval, and verified features
useEffect(() => { useEffect(() => {
const loadContext = async () => { const loadContext = async () => {
// Use provided context or load from file
if (contextContent) { if (contextContent) {
const info = parseAgentContext(contextContent); const info = parseAgentContext(contextContent);
setAgentInfo(info); setAgentInfo(info);
return; return;
} }
// Only load for non-backlog features
if (feature.status === "backlog") { if (feature.status === "backlog") {
setAgentInfo(null); setAgentInfo(null);
return; return;
@@ -276,7 +205,6 @@ export const KanbanCard = memo(function KanbanCard({
const currentProject = (window as any).__currentProject; const currentProject = (window as any).__currentProject;
if (!currentProject?.path) return; if (!currentProject?.path) return;
// Use features API to get agent output
if (api.features) { if (api.features) {
const result = await api.features.getAgentOutput( const result = await api.features.getAgentOutput(
currentProject.path, currentProject.path,
@@ -288,7 +216,6 @@ export const KanbanCard = memo(function KanbanCard({
setAgentInfo(info); setAgentInfo(info);
} }
} else { } else {
// Fallback to direct file read for backward compatibility
const contextPath = `${currentProject.path}/.automaker/features/${feature.id}/agent-output.md`; const contextPath = `${currentProject.path}/.automaker/features/${feature.id}/agent-output.md`;
const result = await api.readFile(contextPath); const result = await api.readFile(contextPath);
@@ -298,14 +225,12 @@ export const KanbanCard = memo(function KanbanCard({
} }
} }
} catch { } catch {
// Context file might not exist
console.debug("[KanbanCard] No context file for feature:", feature.id); console.debug("[KanbanCard] No context file for feature:", feature.id);
} }
}; };
loadContext(); loadContext();
// Reload context periodically while feature is running
if (isCurrentAutoTask) { if (isCurrentAutoTask) {
const interval = setInterval(loadContext, 3000); const interval = setInterval(loadContext, 3000);
return () => clearInterval(interval); return () => clearInterval(interval);
@@ -321,12 +246,6 @@ export const KanbanCard = memo(function KanbanCard({
onDelete(); onDelete();
}; };
// Dragging logic:
// - Backlog items can always be dragged
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
// - waiting_approval items can always be dragged (to allow manual verification via drag)
// - verified items can always be dragged (to allow moving back to waiting_approval or backlog)
// - Non-skipTests (TDD) items in progress cannot be dragged (they are running)
const isDraggable = const isDraggable =
feature.status === "backlog" || feature.status === "backlog" ||
feature.status === "waiting_approval" || feature.status === "waiting_approval" ||
@@ -350,15 +269,11 @@ export const KanbanCard = memo(function KanbanCard({
opacity: isDragging ? 0.5 : undefined, opacity: isDragging ? 0.5 : undefined,
}; };
// Calculate border style based on enabled state and opacity
const borderStyle: React.CSSProperties = { ...style }; const borderStyle: React.CSSProperties = { ...style };
if (!cardBorderEnabled) { if (!cardBorderEnabled) {
(borderStyle as Record<string, string>).borderWidth = "0px"; (borderStyle as Record<string, string>).borderWidth = "0px";
(borderStyle as Record<string, string>).borderColor = "transparent"; (borderStyle as Record<string, string>).borderColor = "transparent";
} else if (cardBorderOpacity !== 100) { } else if (cardBorderOpacity !== 100) {
// Apply border opacity using color-mix to blend the border color with transparent
// The --border variable uses oklch format, so we use color-mix in oklch space
// Ensure border width is set (1px is the default Tailwind border width)
(borderStyle as Record<string, string>).borderWidth = "1px"; (borderStyle as Record<string, string>).borderWidth = "1px";
( (
borderStyle as Record<string, string> borderStyle as Record<string, string>
@@ -370,28 +285,27 @@ export const KanbanCard = memo(function KanbanCard({
ref={setNodeRef} ref={setNodeRef}
style={isCurrentAutoTask ? style : borderStyle} style={isCurrentAutoTask ? style : borderStyle}
className={cn( className={cn(
"cursor-grab active:cursor-grabbing transition-all relative kanban-card-content select-none", "cursor-grab active:cursor-grabbing relative kanban-card-content select-none",
// Apply border class when border is enabled and opacity is 100% "transition-all duration-200 ease-out",
// When opacity is not 100%, we use inline styles for border color // Premium shadow system
// Skip border classes when animated border is active (isCurrentAutoTask) "shadow-sm hover:shadow-md hover:shadow-black/10",
// Subtle lift on hover
"hover:-translate-y-0.5",
!isCurrentAutoTask && !isCurrentAutoTask &&
cardBorderEnabled && cardBorderEnabled &&
cardBorderOpacity === 100 && cardBorderOpacity === 100 &&
"border-border", "border-border/50",
// When border is enabled but opacity is not 100%, we still need border width
!isCurrentAutoTask && !isCurrentAutoTask &&
cardBorderEnabled && cardBorderEnabled &&
cardBorderOpacity !== 100 && cardBorderOpacity !== 100 &&
"border", "border",
// Remove default background when using opacity overlay
!isDragging && "bg-transparent", !isDragging && "bg-transparent",
// Remove default backdrop-blur-sm from Card component when glassmorphism is disabled
!glassmorphism && "backdrop-blur-[0px]!", !glassmorphism && "backdrop-blur-[0px]!",
isDragging && "scale-105 shadow-lg", isDragging && "scale-105 shadow-xl shadow-black/20 rotate-1",
// Error state border (only when not in progress) // Error state - using CSS variable
feature.error && feature.error &&
!isCurrentAutoTask && !isCurrentAutoTask &&
"border-red-500 border-2 shadow-red-500/30 shadow-lg", "border-[var(--status-error)] border-2 shadow-[var(--status-error-bg)] shadow-lg",
!isDraggable && "cursor-default" !isDraggable && "cursor-default"
)} )}
data-testid={`kanban-card-${feature.id}`} data-testid={`kanban-card-${feature.id}`}
@@ -399,7 +313,7 @@ export const KanbanCard = memo(function KanbanCard({
{...attributes} {...attributes}
{...(isDraggable ? listeners : {})} {...(isDraggable ? listeners : {})}
> >
{/* Background overlay with opacity - only affects background, not content */} {/* Background overlay with opacity */}
{!isDragging && ( {!isDragging && (
<div <div
className={cn( className={cn(
@@ -409,88 +323,85 @@ export const KanbanCard = memo(function KanbanCard({
style={{ opacity: opacity / 100 }} style={{ opacity: opacity / 100 }}
/> />
)} )}
{/* Priority badge */}
{hasPriority && ( {/* Skip Tests (Manual) indicator badge */}
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
getBadgeTopPosition(priorityBadgeIndex),
"left-2",
getPriorityBadgeClasses(feature.priority)
)}
data-testid={`priority-badge-${feature.id}`}
title={`Priority: ${priorityLabel}`}
>
<Flag className="w-3 h-3" />
<span>{priorityLabel}</span>
</div>
)}
{/* Skip Tests indicator badge */}
{feature.skipTests && !feature.error && ( {feature.skipTests && !feature.error && (
<div <TooltipProvider delayDuration={200}>
className={cn( <Tooltip>
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10", <TooltipTrigger asChild>
getBadgeTopPosition(skipTestsBadgeIndex), <div
"left-2", className={cn(
"bg-orange-500/20 border border-orange-500/50 text-orange-400" "absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
)} "top-2 left-2",
data-testid={`skip-tests-badge-${feature.id}`} "bg-[var(--status-warning-bg)] border border-[var(--status-warning)]/40 text-[var(--status-warning)]"
title="Manual verification required" )}
> data-testid={`skip-tests-badge-${feature.id}`}
<Hand className="w-3 h-3" /> >
<span>Manual</span> <Hand className="w-3 h-3" />
</div> </div>
</TooltipTrigger>
<TooltipContent side="right" className="text-xs">
<p>Manual verification required</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)} )}
{/* Error indicator badge */} {/* Error indicator badge */}
{feature.error && ( {feature.error && (
<div <TooltipProvider delayDuration={200}>
className={cn( <Tooltip>
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10", <TooltipTrigger asChild>
getBadgeTopPosition(errorBadgeIndex), <div
"left-2", className={cn(
"bg-red-500/20 border border-red-500/50 text-red-400" "absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
)} "top-2 left-2",
data-testid={`error-badge-${feature.id}`} "bg-[var(--status-error-bg)] border border-[var(--status-error)]/40 text-[var(--status-error)]"
title={feature.error} )}
> data-testid={`error-badge-${feature.id}`}
<AlertCircle className="w-3 h-3" /> >
<span>Errored</span> <AlertCircle className="w-3 h-3" />
</div> </div>
</TooltipTrigger>
<TooltipContent side="right" className="text-xs max-w-[250px]">
<p>{feature.error}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)} )}
{/* Just Finished indicator badge - shows when agent just completed work (for 2 minutes) */}
{/* Just Finished indicator badge */}
{isJustFinished && ( {isJustFinished && (
<div <div
className={cn( className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10", "absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
getBadgeTopPosition(justFinishedBadgeIndex), feature.skipTests ? "top-8 left-2" : "top-2 left-2",
"left-2", "bg-[var(--status-success-bg)] border border-[var(--status-success)]/40 text-[var(--status-success)]",
"bg-green-500/20 border border-green-500/50 text-green-400 animate-pulse" "animate-pulse"
)} )}
data-testid={`just-finished-badge-${feature.id}`} data-testid={`just-finished-badge-${feature.id}`}
title="Agent just finished working on this feature" title="Agent just finished working on this feature"
> >
<Sparkles className="w-3 h-3" /> <Sparkles className="w-3 h-3" />
<span>Fresh Baked</span>
</div> </div>
)} )}
{/* Branch badge - show when feature has a worktree */}
{/* Branch badge */}
{hasWorktree && !isCurrentAutoTask && ( {hasWorktree && !isCurrentAutoTask && (
<TooltipProvider delayDuration={300}> <TooltipProvider delayDuration={300}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div <div
className={cn( className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10 cursor-default", "absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10 cursor-default",
"bg-purple-500/20 border border-purple-500/50 text-purple-400", "bg-[var(--status-info-bg)] border border-[var(--status-info)]/40 text-[var(--status-info)]",
getBadgeTopPosition(branchBadgeIndex), feature.error || feature.skipTests || isJustFinished
"left-2" ? "top-8 left-2"
: "top-2 left-2"
)} )}
data-testid={`branch-badge-${feature.id}`} data-testid={`branch-badge-${feature.id}`}
> >
<GitBranch className="w-3 h-3 shrink-0" /> <GitBranch className="w-3 h-3 shrink-0" />
<span className="truncate max-w-[80px]">
{feature.branchName?.replace("feature/", "")}
</span>
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[300px]"> <TooltipContent side="bottom" className="max-w-[300px]">
@@ -501,27 +412,26 @@ export const KanbanCard = memo(function KanbanCard({
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
<CardHeader <CardHeader
className={cn( className={cn(
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout "p-3 pb-2 block",
// Add extra top padding when badges are present to prevent text overlap (feature.skipTests || feature.error || isJustFinished) && "pt-10",
// Calculate padding based on number of badges hasWorktree &&
totalBadgeCount === 1 && "pt-10", (feature.skipTests || feature.error || isJustFinished) &&
totalBadgeCount === 2 && "pt-14", "pt-14"
totalBadgeCount === 3 && "pt-20",
totalBadgeCount >= 4 && "pt-24"
)} )}
> >
{isCurrentAutoTask && ( {isCurrentAutoTask && (
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5"> <div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-[var(--status-in-progress)]/15 border border-[var(--status-in-progress)]/50 rounded-md px-2 py-0.5">
<Loader2 className="w-4 h-4 text-running-indicator animate-spin" /> <Loader2 className="w-3.5 h-3.5 text-[var(--status-in-progress)] animate-spin" />
<span className="text-xs text-running-indicator font-medium"> <span className="text-[10px] text-[var(--status-in-progress)] font-medium">
{formatModelName(feature.model ?? DEFAULT_MODEL)} {formatModelName(feature.model ?? DEFAULT_MODEL)}
</span> </span>
{feature.startedAt && ( {feature.startedAt && (
<CountUpTimer <CountUpTimer
startedAt={feature.startedAt} startedAt={feature.startedAt}
className="text-running-indicator" className="text-[var(--status-in-progress)] text-[10px]"
/> />
)} )}
</div> </div>
@@ -533,21 +443,22 @@ export const KanbanCard = memo(function KanbanCard({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 hover:bg-white/10" className="h-6 w-6 p-0 hover:bg-muted/80 rounded-md"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
data-testid={`menu-${feature.id}`} data-testid={`menu-${feature.id}`}
> >
<MoreVertical className="w-4 h-4" /> <MoreVertical className="w-3.5 h-3.5 text-muted-foreground" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end" className="w-36">
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onEdit(); onEdit();
}} }}
data-testid={`edit-feature-${feature.id}`} data-testid={`edit-feature-${feature.id}`}
className="text-xs"
> >
<Edit className="w-3 h-3 mr-2" /> <Edit className="w-3 h-3 mr-2" />
Edit Edit
@@ -559,13 +470,14 @@ export const KanbanCard = memo(function KanbanCard({
onViewOutput(); onViewOutput();
}} }}
data-testid={`view-logs-${feature.id}`} data-testid={`view-logs-${feature.id}`}
className="text-xs"
> >
<FileText className="w-3 h-3 mr-2" /> <FileText className="w-3 h-3 mr-2" />
Logs View Logs
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuItem <DropdownMenuItem
className="text-destructive focus:text-destructive" className="text-xs text-destructive focus:text-destructive"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDeleteClick(e as unknown as React.MouseEvent); handleDeleteClick(e as unknown as React.MouseEvent);
@@ -582,22 +494,21 @@ export const KanbanCard = memo(function KanbanCard({
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
{isDraggable && ( {isDraggable && (
<div <div
className="-ml-2 -mt-1 p-2 touch-none" className="-ml-2 -mt-1 p-2 touch-none opacity-40 hover:opacity-70 transition-opacity"
data-testid={`drag-handle-${feature.id}`} data-testid={`drag-handle-${feature.id}`}
> >
<GripVertical className="w-4 h-4 text-muted-foreground" /> <GripVertical className="w-3.5 h-3.5 text-muted-foreground" />
</div> </div>
)} )}
<div className="flex-1 min-w-0 overflow-hidden"> <div className="flex-1 min-w-0 overflow-hidden">
<CardTitle <CardTitle
className={cn( className={cn(
"text-sm leading-tight break-words hyphens-auto overflow-hidden", "text-sm leading-snug break-words hyphens-auto overflow-hidden font-medium text-foreground/90",
!isDescriptionExpanded && "line-clamp-3" !isDescriptionExpanded && "line-clamp-3"
)} )}
> >
{feature.description || feature.summary || feature.id} {feature.description || feature.summary || feature.id}
</CardTitle> </CardTitle>
{/* Show More/Less toggle - only show when description is likely truncated */}
{(feature.description || feature.summary || "").length > 100 && ( {(feature.description || feature.summary || "").length > 100 && (
<button <button
onClick={(e) => { onClick={(e) => {
@@ -605,41 +516,42 @@ export const KanbanCard = memo(function KanbanCard({
setIsDescriptionExpanded(!isDescriptionExpanded); setIsDescriptionExpanded(!isDescriptionExpanded);
}} }}
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
className="flex items-center gap-0.5 text-[10px] text-muted-foreground hover:text-foreground mt-1 transition-colors" className="flex items-center gap-0.5 text-[10px] text-muted-foreground/70 hover:text-muted-foreground mt-1.5 transition-colors"
data-testid={`toggle-description-${feature.id}`} data-testid={`toggle-description-${feature.id}`}
> >
{isDescriptionExpanded ? ( {isDescriptionExpanded ? (
<> <>
<ChevronUp className="w-3 h-3" /> <ChevronUp className="w-3 h-3" />
<span>Show Less</span> <span>Less</span>
</> </>
) : ( ) : (
<> <>
<ChevronDown className="w-3 h-3" /> <ChevronDown className="w-3 h-3" />
<span>Show More</span> <span>More</span>
</> </>
)} )}
</button> </button>
)} )}
<CardDescription className="text-xs mt-1 truncate"> <CardDescription className="text-[11px] mt-1.5 truncate text-muted-foreground/70">
{feature.category} {feature.category}
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-3 pt-0"> <CardContent className="p-3 pt-0">
{/* Steps Preview - Show in Standard and Detailed modes */} {/* Steps Preview */}
{showSteps && feature.steps && feature.steps.length > 0 && ( {showSteps && feature.steps && feature.steps.length > 0 && (
<div className="mb-3 space-y-1"> <div className="mb-3 space-y-1.5">
{feature.steps.slice(0, 3).map((step, index) => ( {feature.steps.slice(0, 3).map((step, index) => (
<div <div
key={index} key={index}
className="flex items-start gap-2 text-xs text-muted-foreground" className="flex items-start gap-2 text-[11px] text-muted-foreground/80"
> >
{feature.status === "verified" ? ( {feature.status === "verified" ? (
<CheckCircle2 className="w-3 h-3 mt-0.5 text-green-500 shrink-0" /> <CheckCircle2 className="w-3 h-3 mt-0.5 text-[var(--status-success)] shrink-0" />
) : ( ) : (
<Circle className="w-3 h-3 mt-0.5 shrink-0" /> <Circle className="w-3 h-3 mt-0.5 shrink-0 text-muted-foreground/50" />
)} )}
<span className="break-words hyphens-auto line-clamp-2 leading-relaxed"> <span className="break-words hyphens-auto line-clamp-2 leading-relaxed">
{step} {step}
@@ -647,18 +559,18 @@ export const KanbanCard = memo(function KanbanCard({
</div> </div>
))} ))}
{feature.steps.length > 3 && ( {feature.steps.length > 3 && (
<p className="text-xs text-muted-foreground pl-5"> <p className="text-[10px] text-muted-foreground/60 pl-5">
+{feature.steps.length - 3} more steps +{feature.steps.length - 3} more
</p> </p>
)} )}
</div> </div>
)} )}
{/* Model/Preset Info for Backlog Cards - Show in Detailed mode */} {/* Model/Preset Info for Backlog Cards */}
{showAgentInfo && feature.status === "backlog" && ( {showAgentInfo && feature.status === "backlog" && (
<div className="mb-3 space-y-2 overflow-hidden"> <div className="mb-3 space-y-2 overflow-hidden">
<div className="flex items-center gap-2 text-xs flex-wrap"> <div className="flex items-center gap-2 text-[11px] flex-wrap">
<div className="flex items-center gap-1 text-cyan-400"> <div className="flex items-center gap-1 text-[var(--status-info)]">
<Cpu className="w-3 h-3" /> <Cpu className="w-3 h-3" />
<span className="font-medium"> <span className="font-medium">
{formatModelName(feature.model ?? DEFAULT_MODEL)} {formatModelName(feature.model ?? DEFAULT_MODEL)}
@@ -676,13 +588,12 @@ export const KanbanCard = memo(function KanbanCard({
</div> </div>
)} )}
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */} {/* Agent Info Panel */}
{/* Detailed mode: Show all agent info */}
{showAgentInfo && feature.status !== "backlog" && agentInfo && ( {showAgentInfo && feature.status !== "backlog" && agentInfo && (
<div className="mb-3 space-y-2 overflow-hidden"> <div className="mb-3 space-y-2 overflow-hidden">
{/* Model & Phase */} {/* Model & Phase */}
<div className="flex items-center gap-2 text-xs flex-wrap"> <div className="flex items-center gap-2 text-[11px] flex-wrap">
<div className="flex items-center gap-1 text-cyan-400"> <div className="flex items-center gap-1 text-[var(--status-info)]">
<Cpu className="w-3 h-3" /> <Cpu className="w-3 h-3" />
<span className="font-medium"> <span className="font-medium">
{formatModelName(feature.model ?? DEFAULT_MODEL)} {formatModelName(feature.model ?? DEFAULT_MODEL)}
@@ -691,13 +602,13 @@ export const KanbanCard = memo(function KanbanCard({
{agentInfo.currentPhase && ( {agentInfo.currentPhase && (
<div <div
className={cn( className={cn(
"px-1.5 py-0.5 rounded text-[10px] font-medium", "px-1.5 py-0.5 rounded-md text-[10px] font-medium",
agentInfo.currentPhase === "planning" && agentInfo.currentPhase === "planning" &&
"bg-blue-500/20 text-blue-400", "bg-[var(--status-info-bg)] text-[var(--status-info)]",
agentInfo.currentPhase === "action" && agentInfo.currentPhase === "action" &&
"bg-amber-500/20 text-amber-400", "bg-[var(--status-warning-bg)] text-[var(--status-warning)]",
agentInfo.currentPhase === "verification" && agentInfo.currentPhase === "verification" &&
"bg-green-500/20 text-green-400" "bg-[var(--status-success-bg)] text-[var(--status-success)]"
)} )}
> >
{agentInfo.currentPhase} {agentInfo.currentPhase}
@@ -705,10 +616,10 @@ export const KanbanCard = memo(function KanbanCard({
)} )}
</div> </div>
{/* Task List Progress (if todos found) */} {/* Task List Progress */}
{agentInfo.todos.length > 0 && ( {agentInfo.todos.length > 0 && (
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-1 text-[10px] text-muted-foreground"> <div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
<ListTodo className="w-3 h-3" /> <ListTodo className="w-3 h-3" />
<span> <span>
{ {
@@ -725,20 +636,20 @@ export const KanbanCard = memo(function KanbanCard({
className="flex items-center gap-1.5 text-[10px]" className="flex items-center gap-1.5 text-[10px]"
> >
{todo.status === "completed" ? ( {todo.status === "completed" ? (
<CheckCircle2 className="w-2.5 h-2.5 text-green-500 shrink-0" /> <CheckCircle2 className="w-2.5 h-2.5 text-[var(--status-success)] shrink-0" />
) : todo.status === "in_progress" ? ( ) : todo.status === "in_progress" ? (
<Loader2 className="w-2.5 h-2.5 text-amber-400 animate-spin shrink-0" /> <Loader2 className="w-2.5 h-2.5 text-[var(--status-warning)] animate-spin shrink-0" />
) : ( ) : (
<Circle className="w-2.5 h-2.5 text-muted-foreground shrink-0" /> <Circle className="w-2.5 h-2.5 text-muted-foreground/50 shrink-0" />
)} )}
<span <span
className={cn( className={cn(
"break-words hyphens-auto line-clamp-2 leading-relaxed", "break-words hyphens-auto line-clamp-2 leading-relaxed",
todo.status === "completed" && todo.status === "completed" &&
"text-muted-foreground line-through", "text-muted-foreground/60 line-through",
todo.status === "in_progress" && "text-amber-400", todo.status === "in_progress" && "text-[var(--status-warning)]",
todo.status === "pending" && todo.status === "pending" &&
"text-foreground-secondary" "text-muted-foreground/80"
)} )}
> >
{todo.content} {todo.content}
@@ -746,7 +657,7 @@ export const KanbanCard = memo(function KanbanCard({
</div> </div>
))} ))}
{agentInfo.todos.length > 3 && ( {agentInfo.todos.length > 3 && (
<p className="text-[10px] text-muted-foreground pl-4"> <p className="text-[10px] text-muted-foreground/60 pl-4">
+{agentInfo.todos.length - 3} more +{agentInfo.todos.length - 3} more
</p> </p>
)} )}
@@ -754,16 +665,16 @@ export const KanbanCard = memo(function KanbanCard({
</div> </div>
)} )}
{/* Summary for waiting_approval and verified - prioritize feature.summary from UpdateFeatureStatus */} {/* Summary for waiting_approval and verified */}
{(feature.status === "waiting_approval" || {(feature.status === "waiting_approval" ||
feature.status === "verified") && ( feature.status === "verified") && (
<> <>
{(feature.summary || summary || agentInfo.summary) && ( {(feature.summary || summary || agentInfo.summary) && (
<div className="space-y-1 pt-1 border-t border-border-glass overflow-hidden"> <div className="space-y-1.5 pt-2 border-t border-border/30 overflow-hidden">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1 text-[10px] text-green-400 min-w-0"> <div className="flex items-center gap-1 text-[10px] text-[var(--status-success)] min-w-0">
<Sparkles className="w-3 h-3 shrink-0" /> <Sparkles className="w-3 h-3 shrink-0" />
<span className="truncate">Summary</span> <span className="truncate font-medium">Summary</span>
</div> </div>
<button <button
onClick={(e) => { onClick={(e) => {
@@ -771,31 +682,30 @@ export const KanbanCard = memo(function KanbanCard({
setIsSummaryDialogOpen(true); setIsSummaryDialogOpen(true);
}} }}
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
className="p-0.5 rounded hover:bg-accent transition-colors text-muted-foreground hover:text-foreground shrink-0" className="p-0.5 rounded-md hover:bg-muted/80 transition-colors text-muted-foreground/60 hover:text-muted-foreground shrink-0"
title="View full summary" title="View full summary"
data-testid={`expand-summary-${feature.id}`} data-testid={`expand-summary-${feature.id}`}
> >
<Expand className="w-3 h-3" /> <Expand className="w-3 h-3" />
</button> </button>
</div> </div>
<p className="text-[10px] text-foreground-secondary line-clamp-3 break-words hyphens-auto leading-relaxed overflow-hidden"> <p className="text-[10px] text-muted-foreground/70 line-clamp-3 break-words hyphens-auto leading-relaxed overflow-hidden">
{feature.summary || summary || agentInfo.summary} {feature.summary || summary || agentInfo.summary}
</p> </p>
</div> </div>
)} )}
{/* Show tool count even without summary */}
{!feature.summary && {!feature.summary &&
!summary && !summary &&
!agentInfo.summary && !agentInfo.summary &&
agentInfo.toolCallCount > 0 && ( agentInfo.toolCallCount > 0 && (
<div className="flex items-center gap-2 text-[10px] text-muted-foreground pt-1 border-t border-border-glass"> <div className="flex items-center gap-2 text-[10px] text-muted-foreground/60 pt-2 border-t border-border/30">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Wrench className="w-2.5 h-2.5" /> <Wrench className="w-2.5 h-2.5" />
{agentInfo.toolCallCount} tool calls {agentInfo.toolCallCount} tool calls
</span> </span>
{agentInfo.todos.length > 0 && ( {agentInfo.todos.length > 0 && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<CheckCircle2 className="w-2.5 h-2.5 text-green-500" /> <CheckCircle2 className="w-2.5 h-2.5 text-[var(--status-success)]" />
{ {
agentInfo.todos.filter( agentInfo.todos.filter(
(t) => t.status === "completed" (t) => t.status === "completed"
@@ -812,14 +722,14 @@ export const KanbanCard = memo(function KanbanCard({
)} )}
{/* Actions */} {/* Actions */}
<div className="flex gap-2"> <div className="flex gap-1.5">
{isCurrentAutoTask && ( {isCurrentAutoTask && (
<> <>
{onViewOutput && ( {onViewOutput && (
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
className="flex-1 h-7 text-xs bg-action-view hover:bg-action-view-hover" className="flex-1 h-7 text-[11px] bg-[var(--status-info)] hover:bg-[var(--status-info)]/90"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onViewOutput(); onViewOutput();
@@ -831,7 +741,7 @@ export const KanbanCard = memo(function KanbanCard({
Logs Logs
{shortcutKey && ( {shortcutKey && (
<span <span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20" className="ml-1.5 px-1 py-0.5 text-[9px] font-mono rounded bg-white/20"
data-testid={`shortcut-key-${feature.id}`} data-testid={`shortcut-key-${feature.id}`}
> >
{shortcutKey} {shortcutKey}
@@ -843,7 +753,7 @@ export const KanbanCard = memo(function KanbanCard({
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
className="h-7 text-xs" className="h-7 text-[11px] px-2"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onForceStop(); onForceStop();
@@ -851,20 +761,18 @@ export const KanbanCard = memo(function KanbanCard({
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
data-testid={`force-stop-${feature.id}`} data-testid={`force-stop-${feature.id}`}
> >
<StopCircle className="w-3 h-3 mr-1" /> <StopCircle className="w-3 h-3" />
Stop
</Button> </Button>
)} )}
</> </>
)} )}
{!isCurrentAutoTask && feature.status === "in_progress" && ( {!isCurrentAutoTask && feature.status === "in_progress" && (
<> <>
{/* skipTests features show manual verify button */}
{feature.skipTests && onManualVerify ? ( {feature.skipTests && onManualVerify ? (
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
className="flex-1 h-7 text-xs bg-primary hover:bg-primary/90" className="flex-1 h-7 text-[11px]"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onManualVerify(); onManualVerify();
@@ -879,7 +787,7 @@ export const KanbanCard = memo(function KanbanCard({
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
className="flex-1 h-7 text-xs bg-action-verify hover:bg-action-verify-hover" className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onResume(); onResume();
@@ -894,7 +802,7 @@ export const KanbanCard = memo(function KanbanCard({
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
className="flex-1 h-7 text-xs bg-action-verify hover:bg-action-verify-hover" className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onVerify(); onVerify();
@@ -910,7 +818,7 @@ export const KanbanCard = memo(function KanbanCard({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 text-xs" className="h-7 text-[11px] px-2"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onViewOutput(); onViewOutput();
@@ -918,20 +826,18 @@ export const KanbanCard = memo(function KanbanCard({
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
data-testid={`view-output-inprogress-${feature.id}`} data-testid={`view-output-inprogress-${feature.id}`}
> >
<FileText className="w-3 h-3 mr-1" /> <FileText className="w-3 h-3" />
Logs
</Button> </Button>
)} )}
</> </>
)} )}
{!isCurrentAutoTask && feature.status === "verified" && ( {!isCurrentAutoTask && feature.status === "verified" && (
<> <>
{/* Logs button if context exists */}
{hasContext && onViewOutput && ( {hasContext && onViewOutput && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 text-xs" className="h-7 text-[11px] px-2"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onViewOutput(); onViewOutput();
@@ -939,15 +845,13 @@ export const KanbanCard = memo(function KanbanCard({
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
data-testid={`view-output-verified-${feature.id}`} data-testid={`view-output-verified-${feature.id}`}
> >
<FileText className="w-3 h-3 mr-1" /> <FileText className="w-3 h-3" />
Logs
</Button> </Button>
)} )}
</> </>
)} )}
{!isCurrentAutoTask && feature.status === "waiting_approval" && ( {!isCurrentAutoTask && feature.status === "waiting_approval" && (
<> <>
{/* Revert button - only show when worktree exists (icon only to save space) */}
{hasWorktree && onRevert && ( {hasWorktree && onRevert && (
<TooltipProvider delayDuration={300}> <TooltipProvider delayDuration={300}>
<Tooltip> <Tooltip>
@@ -955,7 +859,7 @@ export const KanbanCard = memo(function KanbanCard({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 w-7 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/20 shrink-0" className="h-7 w-7 p-0 text-[var(--status-error)] hover:text-[var(--status-error)] hover:bg-[var(--status-error-bg)] shrink-0"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setIsRevertDialogOpen(true); setIsRevertDialogOpen(true);
@@ -966,18 +870,17 @@ export const KanbanCard = memo(function KanbanCard({
<Undo2 className="w-3.5 h-3.5" /> <Undo2 className="w-3.5 h-3.5" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side="top" className="text-xs">
<p>Revert changes</p> <p>Revert changes</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
{/* Follow-up prompt button */}
{onFollowUp && ( {onFollowUp && (
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
className="flex-1 h-7 text-xs min-w-0" className="flex-1 h-7 text-[11px] min-w-0"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onFollowUp(); onFollowUp();
@@ -989,12 +892,11 @@ export const KanbanCard = memo(function KanbanCard({
<span className="truncate">Follow-up</span> <span className="truncate">Follow-up</span>
</Button> </Button>
)} )}
{/* Merge button - only show when worktree exists */}
{hasWorktree && onMerge && ( {hasWorktree && onMerge && (
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
className="flex-1 h-7 text-xs bg-purple-600 hover:bg-purple-700 min-w-0" className="flex-1 h-7 text-[11px] bg-[var(--status-info)] hover:bg-[var(--status-info)]/90 min-w-0"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onMerge(); onMerge();
@@ -1007,12 +909,11 @@ export const KanbanCard = memo(function KanbanCard({
<span className="truncate">Merge</span> <span className="truncate">Merge</span>
</Button> </Button>
)} )}
{/* Commit and verify button - show when no worktree */}
{!hasWorktree && onCommit && ( {!hasWorktree && onCommit && (
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
className="flex-1 h-7 text-xs" className="flex-1 h-7 text-[11px]"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onCommit(); onCommit();
@@ -1048,7 +949,7 @@ export const KanbanCard = memo(function KanbanCard({
> >
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-green-400" /> <Sparkles className="w-5 h-5 text-[var(--status-success)]" />
Implementation Summary Implementation Summary
</DialogTitle> </DialogTitle>
<DialogDescription <DialogDescription
@@ -1064,7 +965,7 @@ export const KanbanCard = memo(function KanbanCard({
})()} })()}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border"> <div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border/50">
<Markdown> <Markdown>
{feature.summary || {feature.summary ||
summary || summary ||
@@ -1088,7 +989,7 @@ export const KanbanCard = memo(function KanbanCard({
<Dialog open={isRevertDialogOpen} onOpenChange={setIsRevertDialogOpen}> <Dialog open={isRevertDialogOpen} onOpenChange={setIsRevertDialogOpen}>
<DialogContent data-testid="revert-confirmation-dialog"> <DialogContent data-testid="revert-confirmation-dialog">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-400"> <DialogTitle className="flex items-center gap-2 text-[var(--status-error)]">
<Undo2 className="w-5 h-5" /> <Undo2 className="w-5 h-5" />
Revert Changes Revert Changes
</DialogTitle> </DialogTitle>
@@ -1098,13 +999,13 @@ export const KanbanCard = memo(function KanbanCard({
{feature.branchName && ( {feature.branchName && (
<span className="block mt-2 font-medium"> <span className="block mt-2 font-medium">
Branch{" "} Branch{" "}
<code className="bg-muted px-1 py-0.5 rounded"> <code className="bg-muted px-1.5 py-0.5 rounded text-[11px]">
{feature.branchName} {feature.branchName}
</code>{" "} </code>{" "}
will be deleted. will be deleted.
</span> </span>
)} )}
<span className="block mt-2 text-red-400 font-medium"> <span className="block mt-2 text-[var(--status-error)] font-medium">
This action cannot be undone. This action cannot be undone.
</span> </span>
</DialogDescription> </DialogDescription>

View File

@@ -8,19 +8,19 @@ import type { ReactNode } from "react";
interface KanbanColumnProps { interface KanbanColumnProps {
id: string; id: string;
title: string; title: string;
color: string; colorClass: string;
count: number; count: number;
children: ReactNode; children: ReactNode;
headerAction?: ReactNode; headerAction?: ReactNode;
opacity?: number; // Opacity percentage (0-100) - only affects background opacity?: number;
showBorder?: boolean; // Whether to show column border showBorder?: boolean;
hideScrollbar?: boolean; // Whether to hide the column scrollbar hideScrollbar?: boolean;
} }
export const KanbanColumn = memo(function KanbanColumn({ export const KanbanColumn = memo(function KanbanColumn({
id, id,
title, title,
color, colorClass,
count, count,
children, children,
headerAction, headerAction,
@@ -34,45 +34,53 @@ export const KanbanColumn = memo(function KanbanColumn({
<div <div
ref={setNodeRef} ref={setNodeRef}
className={cn( className={cn(
"relative flex flex-col h-full rounded-lg transition-colors w-72", "relative flex flex-col h-full rounded-xl transition-all duration-200 w-72",
showBorder && "border border-border" showBorder && "border border-border/60",
isOver && "ring-2 ring-primary/30 ring-offset-1 ring-offset-background"
)} )}
data-testid={`kanban-column-${id}`} data-testid={`kanban-column-${id}`}
> >
{/* Background layer with opacity - only this layer is affected by opacity */} {/* Background layer with opacity */}
<div <div
className={cn( className={cn(
"absolute inset-0 rounded-lg backdrop-blur-sm transition-colors", "absolute inset-0 rounded-xl backdrop-blur-sm transition-colors duration-200",
isOver ? "bg-accent" : "bg-card" isOver ? "bg-accent/80" : "bg-card/80"
)} )}
style={{ opacity: opacity / 100 }} style={{ opacity: opacity / 100 }}
/> />
{/* Column Header - positioned above the background */} {/* Column Header */}
<div <div
className={cn( className={cn(
"relative z-10 flex items-center gap-2 p-3", "relative z-10 flex items-center gap-3 px-3 py-2.5",
showBorder && "border-b border-border" showBorder && "border-b border-border/40"
)} )}
> >
<div className={cn("w-3 h-3 rounded-full", color)} /> <div className={cn("w-2.5 h-2.5 rounded-full shrink-0", colorClass)} />
<h3 className="font-medium text-sm flex-1">{title}</h3> <h3 className="font-semibold text-sm text-foreground/90 flex-1 tracking-tight">{title}</h3>
{headerAction} {headerAction}
<span className="text-xs text-muted-foreground bg-background px-2 py-0.5 rounded-full"> <span className="text-xs font-medium text-muted-foreground/80 bg-muted/50 px-2 py-0.5 rounded-md tabular-nums">
{count} {count}
</span> </span>
</div> </div>
{/* Column Content - positioned above the background */} {/* Column Content */}
<div <div
className={cn( className={cn(
"relative z-10 flex-1 overflow-y-auto p-2 space-y-2", "relative z-10 flex-1 overflow-y-auto p-2 space-y-2.5",
hideScrollbar && hideScrollbar &&
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]" "[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]",
// Smooth scrolling
"scroll-smooth"
)} )}
> >
{children} {children}
</div> </div>
{/* Drop zone indicator when dragging over */}
{isOver && (
<div className="absolute inset-0 rounded-xl bg-primary/5 pointer-events-none z-5 border-2 border-dashed border-primary/20" />
)}
</div> </div>
); );
}); });

View File

@@ -157,47 +157,47 @@ export function SettingsView() {
{/* Audio Section */} {/* Audio Section */}
<div <div
id="audio" id="audio"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6" className="rounded-2xl border border-border/50 bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl shadow-sm shadow-black/5 overflow-hidden scroll-mt-6"
> >
<div className="p-6 border-b border-border"> <div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-3 mb-2">
<Volume2 className="w-5 h-5 text-brand-500" /> <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<h2 className="text-lg font-semibold text-foreground"> <Volume2 className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Audio Audio
</h2> </h2>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground/80 ml-12">
Configure audio and notification settings. Configure audio and notification settings.
</p> </p>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
{/* Mute Done Sound Setting */} {/* Mute Done Sound Setting */}
<div className="space-y-3"> <div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<div className="flex items-start space-x-3"> <Checkbox
<Checkbox id="mute-done-sound"
id="mute-done-sound" checked={muteDoneSound}
checked={muteDoneSound} onCheckedChange={(checked) =>
onCheckedChange={(checked) => setMuteDoneSound(checked === true)
setMuteDoneSound(checked === true) }
} className="mt-1"
className="mt-0.5" data-testid="mute-done-sound-checkbox"
data-testid="mute-done-sound-checkbox" />
/> <div className="space-y-1.5">
<div className="space-y-1"> <Label
<Label htmlFor="mute-done-sound"
htmlFor="mute-done-sound" className="text-foreground cursor-pointer font-medium flex items-center gap-2"
className="text-foreground cursor-pointer font-medium flex items-center gap-2" >
> <VolumeX className="w-4 h-4 text-brand-500" />
<VolumeX className="w-4 h-4 text-brand-500" /> Mute notification sound when agents complete
Mute notification sound when agents complete </Label>
</Label> <p className="text-xs text-muted-foreground/80 leading-relaxed">
<p className="text-xs text-muted-foreground"> When enabled, disables the &quot;ding&quot; sound that
When enabled, disables the &quot;ding&quot; sound that plays when an agent completes a feature. The feature
plays when an agent completes a feature. The feature will still move to the completed column, but without
will still move to the completed column, but without audio notification.
audio notification. </p>
</p>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,6 +7,7 @@ import { buildProviderConfigs } from "@/config/api-providers";
import { AuthenticationStatusDisplay } from "./authentication-status-display"; import { AuthenticationStatusDisplay } from "./authentication-status-display";
import { SecurityNotice } from "./security-notice"; import { SecurityNotice } from "./security-notice";
import { useApiKeyManagement } from "./hooks/use-api-key-management"; import { useApiKeyManagement } from "./hooks/use-api-key-management";
import { cn } from "@/lib/utils";
export function ApiKeysSection() { export function ApiKeysSection() {
const { apiKeys } = useAppStore(); const { apiKeys } = useAppStore();
@@ -20,16 +21,22 @@ export function ApiKeysSection() {
return ( return (
<div <div
id="api-keys" id="api-keys"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6" className={cn(
"rounded-2xl overflow-hidden scroll-mt-6",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
)}
> >
<div className="p-6 border-b border-border"> <div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-3 mb-2">
<Key className="w-5 h-5 text-brand-500" /> <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<h2 className="text-lg font-semibold text-foreground">API Keys</h2> <Key className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">API Keys</h2>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground/80 ml-12">
Configure your AI provider API keys. Keys are stored locally in your Configure your AI provider API keys. Keys are stored locally in your browser.
browser.
</p> </p>
</div> </div>
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
@@ -53,7 +60,15 @@ export function ApiKeysSection() {
<Button <Button
onClick={handleSave} onClick={handleSave}
data-testid="save-settings" data-testid="save-settings"
className="min-w-[120px] bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0" className={cn(
"min-w-[140px] h-10",
"bg-gradient-to-r from-brand-500 to-brand-600",
"hover:from-brand-600 hover:to-brand-600",
"text-white font-medium border-0",
"shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.98]"
)}
> >
{saved ? ( {saved ? (
<> <>

View File

@@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Palette } from "lucide-react"; import { Palette } from "lucide-react";
import { themeOptions } from "@/config/theme-options"; import { themeOptions } from "@/config/theme-options";
import { cn } from "@/lib/utils";
import type { Theme, Project } from "../shared/types"; import type { Theme, Project } from "../shared/types";
interface AppearanceSectionProps { interface AppearanceSectionProps {
@@ -18,39 +19,65 @@ export function AppearanceSection({
return ( return (
<div <div
id="appearance" id="appearance"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6" className={cn(
"rounded-2xl overflow-hidden scroll-mt-6",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
)}
> >
<div className="p-6 border-b border-border"> <div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-3 mb-2">
<Palette className="w-5 h-5 text-brand-500" /> <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<h2 className="text-lg font-semibold text-foreground">Appearance</h2> <Palette className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Appearance</h2>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground/80 ml-12">
Customize the look and feel of your application. Customize the look and feel of your application.
</p> </p>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<div className="space-y-3"> <div className="space-y-4">
<Label className="text-foreground"> <Label className="text-foreground font-medium">
Theme{" "} Theme{" "}
{currentProject ? `(for ${currentProject.name})` : "(Global)"} <span className="text-muted-foreground font-normal">
{currentProject ? `(for ${currentProject.name})` : "(Global)"}
</span>
</Label> </Label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{themeOptions.map(({ value, label, Icon, testId }) => { {themeOptions.map(({ value, label, Icon, testId }) => {
const isActive = effectiveTheme === value; const isActive = effectiveTheme === value;
return ( return (
<Button <button
key={value} key={value}
variant={isActive ? "secondary" : "outline"}
onClick={() => onThemeChange(value)} onClick={() => onThemeChange(value)}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${ className={cn(
isActive ? "border-brand-500 ring-1 ring-brand-500/50" : "" "group flex items-center justify-center gap-2.5 px-4 py-3.5 rounded-xl",
}`} "text-sm font-medium transition-all duration-200 ease-out",
isActive
? [
"bg-gradient-to-br from-brand-500/15 to-brand-600/10",
"border-2 border-brand-500/40",
"text-foreground",
"shadow-md shadow-brand-500/10",
]
: [
"bg-accent/30 hover:bg-accent/50",
"border border-border/50 hover:border-border",
"text-muted-foreground hover:text-foreground",
"hover:shadow-sm",
],
"hover:scale-[1.02] active:scale-[0.98]"
)}
data-testid={testId} data-testid={testId}
> >
<Icon className="w-4 h-4" /> <Icon className={cn(
<span className="font-medium text-sm">{label}</span> "w-4 h-4 transition-all duration-200",
</Button> isActive ? "text-brand-500" : "group-hover:text-brand-400"
)} />
<span>{label}</span>
</button>
); );
})} })}
</div> </div>

View File

@@ -5,6 +5,7 @@ import {
AlertCircle, AlertCircle,
RefreshCw, RefreshCw,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils";
import type { CliStatus } from "../shared/types"; import type { CliStatus } from "../shared/types";
interface CliStatusProps { interface CliStatusProps {
@@ -23,13 +24,20 @@ export function ClaudeCliStatus({
return ( return (
<div <div
id="claude" id="claude"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6" className={cn(
"rounded-2xl overflow-hidden scroll-mt-6",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
)}
> >
<div className="p-6 border-b border-border"> <div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<Terminal className="w-5 h-5 text-brand-500" /> <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<h2 className="text-lg font-semibold text-foreground"> <Terminal className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Claude Code CLI Claude Code CLI
</h2> </h2>
</div> </div>
@@ -40,13 +48,18 @@ export function ClaudeCliStatus({
disabled={isChecking} disabled={isChecking}
data-testid="refresh-claude-cli" data-testid="refresh-claude-cli"
title="Refresh Claude CLI detection" title="Refresh Claude CLI detection"
className={cn(
"h-9 w-9 rounded-lg",
"hover:bg-accent/50 hover:scale-105",
"transition-all duration-200"
)}
> >
<RefreshCw <RefreshCw
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`} className={cn("w-4 h-4", isChecking && "animate-spin")}
/> />
</Button> </Button>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground/80 ml-12">
Claude Code CLI provides better performance for long-running tasks, Claude Code CLI provides better performance for long-running tasks,
especially with ultrathink. especially with ultrathink.
</p> </p>
@@ -54,13 +67,15 @@ export function ClaudeCliStatus({
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
{status.success && status.status === "installed" ? ( {status.success && status.status === "installed" ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20"> <div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" /> <div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<div className="flex-1"> <CheckCircle2 className="w-5 h-5 text-emerald-500" />
<p className="text-sm font-medium text-green-400"> </div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-400">
Claude Code CLI Installed Claude Code CLI Installed
</p> </p>
<div className="text-xs text-green-400/80 mt-1 space-y-1"> <div className="text-xs text-emerald-400/70 mt-1.5 space-y-0.5">
{status.method && ( {status.method && (
<p> <p>
Method: <span className="font-mono">{status.method}</span> Method: <span className="font-mono">{status.method}</span>
@@ -68,71 +83,65 @@ export function ClaudeCliStatus({
)} )}
{status.version && ( {status.version && (
<p> <p>
Version:{" "} Version: <span className="font-mono">{status.version}</span>
<span className="font-mono">{status.version}</span>
</p> </p>
)} )}
{status.path && ( {status.path && (
<p className="truncate" title={status.path}> <p className="truncate" title={status.path}>
Path:{" "} Path: <span className="font-mono text-[10px]">{status.path}</span>
<span className="font-mono text-[10px]">
{status.path}
</span>
</p> </p>
)} )}
</div> </div>
</div> </div>
</div> </div>
{status.recommendation && ( {status.recommendation && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground/70 ml-1">
{status.recommendation} {status.recommendation}
</p> </p>
)} )}
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-4">
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20"> <div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" /> <div className="w-10 h-10 rounded-xl bg-amber-500/15 flex items-center justify-center border border-amber-500/20 shrink-0 mt-0.5">
<AlertCircle className="w-5 h-5 text-amber-500" />
</div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium text-yellow-400"> <p className="text-sm font-medium text-amber-400">
Claude Code CLI Not Detected Claude Code CLI Not Detected
</p> </p>
<p className="text-xs text-yellow-400/80 mt-1"> <p className="text-xs text-amber-400/70 mt-1">
{status.recommendation || {status.recommendation ||
"Consider installing Claude Code CLI for optimal performance with ultrathink."} "Consider installing Claude Code CLI for optimal performance with ultrathink."}
</p> </p>
</div> </div>
</div> </div>
{status.installCommands && ( {status.installCommands && (
<div className="space-y-2"> <div className="space-y-3">
<p className="text-xs font-medium text-foreground-secondary"> <p className="text-xs font-medium text-foreground/80">
Installation Commands: Installation Commands:
</p> </p>
<div className="space-y-1"> <div className="space-y-2">
{status.installCommands.npm && ( {status.installCommands.npm && (
<div className="p-2 rounded bg-background border border-border-glass"> <div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-xs text-muted-foreground mb-1">npm:</p> <p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">npm</p>
<code className="text-xs text-foreground-secondary font-mono break-all"> <code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.npm} {status.installCommands.npm}
</code> </code>
</div> </div>
)} )}
{status.installCommands.macos && ( {status.installCommands.macos && (
<div className="p-2 rounded bg-background border border-border-glass"> <div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-xs text-muted-foreground mb-1"> <p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">macOS/Linux</p>
macOS/Linux: <code className="text-xs text-foreground/80 font-mono break-all">
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{status.installCommands.macos} {status.installCommands.macos}
</code> </code>
</div> </div>
)} )}
{status.installCommands.windows && ( {status.installCommands.windows && (
<div className="p-2 rounded bg-background border border-border-glass"> <div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-xs text-muted-foreground mb-1"> <p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">Windows (PowerShell)</p>
Windows (PowerShell): <code className="text-xs text-foreground/80 font-mono break-all">
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{status.installCommands.windows} {status.installCommands.windows}
</code> </code>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import { cn } from "@/lib/utils";
interface SettingsHeaderProps { interface SettingsHeaderProps {
title?: string; title?: string;
@@ -10,15 +11,24 @@ export function SettingsHeader({
description = "Configure your API keys and preferences", description = "Configure your API keys and preferences",
}: SettingsHeaderProps) { }: SettingsHeaderProps) {
return ( return (
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md"> <div className={cn(
"shrink-0",
"border-b border-border/50",
"bg-gradient-to-r from-card/90 via-card/70 to-card/80 backdrop-blur-xl"
)}>
<div className="px-8 py-6"> <div className="px-8 py-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center"> <div className={cn(
<Settings className="w-5 h-5 text-primary-foreground" /> "w-12 h-12 rounded-2xl flex items-center justify-center",
"bg-gradient-to-br from-brand-500 to-brand-600",
"shadow-lg shadow-brand-500/25",
"ring-1 ring-white/10"
)}>
<Settings className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-foreground">{title}</h1> <h1 className="text-2xl font-bold text-foreground tracking-tight">{title}</h1>
<p className="text-sm text-muted-foreground">{description}</p> <p className="text-sm text-muted-foreground/80 mt-0.5">{description}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,8 +16,12 @@ export function SettingsNavigation({
onNavigate, onNavigate,
}: SettingsNavigationProps) { }: SettingsNavigationProps) {
return ( return (
<nav className="hidden lg:block w-48 shrink-0 border-r border-border bg-card/50 backdrop-blur-sm"> <nav className={cn(
<div className="sticky top-0 p-4 space-y-1"> "hidden lg:block w-52 shrink-0",
"border-r border-border/50",
"bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl"
)}>
<div className="sticky top-0 p-4 space-y-1.5">
{navItems {navItems
.filter((item) => item.id !== "danger" || currentProject) .filter((item) => item.id !== "danger" || currentProject)
.map((item) => { .map((item) => {
@@ -28,16 +32,32 @@ export function SettingsNavigation({
key={item.id} key={item.id}
onClick={() => onNavigate(item.id)} onClick={() => onNavigate(item.id)}
className={cn( className={cn(
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all text-left", "group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden",
isActive isActive
? "bg-brand-500/10 text-brand-500 border border-brand-500/20" ? [
: "text-muted-foreground hover:text-foreground hover:bg-accent" "bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5",
"text-foreground",
"border border-brand-500/25",
"shadow-sm shadow-brand-500/5",
]
: [
"text-muted-foreground hover:text-foreground",
"hover:bg-accent/50",
"border border-transparent hover:border-border/40",
],
"hover:scale-[1.01] active:scale-[0.98]"
)} )}
> >
{/* Active indicator bar */}
{isActive && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full" />
)}
<Icon <Icon
className={cn( className={cn(
"w-4 h-4 shrink-0", "w-4 h-4 shrink-0 transition-all duration-200",
isActive ? "text-brand-500" : "" isActive
? "text-brand-500"
: "group-hover:text-brand-400 group-hover:scale-110"
)} )}
/> />
<span className="truncate">{item.label}</span> <span className="truncate">{item.label}</span>

View File

@@ -1,5 +1,6 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Trash2, Folder } from "lucide-react"; import { Trash2, Folder, AlertTriangle } from "lucide-react";
import { cn } from "@/lib/utils";
import type { Project } from "../shared/types"; import type { Project } from "../shared/types";
interface DangerZoneSectionProps { interface DangerZoneSectionProps {
@@ -16,28 +17,35 @@ export function DangerZoneSection({
return ( return (
<div <div
id="danger" id="danger"
className="rounded-xl border border-destructive/30 bg-card backdrop-blur-md overflow-hidden scroll-mt-6" className={cn(
"rounded-2xl overflow-hidden scroll-mt-6",
"border border-destructive/30",
"bg-gradient-to-br from-destructive/5 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-destructive/5"
)}
> >
<div className="p-6 border-b border-destructive/30"> <div className="p-6 border-b border-destructive/20 bg-gradient-to-r from-destructive/5 via-transparent to-transparent">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-3 mb-2">
<Trash2 className="w-5 h-5 text-destructive" /> <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-destructive/20 to-destructive/10 flex items-center justify-center border border-destructive/20">
<h2 className="text-lg font-semibold text-foreground">Danger Zone</h2> <AlertTriangle className="w-5 h-5 text-destructive" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Danger Zone</h2>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground/80 ml-12">
Permanently remove this project from Automaker. Permanently remove this project from Automaker.
</p> </p>
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3.5 min-w-0">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0"> <div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/15 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" /> <Folder className="w-5 h-5 text-brand-500" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="font-medium text-foreground truncate"> <p className="font-medium text-foreground truncate">
{project.name} {project.name}
</p> </p>
<p className="text-xs text-muted-foreground truncate"> <p className="text-xs text-muted-foreground/70 truncate mt-0.5">
{project.path} {project.path}
</p> </p>
</div> </div>
@@ -46,6 +54,12 @@ export function DangerZoneSection({
variant="destructive" variant="destructive"
onClick={onDeleteClick} onClick={onDeleteClick}
data-testid="delete-project-button" data-testid="delete-project-button"
className={cn(
"shrink-0",
"shadow-md shadow-destructive/20 hover:shadow-lg hover:shadow-destructive/25",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.98]"
)}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Delete Project Delete Project

View File

@@ -1,6 +1,7 @@
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { FlaskConical, Settings2, TestTube, GitBranch } from "lucide-react"; import { FlaskConical, Settings2, TestTube, GitBranch } from "lucide-react";
import { cn } from "@/lib/utils";
interface FeatureDefaultsSectionProps { interface FeatureDefaultsSectionProps {
showProfilesOnly: boolean; showProfilesOnly: boolean;
@@ -22,111 +23,112 @@ export function FeatureDefaultsSection({
return ( return (
<div <div
id="defaults" id="defaults"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6" className={cn(
"rounded-2xl overflow-hidden scroll-mt-6",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
)}
> >
<div className="p-6 border-b border-border"> <div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-3 mb-2">
<FlaskConical className="w-5 h-5 text-brand-500" /> <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<h2 className="text-lg font-semibold text-foreground"> <FlaskConical className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Feature Defaults Feature Defaults
</h2> </h2>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground/80 ml-12">
Configure default settings for new features. Configure default settings for new features.
</p> </p>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-6 space-y-5">
{/* Profiles Only Setting */} {/* Profiles Only Setting */}
<div className="space-y-3"> <div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<div className="flex items-start space-x-3"> <Checkbox
<Checkbox id="show-profiles-only"
id="show-profiles-only" checked={showProfilesOnly}
checked={showProfilesOnly} onCheckedChange={(checked) =>
onCheckedChange={(checked) => onShowProfilesOnlyChange(checked === true)
onShowProfilesOnlyChange(checked === true) }
} className="mt-1"
className="mt-0.5" data-testid="show-profiles-only-checkbox"
data-testid="show-profiles-only-checkbox" />
/> <div className="space-y-1.5">
<div className="space-y-1"> <Label
<Label htmlFor="show-profiles-only"
htmlFor="show-profiles-only" className="text-foreground cursor-pointer font-medium flex items-center gap-2"
className="text-foreground cursor-pointer font-medium flex items-center gap-2" >
> <Settings2 className="w-4 h-4 text-brand-500" />
<Settings2 className="w-4 h-4 text-brand-500" /> Show profiles only by default
Show profiles only by default </Label>
</Label> <p className="text-xs text-muted-foreground/80 leading-relaxed">
<p className="text-xs text-muted-foreground"> When enabled, the Add Feature dialog will show only AI profiles
When enabled, the Add Feature dialog will show only AI profiles and hide advanced model tweaking options. This creates a cleaner, less
and hide advanced model tweaking options (Claude SDK, thinking overwhelming UI.
levels). This creates a cleaner, less </p>
overwhelming UI. You can always disable this to access advanced
settings.
</p>
</div>
</div> </div>
</div> </div>
{/* Separator */} {/* Separator */}
<div className="border-t border-border" /> <div className="border-t border-border/30" />
{/* Automated Testing Setting */} {/* Automated Testing Setting */}
<div className="space-y-3"> <div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<div className="flex items-start space-x-3"> <Checkbox
<Checkbox id="default-skip-tests"
id="default-skip-tests" checked={!defaultSkipTests}
checked={!defaultSkipTests} onCheckedChange={(checked) =>
onCheckedChange={(checked) => onDefaultSkipTestsChange(checked !== true)
onDefaultSkipTestsChange(checked !== true) }
} className="mt-1"
className="mt-0.5" data-testid="default-skip-tests-checkbox"
data-testid="default-skip-tests-checkbox" />
/> <div className="space-y-1.5">
<div className="space-y-1"> <Label
<Label htmlFor="default-skip-tests"
htmlFor="default-skip-tests" className="text-foreground cursor-pointer font-medium flex items-center gap-2"
className="text-foreground cursor-pointer font-medium flex items-center gap-2" >
> <TestTube className="w-4 h-4 text-brand-500" />
<TestTube className="w-4 h-4 text-brand-500" /> Enable automated testing by default
Enable automated testing by default </Label>
</Label> <p className="text-xs text-muted-foreground/80 leading-relaxed">
<p className="text-xs text-muted-foreground"> When enabled, new features will use TDD with automated tests. When disabled, features will
When enabled, new features will use TDD (test-driven require manual verification.
development) with automated tests. When disabled, features will </p>
require manual verification. You can still override this for
individual features.
</p>
</div>
</div> </div>
</div> </div>
{/* Separator */}
<div className="border-t border-border/30" />
{/* Worktree Isolation Setting */} {/* Worktree Isolation Setting */}
<div className="space-y-3 pt-2 border-t border-border"> <div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<div className="flex items-start space-x-3"> <Checkbox
<Checkbox id="use-worktrees"
id="use-worktrees" checked={useWorktrees}
checked={useWorktrees} onCheckedChange={(checked) =>
onCheckedChange={(checked) => onUseWorktreesChange(checked === true)
onUseWorktreesChange(checked === true) }
} className="mt-1"
className="mt-0.5" data-testid="use-worktrees-checkbox"
data-testid="use-worktrees-checkbox" />
/> <div className="space-y-1.5">
<div className="space-y-1"> <Label
<Label htmlFor="use-worktrees"
htmlFor="use-worktrees" className="text-foreground cursor-pointer font-medium flex items-center gap-2"
className="text-foreground cursor-pointer font-medium flex items-center gap-2" >
> <GitBranch className="w-4 h-4 text-brand-500" />
<GitBranch className="w-4 h-4 text-brand-500" /> Enable Git Worktree Isolation
Enable Git Worktree Isolation (experimental) <span className="text-[10px] px-1.5 py-0.5 rounded-md bg-amber-500/15 text-amber-500 border border-amber-500/20 font-medium">
</Label> experimental
<p className="text-xs text-muted-foreground"> </span>
Creates isolated git branches for each feature. When disabled, </Label>
agents work directly in the main project directory. This feature <p className="text-xs text-muted-foreground/80 leading-relaxed">
is experimental and may require additional setup like branch Creates isolated git branches for each feature. When disabled,
selection and merge configuration. agents work directly in the main project directory.
</p> </p>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Settings2, Keyboard } from "lucide-react"; import { Settings2, Keyboard } from "lucide-react";
import { cn } from "@/lib/utils";
interface KeyboardShortcutsSectionProps { interface KeyboardShortcutsSectionProps {
onOpenKeyboardMap: () => void; onOpenKeyboardMap: () => void;
@@ -11,43 +12,58 @@ export function KeyboardShortcutsSection({
return ( return (
<div <div
id="keyboard" id="keyboard"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6" className={cn(
"rounded-2xl overflow-hidden scroll-mt-6",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
)}
> >
<div className="p-6 border-b border-border"> <div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-3 mb-2">
<Settings2 className="w-5 h-5 text-brand-500" /> <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<h2 className="text-lg font-semibold text-foreground"> <Settings2 className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Keyboard Shortcuts Keyboard Shortcuts
</h2> </h2>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground/80 ml-12">
Customize keyboard shortcuts for navigation and actions using the Customize keyboard shortcuts for navigation and actions using the
visual keyboard map. visual keyboard map.
</p> </p>
</div> </div>
<div className="p-6"> <div className="p-6">
{/* Centered message directing to keyboard map */} {/* Centered message directing to keyboard map */}
<div className="flex flex-col items-center justify-center py-16 text-center space-y-4"> <div className="flex flex-col items-center justify-center py-12 text-center space-y-5">
<div className="relative"> <div className="relative">
<Keyboard className="w-16 h-16 text-brand-500/30" /> <div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-brand-500/10 to-brand-600/5 flex items-center justify-center border border-brand-500/20">
<div className="absolute inset-0 bg-brand-500/10 blur-xl rounded-full" /> <Keyboard className="w-10 h-10 text-brand-500/60" />
</div>
<div className="absolute inset-0 bg-brand-500/10 blur-2xl rounded-full -z-10" />
</div> </div>
<div className="space-y-2 max-w-md"> <div className="space-y-2 max-w-md">
<h3 className="text-lg font-semibold text-foreground"> <h3 className="text-lg font-semibold text-foreground">
Use the Visual Keyboard Map Use the Visual Keyboard Map
</h3> </h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground/80">
Click the &quot;View Keyboard Map&quot; button above to customize Click the button below to customize your keyboard shortcuts. The visual
your keyboard shortcuts. The visual interface shows all available interface shows all available keys and lets you easily edit shortcuts.
keys and lets you easily edit shortcuts with single-modifier
restrictions.
</p> </p>
</div> </div>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"
onClick={onOpenKeyboardMap} onClick={onOpenKeyboardMap}
className="gap-2 mt-4" className={cn(
"gap-2.5 mt-2 h-11 px-6",
"bg-gradient-to-r from-brand-500 to-brand-600",
"hover:from-brand-600 hover:to-brand-600",
"text-white font-medium border-0",
"shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.98]"
)}
> >
<Keyboard className="w-5 h-5" /> <Keyboard className="w-5 h-5" />
Open Keyboard Map Open Keyboard Map

View File

@@ -530,17 +530,17 @@ export function WelcomeView() {
return ( return (
<div className="flex-1 flex flex-col content-bg" data-testid="welcome-view"> <div className="flex-1 flex flex-col content-bg" data-testid="welcome-view">
{/* Header Section */} {/* Header Section */}
<div className="flex-shrink-0 border-b border-border bg-glass backdrop-blur-md"> <div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
<div className="px-8 py-6"> <div className="px-8 py-6">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-4 animate-in fade-in slide-in-from-top-2 duration-500">
<div className="w-10 h-10 rounded-xl flex items-center justify-center"> <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shadow-lg shadow-brand-500/10">
<img src="/logo.png" alt="Automaker Logo" className="w-10 h-10" /> <img src="/logo.png" alt="Automaker Logo" className="w-8 h-8" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-foreground"> <h1 className="text-2xl font-bold text-foreground tracking-tight">
Welcome to Automaker Welcome to Automaker
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground mt-0.5">
Your autonomous AI development studio Your autonomous AI development studio
</p> </p>
</div> </div>
@@ -550,24 +550,25 @@ export function WelcomeView() {
{/* Content Area */} {/* Content Area */}
<div className="flex-1 overflow-y-auto p-8"> <div className="flex-1 overflow-y-auto p-8">
<div className="max-w-6xl mx-auto"> <div className="max-w-5xl mx-auto">
{/* Quick Actions */} {/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12 animate-in fade-in slide-in-from-bottom-4 duration-500 delay-100">
{/* New Project Card */}
<div <div
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-border-glass transition-all duration-200" className="group relative rounded-xl border border-border bg-card/80 backdrop-blur-sm hover:bg-card hover:border-brand-500/30 hover:shadow-xl hover:shadow-brand-500/5 transition-all duration-300 hover:-translate-y-1"
data-testid="new-project-card" data-testid="new-project-card"
> >
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/5 to-purple-600/5 opacity-0 group-hover:opacity-100 transition-opacity"></div> <div className="absolute inset-0 rounded-xl bg-gradient-to-br from-brand-500/5 via-transparent to-purple-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative p-6 h-full flex flex-col"> <div className="relative p-6 h-full flex flex-col">
<div className="flex items-start gap-4 flex-1"> <div className="flex items-start gap-4 flex-1">
<div className="w-12 h-12 rounded-lg bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center group-hover:scale-110 transition-transform shrink-0"> <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/25 flex items-center justify-center group-hover:scale-105 group-hover:shadow-brand-500/40 transition-all duration-300 shrink-0">
<Plus className="w-6 h-6 text-white" /> <Plus className="w-6 h-6 text-white" />
</div> </div>
<div className="flex-1"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-1"> <h3 className="text-lg font-semibold text-foreground mb-1.5">
New Project New Project
</h3> </h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground leading-relaxed">
Create a new project from scratch with AI-powered Create a new project from scratch with AI-powered
development development
</p> </p>
@@ -576,7 +577,7 @@ export function WelcomeView() {
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
className="w-full mt-4 bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0" className="w-full mt-5 bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white border-0 shadow-md shadow-brand-500/20 hover:shadow-brand-500/30 transition-all"
data-testid="create-new-project" data-testid="create-new-project"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
@@ -604,29 +605,30 @@ export function WelcomeView() {
</div> </div>
</div> </div>
{/* Open Project Card */}
<div <div
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-border-glass transition-all duration-200 cursor-pointer" className="group relative rounded-xl border border-border bg-card/80 backdrop-blur-sm hover:bg-card hover:border-blue-500/30 hover:shadow-xl hover:shadow-blue-500/5 transition-all duration-300 cursor-pointer hover:-translate-y-1"
onClick={handleOpenProject} onClick={handleOpenProject}
data-testid="open-project-card" data-testid="open-project-card"
> >
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-cyan-600/5 opacity-0 group-hover:opacity-100 transition-opacity"></div> <div className="absolute inset-0 rounded-xl bg-gradient-to-br from-blue-500/5 via-transparent to-cyan-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative p-6 h-full flex flex-col"> <div className="relative p-6 h-full flex flex-col">
<div className="flex items-start gap-4 flex-1"> <div className="flex items-start gap-4 flex-1">
<div className="w-12 h-12 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:scale-110 transition-transform shrink-0"> <div className="w-12 h-12 rounded-xl bg-muted border border-border flex items-center justify-center group-hover:bg-blue-500/10 group-hover:border-blue-500/30 group-hover:scale-105 transition-all duration-300 shrink-0">
<FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-foreground transition-colors" /> <FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-blue-500 transition-colors duration-300" />
</div> </div>
<div className="flex-1"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-1"> <h3 className="text-lg font-semibold text-foreground mb-1.5">
Open Project Open Project
</h3> </h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground leading-relaxed">
Open an existing project folder to continue working Open an existing project folder to continue working
</p> </p>
</div> </div>
</div> </div>
<Button <Button
variant="secondary" variant="secondary"
className="w-full mt-4 bg-secondary hover:bg-secondary/80 text-foreground border border-border hover:border-border-glass" className="w-full mt-5 bg-secondary/80 hover:bg-secondary text-foreground border border-border hover:border-blue-500/30 transition-all"
data-testid="open-existing-project" data-testid="open-existing-project"
> >
<FolderOpen className="w-4 h-4 mr-2" /> <FolderOpen className="w-4 h-4 mr-2" />
@@ -638,36 +640,39 @@ export function WelcomeView() {
{/* Recent Projects */} {/* Recent Projects */}
{recentProjects.length > 0 && ( {recentProjects.length > 0 && (
<div> <div className="animate-in fade-in slide-in-from-bottom-4 duration-500 delay-200">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2.5 mb-5">
<Clock className="w-5 h-5 text-muted-foreground" /> <div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center">
<Clock className="w-4 h-4 text-muted-foreground" />
</div>
<h2 className="text-lg font-semibold text-foreground"> <h2 className="text-lg font-semibold text-foreground">
Recent Projects Recent Projects
</h2> </h2>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{recentProjects.map((project) => ( {recentProjects.map((project, index) => (
<div <div
key={project.id} key={project.id}
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-brand-500/50 transition-all duration-200 cursor-pointer" className="group relative rounded-xl border border-border bg-card/60 backdrop-blur-sm hover:bg-card hover:border-brand-500/40 hover:shadow-lg hover:shadow-brand-500/5 transition-all duration-300 cursor-pointer hover:-translate-y-0.5"
onClick={() => handleRecentProjectClick(project)} onClick={() => handleRecentProjectClick(project)}
data-testid={`recent-project-${project.id}`} data-testid={`recent-project-${project.id}`}
style={{ animationDelay: `${index * 50}ms` }}
> >
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/0 to-purple-600/0 group-hover:from-brand-500/5 group-hover:to-purple-600/5 transition-all"></div> <div className="absolute inset-0 rounded-xl bg-gradient-to-br from-brand-500/0 to-purple-600/0 group-hover:from-brand-500/5 group-hover:to-purple-600/5 transition-all duration-300" />
<div className="relative p-4"> <div className="relative p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:border-brand-500/50 transition-colors"> <div className="w-10 h-10 rounded-lg bg-muted/80 border border-border flex items-center justify-center group-hover:bg-brand-500/10 group-hover:border-brand-500/30 transition-all duration-300 shrink-0">
<Folder className="w-5 h-5 text-muted-foreground group-hover:text-brand-500 transition-colors" /> <Folder className="w-5 h-5 text-muted-foreground group-hover:text-brand-500 transition-colors duration-300" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors"> <p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors duration-300">
{project.name} {project.name}
</p> </p>
<p className="text-xs text-muted-foreground/70 truncate mt-0.5"> <p className="text-xs text-muted-foreground/70 truncate mt-1">
{project.path} {project.path}
</p> </p>
{project.lastOpened && ( {project.lastOpened && (
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1.5">
{new Date( {new Date(
project.lastOpened project.lastOpened
).toLocaleDateString()} ).toLocaleDateString()}
@@ -684,14 +689,14 @@ export function WelcomeView() {
{/* Empty State for No Projects */} {/* Empty State for No Projects */}
{recentProjects.length === 0 && ( {recentProjects.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-center"> <div className="flex flex-col items-center justify-center py-16 text-center animate-in fade-in duration-500 delay-200">
<div className="w-16 h-16 rounded-2xl bg-muted border border-border flex items-center justify-center mb-4"> <div className="w-20 h-20 rounded-2xl bg-muted/50 border border-border flex items-center justify-center mb-5">
<Sparkles className="w-8 h-8 text-muted-foreground" /> <Sparkles className="w-10 h-10 text-muted-foreground/50" />
</div> </div>
<h3 className="text-lg font-semibold text-foreground mb-2"> <h3 className="text-xl font-semibold text-foreground mb-2">
No projects yet No projects yet
</h3> </h3>
<p className="text-sm text-zinc-400 max-w-md"> <p className="text-sm text-muted-foreground max-w-md leading-relaxed">
Get started by creating a new project or opening an existing one Get started by creating a new project or opening an existing one
</p> </p>
</div> </div>
@@ -712,35 +717,37 @@ export function WelcomeView() {
{/* Project Initialization Dialog */} {/* Project Initialization Dialog */}
<Dialog open={showInitDialog} onOpenChange={setShowInitDialog}> <Dialog open={showInitDialog} onOpenChange={setShowInitDialog}>
<DialogContent <DialogContent
className="bg-card border-border" className="bg-card border-border shadow-xl"
data-testid="project-init-dialog" data-testid="project-init-dialog"
> >
<DialogHeader> <DialogHeader>
<DialogTitle className="text-foreground flex items-center gap-2"> <DialogTitle className="text-foreground flex items-center gap-2.5">
<Sparkles className="w-5 h-5 text-brand-500" /> <div className="w-8 h-8 rounded-lg bg-brand-500/10 flex items-center justify-center">
<Sparkles className="w-4 h-4 text-brand-500" />
</div>
{initStatus?.isNewProject {initStatus?.isNewProject
? "Project Initialized" ? "Project Initialized"
: "Project Updated"} : "Project Updated"}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-muted-foreground"> <DialogDescription className="text-muted-foreground mt-1">
{initStatus?.isNewProject {initStatus?.isNewProject
? `Created .automaker directory structure for ${initStatus?.projectName}` ? `Created .automaker directory structure for ${initStatus?.projectName}`
: `Updated missing files in .automaker for ${initStatus?.projectName}`} : `Updated missing files in .automaker for ${initStatus?.projectName}`}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="py-4">
<div className="space-y-2"> <div className="space-y-3">
<p className="text-sm text-foreground font-medium"> <p className="text-sm text-foreground font-medium">
Created files: Created files:
</p> </p>
<ul className="space-y-1.5"> <ul className="space-y-2">
{initStatus?.createdFiles.map((file) => ( {initStatus?.createdFiles.map((file) => (
<li <li
key={file} key={file}
className="flex items-center gap-2 text-sm text-muted-foreground" className="flex items-center gap-2.5 text-sm text-muted-foreground"
> >
<div className="w-1.5 h-1.5 rounded-full bg-green-500" /> <div className="w-2 h-2 rounded-full bg-green-500" />
<code className="text-xs bg-muted px-2 py-0.5 rounded"> <code className="text-xs bg-muted px-2.5 py-1 rounded-md font-mono">
{file} {file}
</code> </code>
</li> </li>
@@ -749,18 +756,18 @@ export function WelcomeView() {
</div> </div>
{initStatus?.isNewProject && ( {initStatus?.isNewProject && (
<div className="mt-4 p-3 rounded-lg bg-muted/50 border border-border-glass"> <div className="mt-5 p-4 rounded-xl bg-muted/50 border border-border">
{isAnalyzing ? ( {isAnalyzing ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<Loader2 className="w-4 h-4 text-brand-500 animate-spin" /> <Loader2 className="w-4 h-4 text-brand-500 animate-spin" />
<p className="text-sm text-brand-400"> <p className="text-sm text-brand-500">
AI agent is analyzing your project structure... AI agent is analyzing your project structure...
</p> </p>
</div> </div>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground leading-relaxed">
<span className="text-brand-400">Tip:</span> Edit the{" "} <span className="text-brand-500 font-medium">Tip:</span> Edit the{" "}
<code className="text-xs bg-muted px-1.5 py-0.5 rounded"> <code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
app_spec.txt app_spec.txt
</code>{" "} </code>{" "}
file to describe your project. The AI agent will use this to file to describe your project. The AI agent will use this to
@@ -773,7 +780,7 @@ export function WelcomeView() {
<DialogFooter> <DialogFooter>
<Button <Button
onClick={() => setShowInitDialog(false)} onClick={() => setShowInitDialog(false)}
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0" className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white border-0 shadow-md shadow-brand-500/20"
data-testid="close-init-dialog" data-testid="close-init-dialog"
> >
Get Started Get Started
@@ -795,8 +802,8 @@ export function WelcomeView() {
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm" className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm"
data-testid="project-opening-overlay" data-testid="project-opening-overlay"
> >
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-card border border-border"> <div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-card border border-border shadow-2xl">
<Loader2 className="w-8 h-8 text-brand-500 animate-spin" /> <Loader2 className="w-10 h-10 text-brand-500 animate-spin" />
<p className="text-foreground font-medium"> <p className="text-foreground font-medium">
Initializing project... Initializing project...
</p> </p>