Merge branch 'main' into ui-tweaks

This commit is contained in:
Cody Seibert
2025-12-15 01:13:30 -05:00
60 changed files with 4950 additions and 3662 deletions

View File

@@ -146,9 +146,11 @@ function SortableProjectItem({
ref={setNodeRef}
style={style}
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",
isDragging && "bg-accent shadow-lg",
isHighlighted && "bg-brand-500/10 text-foreground"
"flex items-center gap-2 px-2.5 py-2 rounded-lg cursor-pointer transition-all duration-200",
"text-muted-foreground hover:text-foreground hover:bg-accent/80",
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}`}
>
@@ -156,20 +158,22 @@ function SortableProjectItem({
<button
{...attributes}
{...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}`}
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>
{/* Project content - clickable area */}
<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)}
>
<Folder className="h-4 w-4 shrink-0" />
<span className="flex-1 truncate text-sm">{project.name}</span>
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate text-sm font-medium">
{project.name}
</span>
{currentProjectId === project.id && (
<Check className="h-4 w-4 text-brand-500 shrink-0" />
)}
@@ -1167,7 +1171,13 @@ export function Sidebar() {
return (
<aside
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"
)}
data-testid="sidebar"
@@ -1175,22 +1185,40 @@ export function Sidebar() {
{/* Floating Collapse Toggle Button - Desktop only - At border intersection */}
<button
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"
>
{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 */}
<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"
>
{sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}{" "}
<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"
>
{formatShortcut(shortcuts.toggleSidebar, true)}
@@ -1202,15 +1230,18 @@ export function Sidebar() {
{/* Logo */}
<div
className={cn(
"h-20 border-b border-sidebar-border shrink-0 titlebar-drag-region",
sidebarOpen
? "pt-8 px-3 lg:px-6 flex items-center justify-between"
: "pt-2 pb-2 px-3 flex flex-col items-center justify-center gap-2"
"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",
"flex items-center",
sidebarOpen ? "px-3 lg:px-5 justify-start" : "px-3 justify-center"
)}
>
<div
className={cn(
"flex items-center titlebar-no-drag cursor-pointer group",
"flex items-center gap-3 titlebar-no-drag cursor-pointer group",
!sidebarOpen && "flex-col gap-1"
)}
onClick={() => setCurrentView("welcome")}
@@ -1218,28 +1249,137 @@ export function Sidebar() {
>
{!sidebarOpen ? (
<div className="relative flex items-center justify-center rounded-lg">
<img
src="/logo.png"
alt="Automaker Logo"
className="size-8 group-hover:rotate-12 transition-transform"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
role="img"
aria-label="Automaker Logo"
className="size-8 group-hover:rotate-12 transition-transform duration-300 ease-out"
>
<defs>
<linearGradient
id="bg-collapsed"
x1="0"
y1="0"
x2="256"
y2="256"
gradientUnits="userSpaceOnUse"
>
<stop
offset="0%"
style={{ stopColor: "var(--brand-400)" }}
/>
<stop
offset="100%"
style={{ stopColor: "var(--brand-600)" }}
/>
</linearGradient>
<filter
id="iconShadow-collapsed"
x="-20%"
y="-20%"
width="140%"
height="140%"
>
<feDropShadow
dx="0"
dy="4"
stdDeviation="4"
floodColor="#000000"
floodOpacity="0.25"
/>
</filter>
</defs>
<rect
x="16"
y="16"
width="224"
height="224"
rx="56"
fill="url(#bg-collapsed)"
/>
<g
fill="none"
stroke="#FFFFFF"
strokeWidth="20"
strokeLinecap="round"
strokeLinejoin="round"
filter="url(#iconShadow-collapsed)"
>
<path d="M92 92 L52 128 L92 164" />
<path d="M144 72 L116 184" />
<path d="M164 92 L204 128 L164 164" />
</g>
</svg>
</div>
) : (
<span
className={cn(
"flex items-center font-bold text-sidebar-foreground text-base tracking-tight",
"hidden lg:flex"
)}
>
<img
src="/logo.png"
alt="A"
className="h-[1.8em] w-auto inline-block align-middle group-hover:rotate-12 transition-transform"
/>
<span className="-ml-0.5">
uto<span className="text-brand-500">maker</span>
<div className={cn("flex items-center gap-1", "hidden lg:flex")}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
role="img"
aria-label="automaker"
className="h-[36.8px] w-[36.8px] group-hover:rotate-12 transition-transform duration-300 ease-out"
>
<defs>
<linearGradient
id="bg-expanded"
x1="0"
y1="0"
x2="256"
y2="256"
gradientUnits="userSpaceOnUse"
>
<stop
offset="0%"
style={{ stopColor: "var(--brand-400)" }}
/>
<stop
offset="100%"
style={{ stopColor: "var(--brand-600)" }}
/>
</linearGradient>
<filter
id="iconShadow-expanded"
x="-20%"
y="-20%"
width="140%"
height="140%"
>
<feDropShadow
dx="0"
dy="4"
stdDeviation="4"
floodColor="#000000"
floodOpacity="0.25"
/>
</filter>
</defs>
<rect
x="16"
y="16"
width="224"
height="224"
rx="56"
fill="url(#bg-expanded)"
/>
<g
fill="none"
stroke="#FFFFFF"
strokeWidth="20"
strokeLinecap="round"
strokeLinejoin="round"
filter="url(#iconShadow-expanded)"
>
<path d="M92 92 L52 128 L92 164" />
<path d="M144 72 L116 184" />
<path d="M164 92 L204 128 L164 164" />
</g>
</svg>
<span className="font-bold text-foreground text-[1.7rem] tracking-tight leading-none translate-y-[-2px]">
automaker<span className="text-brand-500">.</span>
</span>
</span>
</div>
)}
</div>
{/* Bug Report Button */}
@@ -1250,7 +1390,12 @@ export function Sidebar() {
"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 absolute right-3",
"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"
data-testid="bug-report-link"
>
@@ -1260,38 +1405,69 @@ export function Sidebar() {
{/* Project Actions - Moved above project selector */}
{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
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"
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">
New
</span>
</button>
<button
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})`}
data-testid="open-project-button"
>
<FolderOpen className="w-4 h-4 shrink-0" />
<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">
<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.5 text-[10px] font-mono rounded-md bg-muted/80 text-muted-foreground ml-2">
{formatShortcut(shortcuts.openProject, true)}
</span>
</button>
<button
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"
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 && (
<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}
</span>
)}
@@ -1301,56 +1477,80 @@ export function Sidebar() {
{/* Project Selector with Cycle Buttons */}
{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
open={isProjectPickerOpen}
onOpenChange={setIsProjectPickerOpen}
>
<DropdownMenuTrigger asChild>
<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"
>
<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" />
<span className="text-sm font-medium truncate">
{currentProject?.name || "Select Project"}
</span>
</div>
<div className="flex items-center gap-1">
<div className="flex items-center gap-1.5">
<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"
>
{formatShortcut(shortcuts.projectPicker, true)}
</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>
</button>
</DropdownMenuTrigger>
<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"
data-testid="project-picker-dropdown"
>
{/* Search input for type-ahead filtering */}
<div className="px-2 pb-2">
<div className="px-1 pb-2">
<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
ref={projectSearchInputRef}
type="text"
placeholder="Search projects..."
value={projectSearchQuery}
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"
/>
</div>
</div>
{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
</div>
) : (
@@ -1363,26 +1563,32 @@ export function Sidebar() {
items={filteredProjects.map((p) => p.id)}
strategy={verticalListSortingStrategy}
>
{filteredProjects.map((project, index) => (
<SortableProjectItem
key={project.id}
project={project}
currentProjectId={currentProject?.id}
isHighlighted={index === selectedProjectIndex}
onSelect={(p) => {
setCurrentProject(p);
setIsProjectPickerOpen(false);
}}
/>
))}
<div className="space-y-0.5 max-h-64 overflow-y-auto">
{filteredProjects.map((project, index) => (
<SortableProjectItem
key={project.id}
project={project}
currentProjectId={currentProject?.id}
isHighlighted={index === selectedProjectIndex}
onSelect={(p) => {
setCurrentProject(p);
setIsProjectPickerOpen(false);
}}
/>
))}
</div>
</SortableContext>
</DndContext>
)}
{/* Keyboard hint */}
<div className="px-2 pt-2 mt-1 border-t border-border">
<p className="text-[10px] text-muted-foreground text-center">
navigate Enter select Esc close
<div className="px-2 pt-2 mt-1.5 border-t border-border/50">
<p className="text-[10px] text-muted-foreground text-center tracking-wide">
<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>
</div>
</DropdownMenuContent>
@@ -1400,14 +1606,24 @@ export function Sidebar() {
>
<DropdownMenuTrigger asChild>
<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"
data-testid="project-options-menu"
>
<MoreVertical className="w-4 h-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuContent
align="end"
className="w-56 bg-popover/95 backdrop-blur-xl"
>
{/* Project Theme Submenu */}
<DropdownMenuSub>
<DropdownMenuSubTrigger data-testid="project-theme-trigger">
@@ -1420,7 +1636,7 @@ export function Sidebar() {
)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="w-56"
className="w-56 bg-popover/95 backdrop-blur-xl"
data-testid="project-theme-menu"
onPointerLeave={() => {
// Clear preview theme when leaving the dropdown
@@ -1561,7 +1777,7 @@ export function Sidebar() {
)}
{/* 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 ? (
// Placeholder when no project is selected (only in expanded state)
<div className="flex items-center justify-center h-full px-4">
@@ -1577,18 +1793,18 @@ export function Sidebar() {
<div key={sectionIdx} className={sectionIdx > 0 ? "mt-6" : ""}>
{/* Section Label */}
{section.label && sidebarOpen && (
<div className="hidden lg:block px-4 mb-2">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
<div className="hidden lg:block px-3 mb-2">
<span className="text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-widest">
{section.label}
</span>
</div>
)}
{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 */}
<div className="space-y-1">
<div className="space-y-1.5">
{section.items.map((item) => {
const isActive = isActiveRoute(item.id);
const Icon = item.icon;
@@ -1598,29 +1814,43 @@ export function Sidebar() {
key={item.id}
onClick={() => setCurrentView(item.id as any)}
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
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
sidebarOpen ? "justify-start" : "justify-center"
? [
// Active: Premium gradient with glow
"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}
data-testid={`nav-${item.id}`}
>
{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
className={cn(
"w-4 h-4 shrink-0 transition-colors",
"w-[18px] h-[18px] shrink-0 transition-all duration-200",
isActive
? "text-brand-500"
: "group-hover:text-brand-400"
? "text-brand-500 drop-shadow-sm"
: "group-hover:text-brand-400 group-hover:scale-110"
)}
/>
<span
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"
)}
>
@@ -1629,9 +1859,10 @@ export function Sidebar() {
{item.shortcut && sidebarOpen && (
<span
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",
isActive &&
"bg-brand-500/20 border-brand-500/50 text-brand-400"
"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
? "bg-brand-500/20 text-brand-400"
: "bg-muted text-muted-foreground group-hover:bg-accent"
)}
data-testid={`shortcut-${item.id}`}
>
@@ -1641,10 +1872,22 @@ export function Sidebar() {
{/* Tooltip for collapsed state */}
{!sidebarOpen && (
<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()}`}
>
{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>
)}
</button>
@@ -1658,7 +1901,15 @@ export function Sidebar() {
</div>
{/* 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 */}
<CoursePromoBadge sidebarOpen={sidebarOpen} />
{/* Wiki Link */}
@@ -1667,36 +1918,57 @@ export function Sidebar() {
<button
onClick={() => setCurrentView("wiki")}
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")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
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",
]
: [
"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}
data-testid="wiki-link"
>
{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
className={cn(
"w-4 h-4 shrink-0 transition-colors",
"w-[18px] h-[18px] shrink-0 transition-all duration-200",
isActiveRoute("wiki")
? "text-brand-500"
: "group-hover:text-brand-400"
? "text-brand-500 drop-shadow-sm"
: "group-hover:text-brand-400 group-hover:scale-110"
)}
/>
<span
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"
)}
>
Wiki
</span>
{!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
</span>
)}
@@ -1709,31 +1981,48 @@ export function Sidebar() {
<button
onClick={() => setCurrentView("running-agents")}
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")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
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",
]
: [
"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}
data-testid="running-agents-link"
>
{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">
<Activity
className={cn(
"w-4 h-4 shrink-0 transition-colors",
"w-[18px] h-[18px] shrink-0 transition-all duration-200",
isActiveRoute("running-agents")
? "text-brand-500"
: "group-hover:text-brand-400"
? "text-brand-500 drop-shadow-sm"
: "group-hover:text-brand-400 group-hover:scale-110"
)}
/>
{/* Running agents count badge - shown in collapsed state */}
{!sidebarOpen && runningAgentsCount > 0 && (
<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"
>
{runningAgentsCount > 99 ? "99" : runningAgentsCount}
@@ -1742,7 +2031,7 @@ export function Sidebar() {
</div>
<span
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"
)}
>
@@ -1752,7 +2041,10 @@ export function Sidebar() {
{sidebarOpen && runningAgentsCount > 0 && (
<span
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"
)}
data-testid="running-agents-count"
@@ -1761,8 +2053,22 @@ export function Sidebar() {
</span>
)}
{!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
{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>
)}
</button>
@@ -1773,29 +2079,41 @@ export function Sidebar() {
<button
onClick={() => setCurrentView("settings")}
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")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
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",
]
: [
"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}
data-testid="settings-button"
>
{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
className={cn(
"w-4 h-4 shrink-0 transition-colors",
"w-[18px] h-[18px] shrink-0 transition-all duration-200",
isActiveRoute("settings")
? "text-brand-500"
: "group-hover:text-brand-400"
? "text-brand-500 drop-shadow-sm"
: "group-hover:text-brand-400 group-hover:rotate-90 group-hover:scale-110"
)}
/>
<span
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"
)}
>
@@ -1804,9 +2122,10 @@ export function Sidebar() {
{sidebarOpen && (
<span
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",
isActiveRoute("settings") &&
"bg-brand-500/20 border-brand-500/50 text-brand-400"
"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")
? "bg-brand-500/20 text-brand-400"
: "bg-muted text-muted-foreground group-hover:bg-accent"
)}
data-testid="shortcut-settings"
>
@@ -1814,15 +2133,27 @@ export function Sidebar() {
</span>
)}
{!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
<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>
)}
</button>
</div>
</div>
<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>
<DialogTitle>Recycle Bin</DialogTitle>
<DialogDescription className="text-muted-foreground">
@@ -1840,7 +2171,7 @@ export function Sidebar() {
{trashedProjects.map((project) => (
<div
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">
<p className="text-sm font-medium text-foreground truncate">
@@ -1931,7 +2262,7 @@ export function Sidebar() {
}
}}
>
<DialogContent className="max-w-2xl">
<DialogContent className="max-w-2xl bg-popover/95 backdrop-blur-xl">
<DialogHeader>
<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">
@@ -1960,7 +2291,7 @@ export function Sidebar() {
</div>
{/* 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">
<CheckCircle2 className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
<div>
@@ -2000,9 +2331,9 @@ export function Sidebar() {
</div>
{/* Info box */}
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-3">
<p className="text-xs text-blue-400 leading-relaxed">
<strong className="text-blue-300">Tip:</strong> You can always
<div className="rounded-xl bg-brand-500/5 border border-brand-500/10 p-3">
<p className="text-xs text-muted-foreground leading-relaxed">
<strong className="text-foreground">Tip:</strong> You can always
generate or edit your app_spec.txt later from the Spec Editor in
the sidebar.
</p>