Compare commits

...

1 Commits

Author SHA1 Message Date
SuperComboGamer
2eb92a0402 feat: Introduce new UI layout with floating dock, visual effects, and expanded theme options. 2025-12-22 21:10:12 -05:00
31 changed files with 1838 additions and 738 deletions

View File

@@ -69,13 +69,15 @@
"cmdk": "^1.1.1",
"dagre": "^0.8.5",
"dotenv": "^17.2.3",
"framer-motion": "^12.23.26",
"geist": "^1.5.1",
"lucide-react": "^0.562.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"react-resizable-panels": "^3.0.6",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.9"

View File

@@ -6,6 +6,8 @@ import { useSettingsMigration } from './hooks/use-settings-migration';
import './styles/global.css';
import './styles/theme-imports';
import { Shell } from './components/layout/shell';
export default function App() {
const [showSplash, setShowSplash] = useState(() => {
// Only show splash once per session
@@ -27,9 +29,9 @@ export default function App() {
}, []);
return (
<>
<Shell>
<RouterProvider router={router} />
{showSplash && <SplashScreen onComplete={handleSplashComplete} />}
</>
</Shell>
);
}

View File

@@ -0,0 +1,118 @@
import { useRef } from 'react';
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
import { useNavigate, useLocation } from '@tanstack/react-router';
import {
LayoutDashboard,
Bot,
FileText,
Database,
Terminal,
Settings,
Users,
type LucideIcon,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
export function FloatingDock() {
const mouseX = useMotionValue(Infinity);
const navigate = useNavigate();
const location = useLocation();
const { currentProject } = useAppStore();
const navItems = [
{ id: 'board', icon: LayoutDashboard, label: 'Board', path: '/board' },
{ id: 'agent', icon: Bot, label: 'Agent', path: '/agent' },
{ id: 'spec', icon: FileText, label: 'Spec', path: '/spec' },
{ id: 'context', icon: Database, label: 'Context', path: '/context' },
{ id: 'profiles', icon: Users, label: 'Profiles', path: '/profiles' },
{ id: 'terminal', icon: Terminal, label: 'Terminal', path: '/terminal' },
{ id: 'settings', icon: Settings, label: 'Settings', path: '/settings' },
];
if (!currentProject) return null;
return (
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50">
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className={cn(
'flex h-16 items-end gap-4 rounded-2xl px-4 pb-3',
'bg-white/5 backdrop-blur-2xl border border-white/10 shadow-2xl'
)}
>
{navItems.map((item) => (
<DockIcon
key={item.id}
mouseX={mouseX}
icon={item.icon}
path={item.path}
label={item.label}
isActive={location.pathname.startsWith(item.path)}
onClick={() => navigate({ to: item.path })}
/>
))}
</motion.div>
</div>
);
}
function DockIcon({
mouseX,
icon: Icon,
path,
label,
isActive,
onClick,
}: {
mouseX: any;
icon: LucideIcon;
path: string;
label: string;
isActive: boolean;
onClick: () => void;
}) {
const ref = useRef<HTMLDivElement>(null);
const distance = useTransform(mouseX, (val: number) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
const widthSync = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
const width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 });
return (
<motion.div
ref={ref}
style={{ width }}
className="aspect-square cursor-pointer group relative"
onClick={onClick}
>
{/* Tooltip */}
<div className="absolute -top-10 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity text-xs font-mono bg-black/80 text-white px-2 py-1 rounded backdrop-blur-md border border-white/10 pointer-events-none whitespace-nowrap">
{label}
</div>
<div
className={cn(
'flex h-full w-full items-center justify-center rounded-full transition-colors',
isActive
? 'bg-primary text-primary-foreground shadow-[0_0_20px_rgba(34,211,238,0.3)]'
: 'bg-white/5 text-muted-foreground hover:bg-white/10'
)}
>
<Icon className="h-[40%] w-[40%]" />
</div>
{/* Active Dot */}
{isActive && (
<motion.div
layoutId="activeDockDot"
className="absolute -bottom-2 left-1/2 w-1 h-1 bg-primary rounded-full -translate-x-1/2"
/>
)}
</motion.div>
);
}

View File

@@ -0,0 +1,70 @@
import { ChevronDown, Command, Folder } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
interface HudProps {
onOpenProjectPicker: () => void;
onOpenFolder: () => void;
}
export function Hud({ onOpenProjectPicker, onOpenFolder }: HudProps) {
const { currentProject, projects, setCurrentProject } = useAppStore();
if (!currentProject) return null;
return (
<div className="fixed top-4 left-4 z-50 flex items-center gap-3">
{/* Project Pill */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className={cn(
'group flex items-center gap-3 px-4 py-2 rounded-full cursor-pointer',
'bg-white/5 backdrop-blur-md border border-white/10',
'hover:bg-white/10 transition-colors'
)}
>
<div className="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.4)] animate-pulse" />
<span className="font-mono text-sm font-medium tracking-tight">
{currentProject.name}
</span>
<ChevronDown className="w-3 h-3 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 glass border-white/10" align="start">
<DropdownMenuLabel>Switch Project</DropdownMenuLabel>
<DropdownMenuSeparator />
{projects.slice(0, 5).map((p) => (
<DropdownMenuItem
key={p.id}
onClick={() => setCurrentProject(p)}
className="font-mono text-xs"
>
{p.name}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onOpenProjectPicker}>
<Command className="mr-2 w-3 h-3" />
All Projects...
</DropdownMenuItem>
<DropdownMenuItem onClick={onOpenFolder}>
<Folder className="mr-2 w-3 h-3" />
Open Local Folder...
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Dynamic Status / Breadcrumbs could go here */}
</div>
);
}

View File

@@ -0,0 +1,17 @@
export function NoiseOverlay() {
return (
<div className="fixed inset-0 z-50 pointer-events-none opacity-[0.015] mix-blend-overlay">
<svg className="w-full h-full">
<filter id="noiseFilter">
<feTurbulence
type="fractalNoise"
baseFrequency="0.80"
numOctaves="3"
stitchTiles="stitch"
/>
</filter>
<rect width="100%" height="100%" filter="url(#noiseFilter)" />
</svg>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';
interface PageShellProps {
children: ReactNode;
className?: string;
fullWidth?: boolean;
}
export function PageShell({ children, className, fullWidth = false }: PageShellProps) {
return (
<div className="relative w-full h-full pt-16 pb-24 px-6 overflow-hidden">
<motion.div
initial={{ opacity: 0, scale: 0.98, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.2, 0, 0, 1] }}
className={cn(
'w-full h-full rounded-3xl overflow-hidden',
'bg-black/20 backdrop-blur-2xl border border-white/5 shadow-2xl',
'flex flex-col',
!fullWidth && 'max-w-7xl mx-auto',
className
)}
>
{children}
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,69 @@
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';
export function PrismField() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePosition({
x: e.clientX,
y: e.clientY,
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return (
<div className="fixed inset-0 z-0 overflow-hidden pointer-events-none bg-[#0b101a]">
{/* Deep Space Base */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(17,24,39,1)_0%,rgba(11,16,26,1)_100%)]" />
{/* Animated Orbs */}
<motion.div
animate={{
x: mousePosition.x * 0.02,
y: mousePosition.y * 0.02,
}}
transition={{ type: 'spring', damping: 50, stiffness: 400 }}
className="absolute top-[-20%] left-[-10%] w-[70vw] h-[70vw] rounded-full bg-cyan-500/5 blur-[120px] mix-blend-screen"
/>
<motion.div
animate={{
x: mousePosition.x * -0.03,
y: mousePosition.y * -0.03,
}}
transition={{ type: 'spring', damping: 50, stiffness: 400 }}
className="absolute bottom-[-20%] right-[-10%] w-[60vw] h-[60vw] rounded-full bg-violet-600/5 blur-[120px] mix-blend-screen"
/>
<motion.div
animate={{
scale: [1, 1.1, 1],
opacity: [0.3, 0.5, 0.3],
}}
transition={{
duration: 8,
repeat: Infinity,
ease: 'easeInOut',
}}
className="absolute top-[30%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 w-[40vw] h-[40vw] rounded-full bg-blue-500/5 blur-[100px] mix-blend-screen"
/>
{/* Grid Overlay */}
<div
className="absolute inset-0 z-10 opacity-[0.03]"
style={{
backgroundImage: `linear-gradient(#fff 1px, transparent 1px), linear-gradient(90deg, #fff 1px, transparent 1px)`,
backgroundSize: '50px 50px',
}}
/>
{/* Vignette */}
<div className="absolute inset-0 z-20 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(11,16,26,0.8)_100%)]" />
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { ReactNode } from 'react';
import { cn } from '../../lib/utils';
import { PrismField } from './prism-field';
import { NoiseOverlay } from './noise-overlay';
interface ShellProps {
children: ReactNode;
className?: string;
showBackgroundElements?: boolean;
}
export function Shell({ children, className, showBackgroundElements = true }: ShellProps) {
return (
<div
className={cn(
'relative min-h-screen w-full overflow-hidden bg-background text-foreground transition-colors duration-500',
className
)}
>
{/* Animated Background Layers */}
{showBackgroundElements && (
<>
<PrismField />
<NoiseOverlay />
</>
)}
{/* Content wrapper */}
<div className="relative z-10 flex h-screen flex-col">{children}</div>
</div>
);
}

View File

@@ -17,8 +17,9 @@ import {
ProjectActions,
SidebarNavigation,
ProjectSelectorWithOptions,
SidebarFooter,
} from './sidebar/components';
import { Hud } from './hud';
import { FloatingDock } from './floating-dock';
import { TrashDialog, OnboardingDialog } from './sidebar/dialogs';
import { SIDEBAR_FEATURE_FLAGS } from './sidebar/constants';
import {
@@ -247,64 +248,27 @@ export function Sidebar() {
};
return (
<aside
className={cn(
'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"
>
<CollapseToggleButton
sidebarOpen={sidebarOpen}
toggleSidebar={toggleSidebar}
shortcut={shortcuts.toggleSidebar}
<>
{/* Heads-Up Display (Top Bar) */}
<Hud
onOpenProjectPicker={() => setIsProjectPickerOpen(true)}
onOpenFolder={handleOpenFolder}
/>
<div className="flex-1 flex flex-col overflow-hidden">
<SidebarHeader sidebarOpen={sidebarOpen} navigate={navigate} />
{/* Project Actions - Moved above project selector */}
{sidebarOpen && (
<ProjectActions
setShowNewProjectModal={setShowNewProjectModal}
handleOpenFolder={handleOpenFolder}
setShowTrashDialog={setShowTrashDialog}
trashedProjects={trashedProjects}
shortcuts={{ openProject: shortcuts.openProject }}
/>
)}
{/* Floating Navigation Dock */}
<FloatingDock />
{/* Project Selector Dialog (Hidden logic, controlled by state) */}
<div className="hidden">
<ProjectSelectorWithOptions
sidebarOpen={sidebarOpen}
sidebarOpen={true}
isProjectPickerOpen={isProjectPickerOpen}
setIsProjectPickerOpen={setIsProjectPickerOpen}
setShowDeleteProjectDialog={setShowDeleteProjectDialog}
/>
<SidebarNavigation
currentProject={currentProject}
sidebarOpen={sidebarOpen}
navSections={navSections}
isActiveRoute={isActiveRoute}
navigate={navigate}
/>
</div>
<SidebarFooter
sidebarOpen={sidebarOpen}
isActiveRoute={isActiveRoute}
navigate={navigate}
hideWiki={hideWiki}
hideRunningAgents={hideRunningAgents}
runningAgentsCount={runningAgentsCount}
shortcuts={{ settings: shortcuts.settings }}
/>
{/* Dialogs & Modals - Preservation of Logic */}
<TrashDialog
open={showTrashDialog}
onOpenChange={setShowTrashDialog}
@@ -317,7 +281,6 @@ export function Sidebar() {
isEmptyingTrash={isEmptyingTrash}
/>
{/* New Project Setup Dialog */}
<CreateSpecDialog
open={showSetupDialog}
onOpenChange={setShowSetupDialog}
@@ -345,7 +308,6 @@ export function Sidebar() {
onGenerateSpec={handleOnboardingGenerateSpec}
/>
{/* Delete Project Confirmation Dialog */}
<DeleteProjectDialog
open={showDeleteProjectDialog}
onOpenChange={setShowDeleteProjectDialog}
@@ -353,7 +315,6 @@ export function Sidebar() {
onConfirm={moveProjectToTrash}
/>
{/* New Project Modal */}
<NewProjectModal
open={showNewProjectModal}
onOpenChange={setShowNewProjectModal}
@@ -362,6 +323,6 @@ export function Sidebar() {
onCreateFromCustomUrl={handleCreateFromCustomUrl}
isCreating={isCreatingProject}
/>
</aside>
</>
);
}

View File

@@ -24,6 +24,13 @@ const badgeVariants = cva(
// 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',
// Prism variants
prism:
'border-cyan-500/30 bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500/20 font-mono tracking-wide rounded-md',
'prism-orange':
'border-amber-500/30 bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 font-mono tracking-wide rounded-md',
'prism-green':
'border-emerald-500/30 bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 font-mono tracking-wide rounded-md',
},
size: {
default: 'px-2.5 py-0.5 text-xs',

View File

@@ -6,25 +6,32 @@ import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
"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]",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-300 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: {
variant: {
default:
'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow-md hover:shadow-primary/25',
'bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary/90 hover:shadow-primary/40 hover:-translate-y-0.5',
destructive:
'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:
'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: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
'border border-border/50 bg-background/50 backdrop-blur-sm shadow-sm hover:bg-accent hover:text-accent-foreground dark:bg-white/5 dark:hover:bg-white/10 hover:border-accent',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 hover:shadow-md',
ghost: 'hover:bg-accent/50 hover:text-accent-foreground hover:backdrop-blur-sm',
link: 'text-primary underline-offset-4 hover:underline active:scale-100',
glass:
'border border-white/10 bg-white/5 text-foreground shadow-sm drop-shadow-sm backdrop-blur-md hover:bg-white/10 hover:border-white/20 hover:shadow-md transition-all duration-300',
'animated-outline': 'relative overflow-hidden rounded-xl hover:bg-transparent shadow-none',
'prism-primary':
'bg-cyan-400 text-slate-950 font-extrabold shadow-lg shadow-cyan-400/20 hover:brightness-110 hover:shadow-cyan-400/40 transition-all duration-200 tracking-wide',
'prism-glass':
'glass hover:bg-white/10 text-xs font-bold rounded-xl transition-all duration-200',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 text-xs',
lg: 'h-11 rounded-md px-8 has-[>svg]:px-5 text-base',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',

View File

@@ -11,9 +11,9 @@ function Card({ className, gradient = false, ...props }: CardProps) {
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-1 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)]',
'bg-white/5 text-card-foreground flex flex-col gap-1 rounded-[1.5rem] border border-white/10 backdrop-blur-xl py-6 transition-all duration-300',
// Prism hover effect
'hover:-translate-y-1 hover:bg-white/[0.06] hover:border-white/15',
// 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',

View File

@@ -66,10 +66,10 @@ function DialogOverlay({
<DialogOverlayPrimitive
data-slot="dialog-overlay"
className={cn(
'fixed inset-0 z-50 bg-black/60 backdrop-blur-sm',
'fixed inset-0 z-50 bg-black/40 backdrop-blur-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'duration-200',
'duration-300',
className
)}
{...props}
@@ -99,15 +99,15 @@ const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
className={cn(
'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',
'bg-card/90 border border-white/10 rounded-2xl shadow-2xl backdrop-blur-xl',
// Premium shadow
'shadow-[0_25px_50px_-12px_rgba(0,0,0,0.25)]',
'shadow-[0_40px_80px_-12px_rgba(0,0,0,0.5)]',
// 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',
'duration-300 ease-out',
compact ? 'max-w-4xl p-4' : !hasCustomMaxWidth ? 'sm:max-w-2xl p-6' : 'p-6',
className
)}

View File

@@ -157,7 +157,8 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[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 min-w-[8rem] overflow-hidden rounded-lg border border-white/10 bg-popover/80 p-1 text-popover-foreground shadow-xl backdrop-blur-xl',
'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-[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',
className
)}
{...props}

View File

@@ -15,17 +15,21 @@ function Input({ className, type, startAddon, endAddon, ...props }: InputProps)
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground/60 selection:bg-primary selection:text-primary-foreground bg-input border-border 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]',
'file:text-foreground placeholder:text-muted-foreground/50 selection:bg-cyan-500/30 selection:text-cyan-100',
'bg-white/5 border-white/10 h-9 w-full min-w-0 rounded-xl border px-3 py-1 text-sm shadow-sm outline-none transition-all duration-200',
'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',
'backdrop-blur-sm',
// Hover state
'hover:bg-white/10 hover:border-white/20',
// Focus state with ring
'focus:bg-white/10 focus:border-cyan-500/50',
'focus-visible:border-cyan-500/50 focus-visible:ring-cyan-500/20 focus-visible:ring-[4px]',
'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',
hasAddons && 'border-0 shadow-none focus-visible:ring-0 bg-transparent',
className
)}
{...props}
@@ -39,10 +43,10 @@ function Input({ className, type, startAddon, endAddon, ...props }: InputProps)
return (
<div
className={cn(
'flex items-center h-9 w-full rounded-md border border-border bg-input shadow-xs',
'flex items-center h-9 w-full rounded-lg border border-input/50 bg-input/50 shadow-xs backdrop-blur-sm transition-all duration-300',
'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]',
'focus-within:bg-input/80 focus-within:border-ring/50',
'focus-within:border-ring focus-within:ring-ring/20 focus-within:ring-[4px]',
'has-[input:disabled]:opacity-50 has-[input:disabled]:cursor-not-allowed',
'has-[input[aria-invalid]]:ring-destructive/20 has-[input[aria-invalid]]:border-destructive'
)}

View File

@@ -50,10 +50,10 @@ const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(({ className, ...p
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderTrackPrimitive className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer">
<SliderRangePrimitive className="slider-range absolute h-full bg-primary" />
<SliderTrackPrimitive className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-white/10 cursor-pointer">
<SliderRangePrimitive className="slider-range absolute h-full bg-cyan-400" />
</SliderTrackPrimitive>
<SliderThumbPrimitive className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent" />
<SliderThumbPrimitive className="slider-thumb block h-4 w-4 rounded-full border border-cyan-400/50 bg-background shadow-none transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400 disabled:pointer-events-none disabled:opacity-50 hover:bg-cyan-950/30 hover:border-cyan-400" />
</SliderRootPrimitive>
));
Slider.displayName = SliderPrimitive.Root.displayName;

View File

@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-cyan-500 data-[state=unchecked]:bg-white/10',
className
)}
{...props}
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-foreground shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
'pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>

View File

@@ -16,6 +16,7 @@ import { RefreshCw } from 'lucide-react';
import { useAutoMode } from '@/hooks/use-auto-mode';
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
import { useWindowState } from '@/hooks/use-window-state';
import { PageShell } from '@/components/layout/page-shell';
// Board-view specific imports
import { BoardHeader } from './board-view/board-header';
import { BoardSearchBar } from './board-view/board-search-bar';

View File

@@ -1,4 +1,5 @@
import { HotkeyButton } from '@/components/ui/hotkey-button';
import { cn } from '@/lib/utils';
import { Slider } from '@/components/ui/slider';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
@@ -39,23 +40,20 @@ export function BoardHeader({
const showUsageTracking = !apiKeys.anthropic && !isWindows;
return (
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<header className="h-16 flex items-center justify-between px-8 border-b border-white/5 bg-[#0b101a]/40 backdrop-blur-md z-20 shrink-0">
<div>
<h1 className="text-xl font-bold">Kanban Board</h1>
<p className="text-sm text-muted-foreground">{projectName}</p>
<h2 className="text-lg font-bold text-white tracking-tight">Kanban Board</h2>
<p className="text-[10px] text-slate-500 uppercase tracking-[0.2em] font-bold mono">
{projectName}
</p>
</div>
<div className="flex gap-2 items-center">
{/* Usage Popover - only show for CLI users (not API key users) */}
{isMounted && showUsageTracking && <ClaudeUsagePopover />}
{/* Concurrency Slider - only show after mount to prevent hydration issues */}
<div className="flex items-center gap-5">
{/* Concurrency/Agent Control - Styled as Toggle for visual matching, but keeps slider logic if needed or simplified */}
{isMounted && (
<div
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border"
data-testid="concurrency-slider-container"
>
<Bot className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">Agents</span>
<div className="flex items-center bg-white/5 border border-white/10 rounded-full px-4 py-1.5 gap-3">
<Bot className="w-4 h-4 text-slate-500" />
{/* We keep the slider for functionality, but could style it to look like the toggle or just use the slider cleanly */}
<Slider
value={[maxConcurrency]}
onValueChange={(value) => onConcurrencyChange(value[0])}
@@ -63,43 +61,43 @@ export function BoardHeader({
max={10}
step={1}
className="w-20"
data-testid="concurrency-slider"
/>
<span
className="text-sm text-muted-foreground min-w-[5ch] text-center"
data-testid="concurrency-value"
>
<span className="mono text-xs font-bold text-slate-400">
{runningAgentsCount} / {maxConcurrency}
</span>
</div>
)}
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
{/* Auto Mode Button */}
{isMounted && (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border">
<Label htmlFor="auto-mode-toggle" className="text-sm font-medium cursor-pointer">
Auto Mode
</Label>
<Switch
id="auto-mode-toggle"
checked={isAutoModeRunning}
onCheckedChange={onAutoModeToggle}
data-testid="auto-mode-toggle"
<button
onClick={() => onAutoModeToggle(!isAutoModeRunning)}
className={cn(
'flex items-center gap-2 px-5 py-2 rounded-xl text-xs font-bold transition',
isAutoModeRunning
? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/20'
: 'glass hover:bg-white/10'
)}
>
<div
className={cn(
'w-2 h-2 rounded-full',
isAutoModeRunning ? 'bg-cyan-400 animate-pulse' : 'bg-slate-500'
)}
/>
</div>
Auto Mode
</button>
)}
<HotkeyButton
size="sm"
{/* Add Feature Button */}
<button
onClick={onAddFeature}
hotkey={addFeatureShortcut}
hotkeyActive={false}
data-testid="add-feature-button"
className="btn-cyan px-6 py-2 rounded-xl text-xs font-black flex items-center gap-2 shadow-lg shadow-cyan-500/20"
>
<Plus className="w-4 h-4 mr-2" />
Add Feature
</HotkeyButton>
<Plus className="w-4 h-4 stroke-[3.5px]" />
ADD FEATURE
</button>
</div>
</div>
</header>
);
}

View File

@@ -7,6 +7,7 @@ interface KanbanColumnProps {
id: string;
title: string;
colorClass: string;
columnClass?: string;
count: number;
children: ReactNode;
headerAction?: ReactNode;
@@ -21,6 +22,7 @@ export const KanbanColumn = memo(function KanbanColumn({
id,
title,
colorClass,
columnClass,
count,
children,
headerAction,
@@ -43,7 +45,8 @@ export const KanbanColumn = memo(function KanbanColumn({
'transition-[box-shadow,ring] duration-200',
!width && 'w-72', // Only apply w-72 if no custom width
showBorder && 'border border-border/60',
isOver && 'ring-2 ring-primary/30 ring-offset-1 ring-offset-background'
isOver && 'ring-2 ring-primary/30 ring-offset-1 ring-offset-background',
columnClass
)}
style={widthStyle}
data-testid={`kanban-column-${id}`}

View File

@@ -2,21 +2,25 @@ import { Feature } from '@/store/app-store';
export type ColumnId = Feature['status'];
export const COLUMNS: { id: ColumnId; title: string; colorClass: string }[] = [
{ id: 'backlog', title: 'Backlog', colorClass: 'bg-[var(--status-backlog)]' },
{
id: 'in_progress',
title: 'In Progress',
colorClass: 'bg-[var(--status-in-progress)]',
},
{
id: 'waiting_approval',
title: 'Waiting Approval',
colorClass: 'bg-[var(--status-waiting)]',
},
{
id: 'verified',
title: 'Verified',
colorClass: 'bg-[var(--status-success)]',
},
];
export const COLUMNS: { id: ColumnId; title: string; colorClass: string; columnClass?: string }[] =
[
{ id: 'backlog', title: 'Backlog', colorClass: 'bg-white/20', columnClass: '' },
{
id: 'in_progress',
title: 'In Progress',
colorClass: 'bg-cyan-400',
columnClass: 'col-in-progress',
},
{
id: 'waiting_approval',
title: 'Waiting Approval',
colorClass: 'bg-amber-500',
columnClass: 'col-waiting',
},
{
id: 'verified',
title: 'Verified',
colorClass: 'bg-emerald-500',
columnClass: 'col-verified',
},
];

View File

@@ -102,6 +102,7 @@ export function KanbanBoard({
id={column.id}
title={column.title}
colorClass={column.colorClass}
columnClass={column.columnClass}
count={columnFeatures.length}
width={columnWidth}
opacity={backgroundSettings.columnOpacity}

View File

@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useAppStore } from '@/store/app-store';
import { PageShell } from '@/components/layout/page-shell';
import { useCliStatus, useSettingsView } from './settings-view/hooks';
import { NAV_ITEMS } from './settings-view/config/navigation';
@@ -156,36 +157,38 @@ export function SettingsView() {
};
return (
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="settings-view">
{/* Header Section */}
<SettingsHeader />
<PageShell>
<div className="flex-1 flex flex-col overflow-hidden h-full" data-testid="settings-view">
{/* Header Section */}
<SettingsHeader />
{/* Content Area with Sidebar */}
<div className="flex-1 flex overflow-hidden">
{/* Side Navigation - No longer scrolls, just switches views */}
<SettingsNavigation
navItems={NAV_ITEMS}
activeSection={activeView}
currentProject={currentProject}
onNavigate={navigateTo}
/>
{/* Content Area with Sidebar */}
<div className="flex-1 flex overflow-hidden">
{/* Side Navigation - No longer scrolls, just switches views */}
<SettingsNavigation
navItems={NAV_ITEMS}
activeSection={activeView}
currentProject={currentProject}
onNavigate={navigateTo}
/>
{/* Content Panel - Shows only the active section */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto">{renderActiveSection()}</div>
{/* Content Panel - Shows only the active section */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto">{renderActiveSection()}</div>
</div>
</div>
{/* Keyboard Map Dialog */}
<KeyboardMapDialog open={showKeyboardMapDialog} onOpenChange={setShowKeyboardMapDialog} />
{/* Delete Project Confirmation Dialog */}
<DeleteProjectDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
project={currentProject}
onConfirm={moveProjectToTrash}
/>
</div>
{/* Keyboard Map Dialog */}
<KeyboardMapDialog open={showKeyboardMapDialog} onOpenChange={setShowKeyboardMapDialog} />
{/* Delete Project Confirmation Dialog */}
<DeleteProjectDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
project={currentProject}
onConfirm={moveProjectToTrash}
/>
</div>
</PageShell>
);
}

View File

@@ -11,13 +11,7 @@ export function SettingsHeader({
description = 'Configure your API keys and preferences',
}: SettingsHeaderProps) {
return (
<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={cn('shrink-0', 'border-b border-white/10', 'bg-white/5 backdrop-blur-xl')}>
<div className="px-8 py-6">
<div className="flex items-center gap-4">
<div

View File

@@ -115,7 +115,7 @@ export function SpecView() {
// Main view - spec exists
return (
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="spec-view">
<div className="flex-1 flex flex-col overflow-hidden" data-testid="spec-view">
<SpecHeader
projectPath={currentProject.path}
isRegenerating={isRegenerating}

View File

@@ -31,7 +31,7 @@ export function SpecHeader({
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
return (
<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 p-4 border-b border-white/10 bg-white/5 backdrop-blur-xl">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-muted-foreground" />
<div>

View File

@@ -46,6 +46,7 @@ import {
defaultDropAnimationSideEffects,
} from '@dnd-kit/core';
import { cn } from '@/lib/utils';
import { PageShell } from '@/components/layout/page-shell';
interface TerminalStatus {
enabled: boolean;
@@ -141,11 +142,11 @@ function TerminalTabButton({
{...dragAttributes}
{...dragListeners}
className={cn(
'flex items-center gap-1 px-3 py-1.5 text-sm rounded-t-md border-b-2 cursor-grab active:cursor-grabbing transition-colors select-none',
'flex items-center gap-1 px-3 py-1.5 text-sm rounded-t-md border-b-2 cursor-grab active:cursor-grabbing transition-all select-none',
isActive
? 'bg-background border-brand-500 text-foreground'
: 'bg-muted border-transparent text-muted-foreground hover:text-foreground hover:bg-accent',
isOver && isDropTarget && isDraggingTab && 'ring-2 ring-blue-500 bg-blue-500/10',
? 'bg-white/10 border-cyan-500 text-cyan-100 shadow-[0_-1px_10px_rgba(6,182,212,0.1)]'
: 'bg-white/5 border-transparent text-muted-foreground hover:text-cyan-50 hover:bg-white/10',
isOver && isDropTarget && isDraggingTab && 'ring-2 ring-cyan-500/50 bg-cyan-500/10',
isDragging && 'opacity-50'
)}
onClick={onClick}
@@ -192,8 +193,8 @@ function NewTabDropZone({ isDropTarget }: { isDropTarget: boolean }) {
className={cn(
'flex items-center justify-center px-3 py-1.5 rounded-t-md border-2 border-dashed transition-all',
isOver && isDropTarget
? 'border-green-500 bg-green-500/10 text-green-500'
: 'border-transparent text-muted-foreground hover:border-border'
? 'border-cyan-500/50 bg-cyan-500/10 text-cyan-400'
: 'border-transparent text-muted-foreground hover:border-white/10'
)}
>
<SquarePlus className="h-4 w-4" />
@@ -1414,252 +1415,256 @@ export function TerminalView() {
// Terminal view with tabs
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<div className="flex-1 flex flex-col overflow-hidden">
{/* Tab bar */}
<div className="flex items-center bg-card border-b border-border px-2">
{/* Tabs */}
<div className="flex items-center gap-1 flex-1 overflow-x-auto py-1">
{terminalState.tabs.map((tab) => (
<TerminalTabButton
key={tab.id}
tab={tab}
isActive={tab.id === terminalState.activeTabId}
onClick={() => setActiveTerminalTab(tab.id)}
onClose={() => killTerminalTab(tab.id)}
onRename={(newName) => renameTerminalTab(tab.id, newName)}
isDropTarget={activeDragId !== null || activeDragTabId !== null}
isDraggingTab={activeDragTabId !== null}
/>
))}
{(activeDragId || activeDragTabId) && <NewTabDropZone isDropTarget={true} />}
{/* New tab button */}
<button
className="flex items-center justify-center p-1.5 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
onClick={createTerminalInNewTab}
title="New Tab"
>
<Plus className="h-4 w-4" />
</button>
</div>
{/* Toolbar buttons */}
<div className="flex items-center gap-1 pl-2 border-l border-border">
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
onClick={() => createTerminal('horizontal')}
title="Split Right"
>
<SplitSquareHorizontal className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
onClick={() => createTerminal('vertical')}
title="Split Down"
>
<SplitSquareVertical className="h-4 w-4" />
</Button>
{/* Global Terminal Settings */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
title="Terminal Settings"
>
<Settings className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="end">
<div className="space-y-4">
<div className="space-y-2">
<h4 className="font-medium text-sm">Terminal Settings</h4>
<p className="text-xs text-muted-foreground">
Configure global terminal appearance
</p>
</div>
{/* Default Font Size */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Default Font Size</Label>
<span className="text-sm text-muted-foreground">
{terminalState.defaultFontSize}px
</span>
</div>
<Slider
value={[terminalState.defaultFontSize]}
min={8}
max={24}
step={1}
onValueChange={([value]) => setTerminalDefaultFontSize(value)}
onValueCommit={() => {
toast.info('Font size changed', {
description: 'New terminals will use this size',
});
}}
/>
</div>
{/* Font Family */}
<div className="space-y-2">
<Label className="text-sm">Font Family</Label>
<select
value={terminalState.fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className={cn(
'w-full px-2 py-1.5 rounded-md text-sm',
'bg-accent/50 border border-border',
'text-foreground',
'focus:outline-none focus:ring-2 focus:ring-ring'
)}
>
{TERMINAL_FONT_OPTIONS.map((font) => (
<option key={font.value} value={font.value}>
{font.label}
</option>
))}
</select>
</div>
{/* Line Height */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Line Height</Label>
<span className="text-sm text-muted-foreground">
{terminalState.lineHeight.toFixed(1)}
</span>
</div>
<Slider
value={[terminalState.lineHeight]}
min={1.0}
max={2.0}
step={0.1}
onValueChange={([value]) => setTerminalLineHeight(value)}
onValueCommit={() => {
toast.info('Line height changed', {
description: 'Restart terminal for changes to take effect',
});
}}
/>
</div>
{/* Default Run Script */}
<div className="space-y-2">
<Label className="text-sm">Default Run Script</Label>
<Input
value={terminalState.defaultRunScript}
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
placeholder="e.g., claude, npm run dev"
className="h-8 text-sm"
/>
<p className="text-xs text-muted-foreground">
Command to run when opening new terminals
</p>
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
{/* Active tab content */}
<div className="flex-1 overflow-hidden bg-background">
{terminalState.maximizedSessionId ? (
// When a terminal is maximized, render only that terminal
<TerminalErrorBoundary
key={`boundary-maximized-${terminalState.maximizedSessionId}`}
sessionId={terminalState.maximizedSessionId}
onRestart={() => {
const sessionId = terminalState.maximizedSessionId!;
toggleTerminalMaximized(sessionId);
killTerminal(sessionId);
createTerminal();
}}
>
<TerminalPanel
key={`maximized-${terminalState.maximizedSessionId}`}
sessionId={terminalState.maximizedSessionId}
authToken={terminalState.authToken}
isActive={true}
onFocus={() => setActiveTerminalSession(terminalState.maximizedSessionId!)}
onClose={() => killTerminal(terminalState.maximizedSessionId!)}
onSplitHorizontal={() =>
createTerminal('horizontal', terminalState.maximizedSessionId!)
}
onSplitVertical={() =>
createTerminal('vertical', terminalState.maximizedSessionId!)
}
onNewTab={createTerminalInNewTab}
onSessionInvalid={() => {
const sessionId = terminalState.maximizedSessionId!;
console.log(
`[Terminal] Maximized session ${sessionId} is invalid, removing from layout`
);
killTerminal(sessionId);
}}
isDragging={false}
isDropTarget={false}
fontSize={findTerminalFontSize(terminalState.maximizedSessionId)}
onFontSizeChange={(size) =>
setTerminalPanelFontSize(terminalState.maximizedSessionId!, size)
}
isMaximized={true}
onToggleMaximize={() => toggleTerminalMaximized(terminalState.maximizedSessionId!)}
/>
</TerminalErrorBoundary>
) : activeTab?.layout ? (
renderPanelContent(activeTab.layout)
) : (
<div className="flex-1 flex flex-col items-center justify-center text-center p-6">
<p className="text-muted-foreground mb-4">This tab is empty</p>
<Button variant="outline" size="sm" onClick={() => createTerminal()}>
<Plus className="h-4 w-4 mr-2" />
New Terminal
</Button>
</div>
)}
</div>
</div>
{/* Drag overlay */}
<DragOverlay
dropAnimation={{
sideEffects: defaultDropAnimationSideEffects({
styles: { active: { opacity: '0.5' } },
}),
}}
zIndex={1000}
<PageShell fullWidth>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
{activeDragId ? (
<div className="relative inline-flex items-center gap-2 px-3.5 py-2 bg-card border-2 border-brand-500 rounded-lg shadow-xl pointer-events-none overflow-hidden">
<TerminalIcon className="h-4 w-4 text-brand-500 shrink-0" />
<span className="text-sm font-medium text-foreground whitespace-nowrap">
{dragOverTabId === 'new' ? 'New tab' : dragOverTabId ? 'Move to tab' : 'Terminal'}
</span>
<div className="flex-1 flex flex-col overflow-hidden">
{/* Tab bar */}
<div className="flex items-center bg-card border-b border-border px-2">
{/* Tabs */}
<div className="flex items-center gap-1 flex-1 overflow-x-auto py-1">
{terminalState.tabs.map((tab) => (
<TerminalTabButton
key={tab.id}
tab={tab}
isActive={tab.id === terminalState.activeTabId}
onClick={() => setActiveTerminalTab(tab.id)}
onClose={() => killTerminalTab(tab.id)}
onRename={(newName) => renameTerminalTab(tab.id, newName)}
isDropTarget={activeDragId !== null || activeDragTabId !== null}
isDraggingTab={activeDragTabId !== null}
/>
))}
{(activeDragId || activeDragTabId) && <NewTabDropZone isDropTarget={true} />}
{/* New tab button */}
<button
className="flex items-center justify-center p-1.5 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
onClick={createTerminalInNewTab}
title="New Tab"
>
<Plus className="h-4 w-4" />
</button>
</div>
{/* Toolbar buttons */}
<div className="flex items-center gap-1 pl-2 border-l border-border">
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
onClick={() => createTerminal('horizontal')}
title="Split Right"
>
<SplitSquareHorizontal className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
onClick={() => createTerminal('vertical')}
title="Split Down"
>
<SplitSquareVertical className="h-4 w-4" />
</Button>
{/* Global Terminal Settings */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
title="Terminal Settings"
>
<Settings className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="end">
<div className="space-y-4">
<div className="space-y-2">
<h4 className="font-medium text-sm">Terminal Settings</h4>
<p className="text-xs text-muted-foreground">
Configure global terminal appearance
</p>
</div>
{/* Default Font Size */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Default Font Size</Label>
<span className="text-sm text-muted-foreground">
{terminalState.defaultFontSize}px
</span>
</div>
<Slider
value={[terminalState.defaultFontSize]}
min={8}
max={24}
step={1}
onValueChange={([value]) => setTerminalDefaultFontSize(value)}
onValueCommit={() => {
toast.info('Font size changed', {
description: 'New terminals will use this size',
});
}}
/>
</div>
{/* Font Family */}
<div className="space-y-2">
<Label className="text-sm">Font Family</Label>
<select
value={terminalState.fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className={cn(
'w-full px-2 py-1.5 rounded-md text-sm',
'bg-accent/50 border border-border',
'text-foreground',
'focus:outline-none focus:ring-2 focus:ring-ring'
)}
>
{TERMINAL_FONT_OPTIONS.map((font) => (
<option key={font.value} value={font.value}>
{font.label}
</option>
))}
</select>
</div>
{/* Line Height */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Line Height</Label>
<span className="text-sm text-muted-foreground">
{terminalState.lineHeight.toFixed(1)}
</span>
</div>
<Slider
value={[terminalState.lineHeight]}
min={1.0}
max={2.0}
step={0.1}
onValueChange={([value]) => setTerminalLineHeight(value)}
onValueCommit={() => {
toast.info('Line height changed', {
description: 'Restart terminal for changes to take effect',
});
}}
/>
</div>
{/* Default Run Script */}
<div className="space-y-2">
<Label className="text-sm">Default Run Script</Label>
<Input
value={terminalState.defaultRunScript}
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
placeholder="e.g., claude, npm run dev"
className="h-8 text-sm"
/>
<p className="text-xs text-muted-foreground">
Command to run when opening new terminals
</p>
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
) : null}
</DragOverlay>
</DndContext>
{/* Active tab content */}
<div className="flex-1 overflow-hidden bg-background">
{terminalState.maximizedSessionId ? (
// When a terminal is maximized, render only that terminal
<TerminalErrorBoundary
key={`boundary-maximized-${terminalState.maximizedSessionId}`}
sessionId={terminalState.maximizedSessionId}
onRestart={() => {
const sessionId = terminalState.maximizedSessionId!;
toggleTerminalMaximized(sessionId);
killTerminal(sessionId);
createTerminal();
}}
>
<TerminalPanel
key={`maximized-${terminalState.maximizedSessionId}`}
sessionId={terminalState.maximizedSessionId}
authToken={terminalState.authToken}
isActive={true}
onFocus={() => setActiveTerminalSession(terminalState.maximizedSessionId!)}
onClose={() => killTerminal(terminalState.maximizedSessionId!)}
onSplitHorizontal={() =>
createTerminal('horizontal', terminalState.maximizedSessionId!)
}
onSplitVertical={() =>
createTerminal('vertical', terminalState.maximizedSessionId!)
}
onNewTab={createTerminalInNewTab}
onSessionInvalid={() => {
const sessionId = terminalState.maximizedSessionId!;
console.log(
`[Terminal] Maximized session ${sessionId} is invalid, removing from layout`
);
killTerminal(sessionId);
}}
isDragging={false}
isDropTarget={false}
fontSize={findTerminalFontSize(terminalState.maximizedSessionId)}
onFontSizeChange={(size) =>
setTerminalPanelFontSize(terminalState.maximizedSessionId!, size)
}
isMaximized={true}
onToggleMaximize={() =>
toggleTerminalMaximized(terminalState.maximizedSessionId!)
}
/>
</TerminalErrorBoundary>
) : activeTab?.layout ? (
renderPanelContent(activeTab.layout)
) : (
<div className="flex-1 flex flex-col items-center justify-center text-center p-6">
<p className="text-muted-foreground mb-4">This tab is empty</p>
<Button variant="outline" size="sm" onClick={() => createTerminal()}>
<Plus className="h-4 w-4 mr-2" />
New Terminal
</Button>
</div>
)}
</div>
</div>
{/* Drag overlay */}
<DragOverlay
dropAnimation={{
sideEffects: defaultDropAnimationSideEffects({
styles: { active: { opacity: '0.5' } },
}),
}}
zIndex={1000}
>
{activeDragId ? (
<div className="relative inline-flex items-center gap-2 px-3.5 py-2 bg-card border-2 border-brand-500 rounded-lg shadow-xl pointer-events-none overflow-hidden">
<TerminalIcon className="h-4 w-4 text-brand-500 shrink-0" />
<span className="text-sm font-medium text-foreground whitespace-nowrap">
{dragOverTabId === 'new' ? 'New tab' : dragOverTabId ? 'Move to tab' : 'Terminal'}
</span>
</div>
) : null}
</DragOverlay>
</DndContext>
</PageShell>
);
}

View File

@@ -4,14 +4,12 @@ import type { Project, TrashedProject } from '@/lib/electron';
import type {
Feature as BaseFeature,
FeatureImagePath,
FeatureTextFilePath, // Import missing type
AgentModel,
PlanningMode,
AIProfile,
} from '@automaker/types';
// Re-export ThemeMode for convenience
export type { ThemeMode };
export type ViewMode =
| 'welcome'
| 'setup'
@@ -43,7 +41,25 @@ export type ThemeMode =
| 'red'
| 'cream'
| 'sunset'
| 'gray';
| 'gray'
| 'forest'
| 'ocean'
| 'light'
| 'cream'
| 'solarizedlight'
| 'github'
| 'paper'
| 'rose'
| 'mint'
| 'lavender'
| 'sand'
| 'sky'
| 'peach'
| 'snow'
| 'sepia'
| 'gruvboxlight'
| 'nordlight'
| 'blossom';
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
@@ -903,19 +919,19 @@ const initialState: AppState = {
chatHistoryOpen: false,
autoModeByProject: {},
autoModeActivityLog: [],
maxConcurrency: 3, // Default to 3 concurrent agents
kanbanCardDetailLevel: 'standard', // Default to standard detail level
boardViewMode: 'kanban', // Default to kanban view
defaultSkipTests: true, // Default to manual verification (tests disabled)
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
useWorktrees: false, // Default to disabled (worktree feature is experimental)
maxConcurrency: 3,
kanbanCardDetailLevel: 'standard',
boardViewMode: 'kanban',
defaultSkipTests: false,
enableDependencyBlocking: true,
useWorktrees: false,
currentWorktreeByProject: {},
worktreesByProject: {},
showProfilesOnly: false, // Default to showing all options (not profiles only)
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
muteDoneSound: false, // Default to sound enabled (not muted)
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
aiProfiles: DEFAULT_AI_PROFILES,
showProfilesOnly: false,
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
muteDoneSound: false,
enhancementModel: 'sonnet',
projectAnalysis: null,
isAnalyzing: false,
boardBackgroundByProject: {},
@@ -928,19 +944,23 @@ const initialState: AppState = {
activeSessionId: null,
maximizedSessionId: null,
defaultFontSize: 14,
defaultRunScript: '',
defaultRunScript: '', // Empty string = standard shell
screenReaderMode: false,
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
scrollbackLines: 5000,
lineHeight: 1.0,
maxSessions: 100,
fontFamily: 'monospace',
scrollbackLines: 1000,
lineHeight: 1.2,
maxSessions: 20,
},
terminalLayoutByProject: {},
specCreatingForProject: null,
defaultPlanningMode: 'skip' as PlanningMode,
defaultRequirePlanApproval: false,
defaultPlanningMode: 'lite',
defaultRequirePlanApproval: true,
defaultAIProfileId: null,
pendingPlanApproval: null,
// Claude Usage Defaults
claudeRefreshInterval: 60,
claudeUsage: null,
claudeUsageLastUpdated: null,
};
export const useAppStore = create<AppState & AppActions>()(

View File

@@ -1,3 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
@import 'tailwindcss';
@import 'tw-animate-css';
@@ -43,8 +44,8 @@
--color-foreground: var(--foreground);
--color-foreground-secondary: var(--foreground-secondary);
--color-foreground-muted: var(--foreground-muted);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace;
/* Sidebar colors */
--color-sidebar-ring: var(--sidebar-ring);
@@ -119,29 +120,94 @@
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
/* Animations */
--animate-in-fade: in-fade 0.3s cubic-bezier(0.16, 1, 0.3, 1);
--animate-in-scale: in-scale 0.3s cubic-bezier(0.16, 1, 0.3, 1);
--animate-out-fade: out-fade 0.2s cubic-bezier(0.16, 1, 0.3, 1);
--animate-out-scale: out-scale 0.2s cubic-bezier(0.16, 1, 0.3, 1);
@keyframes in-fade {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes in-scale {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes out-fade {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(8px);
}
}
@keyframes out-scale {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.96);
}
}
}
:root {
/* Default to light mode */
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
/* Default to light mode (overridden by Prism values below for now as we pivot) */
--radius: 0.75rem;
/* PRISM THEME VALUES (Base) */
--bg-deep: #0b101a;
--sidebar-bg: rgba(13, 17, 26, 0.7);
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.07);
/* Accents */
--accent-cyan: #22d3ee;
--accent-orange: #f59e0b;
--accent-green: #10b981;
--accent-red: #ef4444;
/* Mapping to existing variables where possible, or overriding for the visual appearance */
--background: oklch(0.08 0.03 260); /* Approximation of #0b101a */
--foreground: oklch(0.95 0 0);
--card: oklch(1 0 0 / 0.03); /* Glass card default */
--card-foreground: oklch(0.95 0 0);
--popover: oklch(0.08 0.03 260 / 0.9);
--popover-foreground: oklch(0.95 0 0);
--primary: oklch(0.78 0.15 200); /* Cyan-ish */
--primary-foreground: oklch(0.1 0.03 260);
--secondary: oklch(1 0 0 / 0.1);
--secondary-foreground: oklch(0.98 0 0);
--muted: oklch(1 0 0 / 0.05);
--muted-foreground: oklch(0.7 0 0);
--accent: oklch(0.78 0.15 200);
--accent-foreground: oklch(0.1 0.03 260);
--destructive: oklch(0.6 0.2 20);
--border: oklch(1 0 0 / 0.1);
--input: oklch(1 0 0 / 0.05);
--ring: oklch(0.78 0.15 200);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
@@ -159,7 +225,7 @@
--background-80: oklch(1 0 0 / 0.8);
--foreground-secondary: oklch(0.4 0 0);
--foreground-muted: oklch(0.556 0 0);
--border-glass: oklch(0.145 0 0 / 0.1);
--border-glass: oklch(0.145 0 0 / 0.08);
--brand-400: oklch(0.6 0.22 265);
--brand-500: oklch(0.55 0.25 265);
--brand-600: oklch(0.5 0.28 270);
@@ -191,17 +257,17 @@
--status-in-progress: oklch(0.7 0.15 70);
--status-waiting: oklch(0.65 0.18 50);
/* Shadow tokens */
/* Shadow tokens - more diffused for modern feel */
--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);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 10px rgba(0, 0, 0, 0.02);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 12px 24px -4px rgba(0, 0, 0, 0.04);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 20px 40px -8px rgba(0, 0, 0, 0.06);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 30px 60px -12px rgba(0, 0, 0, 0.1);
/* Transition tokens */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
--transition-slow: 300ms ease-out;
--transition-fast: 150ms cubic-bezier(0.16, 1, 0.3, 1);
--transition-normal: 250ms cubic-bezier(0.16, 1, 0.3, 1);
--transition-slow: 400ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* Apply dark mode immediately based on system preference (before JS runs) */
@@ -214,13 +280,13 @@
/* Text colors following hierarchy */
--foreground: oklch(1 0 0); /* text-white */
--foreground-secondary: oklch(0.588 0 0); /* text-zinc-400 */
--foreground-muted: oklch(0.525 0 0); /* text-zinc-500 */
--foreground-secondary: oklch(0.65 0 0); /* lighter for better readability */
--foreground-muted: oklch(0.525 0 0);
/* Card and popover backgrounds */
--card: oklch(0.14 0 0);
--card: oklch(0.1 0 0 / 0.6); /* Slightly transparent */
--card-foreground: oklch(1 0 0);
--popover: oklch(0.1 0 0);
--popover: oklch(0.08 0 0 / 0.9);
--popover-foreground: oklch(1 0 0);
/* Brand colors - purple/violet theme */
@@ -231,18 +297,18 @@
--brand-600: oklch(0.5 0.28 270);
/* Glass morphism borders and accents */
--secondary: oklch(1 0 0 / 0.05);
--secondary: oklch(1 0 0 / 0.08);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.176 0 0);
--muted-foreground: oklch(0.588 0 0);
--accent: oklch(1 0 0 / 0.1);
--muted-foreground: oklch(0.6 0 0);
--accent: oklch(1 0 0 / 0.12);
--accent-foreground: oklch(1 0 0);
/* Borders with transparency for glass effect */
--border: oklch(0.176 0 0);
--border-glass: oklch(1 0 0 / 0.1);
--border: oklch(1 0 0 / 0.08);
--border-glass: oklch(1 0 0 / 0.12);
--destructive: oklch(0.6 0.25 25);
--input: oklch(0.04 0 0 / 0.8);
--input: oklch(1 0 0 / 0.08);
--ring: oklch(0.55 0.25 265);
/* Chart colors with brand theme */
@@ -253,13 +319,13 @@
--chart-5: oklch(0.6 0.25 20);
/* Sidebar with glass morphism */
--sidebar: oklch(0.04 0 0 / 0.5);
--sidebar: oklch(0.04 0 0 / 0.6);
--sidebar-foreground: oklch(1 0 0);
--sidebar-primary: oklch(0.55 0.25 265);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(1 0 0 / 0.05);
--sidebar-accent: oklch(1 0 0 / 0.08);
--sidebar-accent-foreground: oklch(1 0 0);
--sidebar-border: oklch(1 0 0 / 0.1);
--sidebar-border: oklch(1 0 0 / 0.08);
--sidebar-ring: oklch(0.55 0.25 265);
/* Action button colors */
@@ -292,67 +358,21 @@
/* 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);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.6), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.7), 0 10px 10px -5px rgba(0, 0, 0, 0.5);
}
}
.light {
/* Explicit light mode - same as root but ensures it overrides any dark defaults */
/* Explicit light mode overrides */
--background: oklch(1 0 0); /* White */
--background-50: oklch(1 0 0 / 0.5);
--background-80: oklch(1 0 0 / 0.8);
--foreground: oklch(0.145 0 0); /* Dark text */
--foreground-secondary: oklch(0.4 0 0);
--foreground-muted: oklch(0.556 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.55 0.25 265);
--primary-foreground: oklch(1 0 0);
--brand-400: oklch(0.6 0.22 265);
--brand-500: oklch(0.55 0.25 265);
--brand-600: oklch(0.5 0.28 270);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.95 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--border-glass: oklch(0.145 0 0 / 0.1);
--input: oklch(1 0 0);
--ring: oklch(0.55 0.25 265);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.98 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.55 0.25 265);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.95 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.9 0 0);
--sidebar-ring: oklch(0.55 0.25 265);
/* Action button colors */
--action-view: oklch(0.55 0.25 265); /* Purple */
--action-view-hover: oklch(0.5 0.28 270);
--action-followup: oklch(0.55 0.2 230); /* Blue */
--action-followup-hover: oklch(0.5 0.22 230);
--action-commit: oklch(0.55 0.2 140); /* Green */
--action-commit-hover: oklch(0.5 0.22 140);
--action-verify: oklch(0.55 0.2 140); /* Green */
--action-verify-hover: oklch(0.5 0.22 140);
/* Running indicator - Purple */
--running-indicator: oklch(0.55 0.25 265);
--running-indicator-text: oklch(0.6 0.22 265);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
/* Re-declare all light vars to ensure priority if needed,
but :root typically handles this.
Just ensuring important overrides. */
--card: oklch(1 0 0 / 0.8);
}
@layer base {
@@ -360,10 +380,13 @@
@apply border-border outline-ring/50;
}
html {
@apply bg-background;
@apply bg-background antialiased;
font-feature-settings:
'rlig' 1,
'calt' 1;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground tracking-tight;
background-color: var(--background);
}
@@ -386,6 +409,7 @@
select:disabled,
textarea:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
@@ -407,8 +431,8 @@
.gray
)
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 6px;
height: 6px;
}
:is(
@@ -428,110 +452,35 @@
.gray
)
::-webkit-scrollbar-track {
background: var(--muted);
background: transparent;
}
:is(.dark, .retro) ::-webkit-scrollbar-thumb {
background: oklch(0.3 0 0);
border-radius: 4px;
background: oklch(1 0 0 / 0.2);
border-radius: 99px;
transition: background 0.2s;
}
:is(.dark, .retro) ::-webkit-scrollbar-thumb:hover {
background: oklch(0.4 0 0);
}
/* Retro Scrollbar override */
.retro ::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 0;
}
.retro ::-webkit-scrollbar-track {
background: var(--background);
}
/* Red theme scrollbar */
.red ::-webkit-scrollbar-thumb {
background: oklch(0.35 0.15 25);
border-radius: 4px;
}
.red ::-webkit-scrollbar-thumb:hover {
background: oklch(0.45 0.18 25);
}
.red ::-webkit-scrollbar-track {
background: oklch(0.15 0.05 25);
}
/* Always visible scrollbar for file diffs and code blocks */
.scrollbar-visible {
overflow-y: auto !important;
scrollbar-width: thin;
scrollbar-color: var(--muted-foreground) var(--muted);
}
.scrollbar-visible::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.scrollbar-visible::-webkit-scrollbar-track {
background: var(--muted);
border-radius: 4px;
}
.scrollbar-visible::-webkit-scrollbar-thumb {
background: var(--muted-foreground);
border-radius: 4px;
min-height: 40px;
}
.scrollbar-visible::-webkit-scrollbar-thumb:hover {
background: var(--foreground-secondary);
}
/* Force scrollbar to always be visible (not auto-hide) */
.scrollbar-visible::-webkit-scrollbar-thumb {
visibility: visible;
}
/* Styled scrollbar for code blocks and log entries (horizontal/vertical) */
.scrollbar-styled {
scrollbar-width: thin;
scrollbar-color: var(--muted-foreground) transparent;
}
.scrollbar-styled::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.scrollbar-styled::-webkit-scrollbar-track {
background: transparent;
border-radius: 3px;
}
.scrollbar-styled::-webkit-scrollbar-thumb {
background: oklch(0.35 0 0);
border-radius: 3px;
}
.scrollbar-styled::-webkit-scrollbar-thumb:hover {
background: oklch(0.45 0 0);
background: oklch(1 0 0 / 0.3);
}
/* Glass morphism utilities */
@layer utilities {
.glass {
@apply backdrop-blur-md border-white/10;
@apply backdrop-blur-xl border border-white/10 bg-white/5;
}
.glass-subtle {
@apply backdrop-blur-sm border-white/5;
@apply backdrop-blur-md border border-white/5 bg-white/3;
}
.glass-strong {
@apply backdrop-blur-xl border-white/20;
@apply backdrop-blur-2xl border border-white/10 bg-black/40;
}
.glass-panel {
@apply backdrop-blur-3xl border border-border-glass bg-background/60 shadow-xl;
}
/* Text color hierarchy utilities */
@@ -540,55 +489,73 @@
}
.text-secondary {
color: oklch(0.588 0 0); /* zinc-400 equivalent */
color: var(--foreground-secondary);
}
.text-muted {
color: oklch(0.525 0 0); /* zinc-500 equivalent */
color: var(--foreground-muted);
}
/* Brand gradient utilities */
.gradient-brand {
background: linear-gradient(135deg, oklch(0.55 0.25 265), oklch(0.5 0.28 270));
background: linear-gradient(135deg, var(--brand-500), var(--brand-600));
}
.gradient-brand-subtle {
background: linear-gradient(135deg, oklch(0.55 0.25 265 / 0.1), oklch(0.5 0.28 270 / 0.1));
background: linear-gradient(
135deg,
color-mix(in oklch, var(--brand-500), transparent 90%),
color-mix(in oklch, var(--brand-600), transparent 90%)
);
}
/* Glass morphism background utilities */
.bg-glass {
background: var(--card);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.bg-glass-80 {
background: var(--popover);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
.gradient-glass-border {
background: linear-gradient(
to bottom right,
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0.05)
);
}
/* Hover state utilities */
.hover-glass {
transition: background-color 0.2s ease;
transition:
background-color 0.2s ease,
transform 0.2s ease;
}
.hover-glass:hover {
background: oklch(1 0 0 / 0.05);
background: color-mix(in oklch, var(--foreground), transparent 95%);
}
.hover-glass-strong {
transition: background-color 0.2s ease;
.hover-lift {
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.hover-glass-strong:hover {
background: oklch(1 0 0 / 0.1);
.hover-lift:hover {
transform: translateY(-2px);
}
.hover-glow {
transition: box-shadow 0.3s ease;
}
.hover-glow:hover {
box-shadow: 0 0 20px -5px var(--primary);
}
/* Content area background */
.content-bg {
background: var(--background);
background: transparent;
}
/* Utils for hiding scrollbar but allowing scroll */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Action button utilities */
@@ -644,7 +611,6 @@
}
/* Animated border for in-progress cards */
/* Using a subtle pulse animation instead of continuous gradient rotation for GPU efficiency */
@keyframes border-pulse {
0%,
100% {
@@ -657,8 +623,8 @@
.animated-border-wrapper {
position: relative;
border-radius: 0.75rem;
padding: 2px;
border-radius: var(--radius);
padding: 1px;
background: linear-gradient(
135deg,
var(--running-indicator),
@@ -677,7 +643,7 @@
}
.animated-border-wrapper > * {
border-radius: calc(0.75rem - 2px);
border-radius: calc(var(--radius) - 1px);
}
}
@@ -685,6 +651,7 @@
.retro * {
border-radius: 0 !important;
--radius: 0;
}
/* Animated Outline Button Styles */
@@ -693,12 +660,6 @@
background: conic-gradient(from 90deg at 50% 50%, #a855f7 0%, #3b82f6 50%, #a855f7 100%);
}
/* Light mode - deeper purple to blue gradient for better visibility */
/* Dark mode - purple to blue gradient */
/* Retro mode - unique scanline + neon effect */
@keyframes retro-glow {
from {
filter: brightness(1) drop-shadow(0 0 2px #00ff41);
@@ -708,24 +669,6 @@
}
}
/* Dracula animated-outline - purple/pink */
/* Nord animated-outline - frost blue */
/* Monokai animated-outline - pink/yellow */
/* Tokyo Night animated-outline - blue/magenta */
/* Solarized animated-outline - blue/cyan */
/* Gruvbox animated-outline - yellow/orange */
/* Catppuccin animated-outline - mauve/pink */
/* One Dark animated-outline - blue/magenta */
/* Synthwave animated-outline - hot pink/cyan with glow */
@keyframes synthwave-glow {
from {
filter: brightness(1) drop-shadow(0 0 3px #f97e72);
@@ -735,26 +678,6 @@
}
}
/* Slider Theme Styles */
/* Dracula slider */
/* Nord slider */
/* Monokai slider */
/* Tokyo Night slider */
/* Solarized slider */
/* Gruvbox slider */
/* Catppuccin slider */
/* One Dark slider */
/* Synthwave slider */
/* Line clamp utilities for text overflow prevention */
.line-clamp-2 {
display: -webkit-box;
@@ -802,30 +725,6 @@
Theme-aware colors for XML editor
======================================== */
/* Light theme - professional and readable */
/* Dark theme - high contrast */
/* Retro theme - neon green on black */
/* Dracula theme */
/* Nord theme */
/* Monokai theme */
/* Tokyo Night theme */
/* Solarized theme */
/* Gruvbox theme */
/* Catppuccin theme */
/* One Dark theme */
/* Synthwave theme */
/* XML Editor container styles */
.xml-editor {
position: relative;
@@ -1058,3 +957,88 @@
animation: none;
}
}
/* Prism Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Prism Shortcut Badge */
.shortcut-badge {
font-size: 9px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.3);
padding: 1px 5px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.08);
font-family: var(--font-mono);
font-weight: 600;
}
/* Prism Toggle Switch */
.toggle-track {
width: 42px;
height: 20px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
position: relative;
cursor: pointer;
}
.toggle-thumb {
position: absolute;
right: 3px;
top: 3px;
width: 12px;
height: 12px;
background: var(--accent-cyan);
border-radius: 50%;
box-shadow: 0 0 8px var(--accent-cyan);
}
/* Prism Column Styles */
.col-in-progress {
border: 1px solid rgba(34, 211, 238, 0.25);
background: rgba(34, 211, 238, 0.02);
box-shadow: inset 0 0 40px rgba(34, 211, 238, 0.03);
}
.col-waiting {
border-top: 2px solid rgba(245, 158, 11, 0.3);
}
.col-verified {
border-top: 2px solid rgba(16, 185, 129, 0.3);
}
/* Prism Font Overrides - Ensure these take precedence */
:root {
--font-sans:
'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
--font-mono:
'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
/* Ensure backgrounds are deep prism color */
--background: oklch(0.08 0.03 260);
--foreground: oklch(0.95 0 0);
}
body {
background-color: var(--bg-deep);
font-family: var(--font-sans);
}
.mono {
font-family: var(--font-mono) !important;
}

723
index (25).html Normal file
View File

@@ -0,0 +1,723 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>automaker. | Kanban Board</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<style>
:root {
--bg-deep: #0b101a;
--sidebar-bg: rgba(13, 17, 26, 0.7);
--accent-cyan: #22d3ee;
--accent-orange: #f59e0b;
--accent-green: #10b981;
--accent-red: #ef4444;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.07);
--card-radius: 1.5rem;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-deep);
color: #e2e8f0;
overflow: hidden;
height: 100vh;
}
.mono {
font-family: 'JetBrains Mono', monospace;
}
/* Rainbow Prism Background Effect */
.prism-bg {
position: fixed;
inset: 0;
z-index: -1;
background:
radial-gradient(circle at 10% 20%, rgba(34, 211, 238, 0.1) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(139, 92, 246, 0.08) 0%, transparent 40%),
radial-gradient(circle at 50% 50%, rgba(245, 158, 11, 0.04) 0%, transparent 60%),
linear-gradient(
145deg,
rgba(255, 0, 0, 0.02) 0%,
rgba(0, 255, 255, 0.02) 50%,
rgba(0, 0, 255, 0.02) 100%
);
filter: blur(80px);
}
/* Glassmorphism */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(24px);
border: 1px solid var(--glass-border);
}
.sidebar-glass {
background: var(--sidebar-bg);
backdrop-filter: blur(40px);
border-right: 1px solid rgba(255, 255, 255, 0.04);
}
/* Nav Active State */
.nav-active {
background: linear-gradient(90deg, rgba(34, 211, 238, 0.12) 0%, transparent 100%);
border-left: 3px solid var(--accent-cyan);
color: #fff;
}
/* Column Specific Visuals */
.col-in-progress {
border: 1px solid rgba(34, 211, 238, 0.25);
background: rgba(34, 211, 238, 0.02);
box-shadow: inset 0 0 40px rgba(34, 211, 238, 0.03);
}
.col-waiting {
border-top: 2px solid rgba(245, 158, 11, 0.3);
}
.col-verified {
border-top: 2px solid rgba(16, 185, 129, 0.3);
}
/* Custom Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Shortcut Badge */
.shortcut-badge {
font-size: 9px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.3);
padding: 1px 5px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.08);
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
}
/* Toggle Switch */
.toggle-track {
width: 42px;
height: 20px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
position: relative;
cursor: pointer;
}
.toggle-thumb {
position: absolute;
right: 3px;
top: 3px;
width: 12px;
height: 12px;
background: var(--accent-cyan);
border-radius: 50%;
box-shadow: 0 0 8px var(--accent-cyan);
}
/* Card Styles */
.kanban-card {
border-radius: var(--card-radius);
padding: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.kanban-card:hover {
transform: translateY(-3px);
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.15);
}
.btn-cyan {
background: var(--accent-cyan);
color: #0b101a;
font-weight: 800;
transition: all 0.2s;
}
.btn-cyan:hover {
filter: brightness(1.1);
box-shadow: 0 0 15px rgba(34, 211, 238, 0.4);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.glow-cyan {
box-shadow: 0 0 10px var(--accent-cyan);
}
.glow-orange {
box-shadow: 0 0 10px var(--accent-orange);
}
.glow-green {
box-shadow: 0 0 10px var(--accent-green);
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body class="flex h-screen w-screen overflow-hidden">
<div class="prism-bg"></div>
<!-- SIDEBAR -->
<aside class="w-64 h-full sidebar-glass flex flex-col z-30 shrink-0">
<!-- Brand -->
<div class="p-6 flex items-center gap-2.5">
<div class="bg-cyan-500/10 p-1.5 rounded-lg border border-cyan-500/30">
<i data-lucide="code-2" class="w-5 h-5 text-cyan-400 stroke-[2.5px]"></i>
</div>
<h1 class="text-xl font-bold tracking-tighter text-white">
automaker<span class="text-cyan-400">.</span>
</h1>
</div>
<!-- Top Actions -->
<div class="px-4 space-y-3 mb-8">
<div class="flex gap-2">
<button
class="flex-1 glass py-2 rounded-xl text-[11px] font-bold hover:bg-white/10 transition flex items-center justify-center gap-2"
>
<i data-lucide="plus" class="w-3.5 h-3.5"></i> New
</button>
<button
class="w-10 glass rounded-xl flex items-center justify-center hover:bg-white/10 transition relative"
>
<i data-lucide="file-text" class="w-4 h-4 opacity-40"></i>
<span class="mono text-[8px] absolute top-1 right-1.5 opacity-40 font-bold">0</span>
</button>
<button
class="w-10 glass rounded-xl flex items-center justify-center hover:bg-white/10 transition relative"
>
<i data-lucide="layers" class="w-4 h-4 opacity-40"></i>
<span
class="absolute -top-0.5 -right-0.5 w-2.5 h-2.5 bg-rose-500 rounded-full border border-[#0b101a]"
></span>
</button>
</div>
<!-- Project Dropdown -->
<div
class="glass p-3.5 rounded-2xl flex items-center justify-between cursor-pointer hover:bg-white/5 transition group"
>
<div class="flex items-center gap-3">
<i data-lucide="folder-kanban" class="w-4 h-4 text-cyan-400"></i>
<span class="text-xs font-bold text-slate-200">test case 1</span>
</div>
<div class="flex items-center gap-2">
<span class="shortcut-badge">P</span>
<i data-lucide="chevron-down" class="w-4 h-4 text-slate-500 group-hover:text-white"></i>
</div>
</div>
</div>
<!-- Navigation -->
<nav class="flex-1 px-0 space-y-8 overflow-y-auto custom-scrollbar">
<div>
<p class="px-6 text-[10px] font-black text-slate-600 uppercase tracking-[0.2em] mb-3">
Project
</p>
<div class="space-y-0.5">
<a href="#" class="nav-active flex items-center justify-between px-6 py-3 text-sm">
<div class="flex items-center gap-3">
<i data-lucide="layout-grid" class="w-4 h-4"></i
><span class="font-medium">Kanban Board</span>
</div>
<span class="shortcut-badge">E</span>
</a>
<a
href="#"
class="flex items-center justify-between px-6 py-3 text-sm text-slate-400 hover:text-white hover:bg-white/5 transition"
>
<div class="flex items-center gap-3">
<i data-lucide="zap" class="w-4 h-4"></i
><span class="font-medium">Agent Runner</span>
</div>
<span class="shortcut-badge">A</span>
</a>
</div>
</div>
<div>
<p class="px-6 text-[10px] font-black text-slate-600 uppercase tracking-[0.2em] mb-3">
Tools
</p>
<div class="space-y-0.5">
<a
href="#"
class="flex items-center justify-between px-6 py-3 text-sm text-slate-400 hover:text-white hover:bg-white/5 transition"
>
<div class="flex items-center gap-3">
<i data-lucide="file-edit" class="w-4 h-4"></i><span>Spec Editor</span>
</div>
<span class="shortcut-badge">D</span>
</a>
<a
href="#"
class="flex items-center justify-between px-6 py-3 text-sm text-slate-400 hover:text-white hover:bg-white/5 transition"
>
<div class="flex items-center gap-3">
<i data-lucide="database" class="w-4 h-4"></i><span>Context</span>
</div>
<span class="shortcut-badge">C</span>
</a>
<a
href="#"
class="flex items-center justify-between px-6 py-3 text-sm text-slate-400 hover:text-white hover:bg-white/5 transition"
>
<div class="flex items-center gap-3">
<i data-lucide="user-circle" class="w-4 h-4"></i><span>AI Profiles</span>
</div>
<span class="shortcut-badge">H</span>
</a>
<a
href="#"
class="flex items-center justify-between px-6 py-3 text-sm text-slate-400 hover:text-white hover:bg-white/5 transition"
>
<div class="flex items-center gap-3">
<i data-lucide="terminal" class="w-4 h-4"></i><span>Terminal</span>
</div>
<span class="shortcut-badge">T</span>
</a>
</div>
</div>
</nav>
<!-- Footer -->
<div class="p-4 border-t border-white/5 space-y-1 mt-auto bg-black/10">
<a
href="#"
class="flex items-center gap-3 px-3 py-2 text-sm text-slate-400 hover:text-white transition"
>
<i data-lucide="book-open" class="w-4 h-4"></i> Wiki
</a>
<div
class="flex items-center justify-between px-3 py-2 text-sm text-slate-400 hover:text-white cursor-pointer group transition"
>
<div class="flex items-center gap-3">
<i data-lucide="activity" class="w-4 h-4 text-cyan-400"></i> Running Agents
</div>
<span class="bg-cyan-500 text-slate-950 font-black text-[10px] px-2 py-0.5 rounded-full"
>3</span
>
</div>
<a
href="#"
class="flex items-center gap-3 px-3 py-2 text-sm text-slate-400 hover:text-white transition"
>
<i data-lucide="settings" class="w-4 h-4"></i> Settings
<span class="ml-auto shortcut-badge">S</span>
</a>
</div>
</aside>
<!-- MAIN CONTENT -->
<main class="flex-1 flex flex-col min-w-0">
<!-- Header -->
<header
class="h-16 flex items-center justify-between px-8 border-b border-white/5 bg-[#0b101a]/40 backdrop-blur-md z-20 shrink-0"
>
<div>
<h2 class="text-lg font-bold text-white tracking-tight">Kanban Board</h2>
<p class="text-[10px] text-slate-500 uppercase tracking-[0.2em] font-bold mono">
test case 1
</p>
</div>
<div class="flex items-center gap-5">
<div
class="flex items-center bg-white/5 border border-white/10 rounded-full px-4 py-1.5 gap-3"
>
<i data-lucide="users" class="w-4 h-4 text-slate-500"></i>
<div class="toggle-track">
<div class="toggle-thumb"></div>
</div>
<span class="mono text-xs font-bold text-slate-400">3</span>
</div>
<button
class="flex items-center gap-2 glass px-5 py-2 rounded-xl text-xs font-bold hover:bg-white/10 transition"
>
<i data-lucide="play" class="w-3.5 h-3.5 text-cyan-400 fill-cyan-400"></i> Auto Mode
</button>
<button
class="btn-cyan px-6 py-2 rounded-xl text-xs font-black flex items-center gap-2 shadow-lg shadow-cyan-500/20"
>
<i data-lucide="plus" class="w-4 h-4 stroke-[3.5px]"></i> ADD FEATURE
</button>
</div>
</header>
<!-- Search Bar -->
<div class="px-8 py-4 flex items-center justify-between shrink-0">
<div class="relative flex-1 max-w-2xl group">
<i
data-lucide="search"
class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 group-focus-within:text-cyan-400 transition-colors"
></i>
<input
type="text"
placeholder="Search features by keyword..."
class="w-full bg-white/5 border border-white/10 rounded-2xl py-2.5 pl-12 pr-12 text-sm focus:outline-none focus:border-cyan-500/50 transition-all mono"
/>
<div class="absolute right-4 top-1/2 -translate-y-1/2">
<span class="shortcut-badge">/</span>
</div>
</div>
<div class="flex items-center gap-2 ml-6">
<button class="p-2.5 glass rounded-xl text-slate-500 hover:text-white transition">
<i data-lucide="history" class="w-4.5 h-4.5"></i>
</button>
<button class="p-2.5 glass rounded-xl text-slate-500 hover:text-white transition">
<i data-lucide="trash-2" class="w-4.5 h-4.5"></i>
</button>
<div class="w-px h-6 bg-white/10 mx-1"></div>
<button class="p-2.5 glass rounded-xl text-slate-500 hover:text-white transition">
<i data-lucide="maximize-2" class="w-4.5 h-4.5"></i>
</button>
<button class="p-2.5 glass rounded-xl text-slate-500 hover:text-white transition">
<i data-lucide="layout" class="w-4.5 h-4.5"></i>
</button>
</div>
</div>
<!-- Kanban Grid -->
<div class="flex-1 overflow-x-auto custom-scrollbar px-8 pb-8">
<div class="flex gap-6 h-full min-w-max items-start">
<!-- COLUMN: BACKLOG -->
<div class="w-80 flex flex-col gap-5 h-full">
<div class="flex items-center justify-between px-2 shrink-0">
<div class="flex items-center gap-3">
<span class="status-dot bg-slate-600"></span>
<h3 class="text-[11px] font-black text-slate-400 uppercase tracking-widest">
Backlog
</h3>
<div class="flex items-center gap-1.5 opacity-40">
<i data-lucide="lightbulb" class="w-3.5 h-3.5 text-yellow-500"></i>
<i data-lucide="git-branch" class="w-3.5 h-3.5 text-cyan-400"></i>
<span class="mono text-[9px] text-cyan-400 font-bold">Mabe 6</span>
</div>
</div>
<span
class="mono text-[10px] bg-white/5 px-2.5 py-0.5 rounded-full text-slate-500 border border-white/5"
>47</span
>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-4 pr-2">
<div class="glass kanban-card flex flex-col gap-4 group relative">
<div
class="absolute top-5 right-6 opacity-0 group-hover:opacity-100 transition-opacity"
>
<i
data-lucide="trash-2"
class="w-4 h-4 text-slate-600 hover:text-red-400 cursor-pointer"
></i>
</div>
<p class="text-[13px] text-slate-300 leading-relaxed font-medium line-clamp-3">
Create a bouncing animation using CSS keyframes that simulates elastic motion...
</p>
<div
class="flex items-center gap-1 text-[10px] text-slate-500 -mt-1 cursor-pointer hover:text-slate-300"
>
<i data-lucide="chevron-down" class="w-3 h-3"></i> More
</div>
<div
class="text-[10px] font-bold text-cyan-400/80 mono flex items-center gap-1.5 uppercase tracking-tight"
>
<i data-lucide="layers" class="w-3.5 h-3.5"></i> Opus 4.2
</div>
<div class="flex gap-2">
<button
class="flex-1 glass py-2.5 rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 hover:bg-white/10 transition"
>
<i data-lucide="edit-3" class="w-3.5 h-3.5"></i> Edit
</button>
<button
class="flex-1 bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 border border-cyan-500/20 py-2.5 rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 transition"
>
<i data-lucide="target" class="w-3.5 h-3.5"></i> Make
</button>
</div>
</div>
<div class="glass kanban-card flex flex-col gap-4">
<p class="text-[13px] text-slate-300 leading-relaxed font-medium line-clamp-3">
Implement CSS reset rules and establish base styling for the page including
typography, spacing...
</p>
<div
class="text-[10px] font-bold text-cyan-400/80 mono flex items-center gap-1.5 uppercase"
>
<i data-lucide="layers" class="w-3.5 h-3.5"></i> Opus 4.3
</div>
<div class="flex gap-2">
<button class="flex-1 glass py-2.5 rounded-xl text-[11px] font-bold">Edit</button>
<button
class="flex-1 bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 py-2.5 rounded-xl text-[11px] font-bold"
>
Make
</button>
</div>
</div>
</div>
</div>
<!-- COLUMN: IN PROGRESS -->
<div class="w-80 flex flex-col gap-5 col-in-progress rounded-[2.5rem] p-3 h-full">
<div class="flex items-center justify-between px-2 shrink-0">
<div class="flex items-center gap-3">
<span class="status-dot bg-cyan-400 glow-cyan"></span>
<h3 class="text-[11px] font-black text-slate-200 uppercase tracking-widest">
In Progress
</h3>
</div>
<span
class="mono text-[10px] bg-cyan-500/10 px-2.5 py-0.5 rounded-full text-cyan-400 border border-cyan-500/20"
>3</span
>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-4 pr-1">
<!-- Active Card -->
<div
class="glass kanban-card border-cyan-500/40 bg-cyan-500/[0.08] flex flex-col gap-4"
>
<div class="flex justify-end items-center gap-2">
<div
class="bg-orange-500/15 text-orange-400 text-[9px] px-2.5 py-1 rounded-lg border border-orange-500/20 flex items-center gap-1.5 font-black mono"
>
<i data-lucide="refresh-cw" class="w-3 h-3"></i> Opus 6.5
</div>
<div
class="bg-slate-900/50 text-slate-500 text-[9px] px-2 py-1 rounded-lg border border-white/5 font-mono"
>
00:04
</div>
</div>
<p class="text-[13px] text-white leading-relaxed font-semibold">
Configure the application for deployment to a web hosting platform. Set up
necessary build processes...
</p>
<div class="flex gap-2 mt-2">
<button
class="flex-[4] btn-cyan py-3 rounded-xl text-[11px] font-black flex items-center justify-center gap-2 tracking-widest"
>
<i data-lucide="terminal" class="w-4 h-4 stroke-[2.5px]"></i> LOGS
<span class="bg-black/10 px-1.5 rounded ml-1">8</span>
</button>
<button
class="flex-1 bg-rose-500 hover:bg-rose-600 text-white py-3 rounded-xl flex items-center justify-center transition shadow-lg shadow-rose-500/20"
>
<i data-lucide="square" class="w-4 h-4 fill-current"></i>
</button>
</div>
</div>
<!-- Card 2 -->
<div class="glass kanban-card flex flex-col gap-4">
<div class="flex justify-end gap-2">
<div
class="bg-orange-500/10 text-orange-400 text-[9px] px-2.5 py-1 rounded-lg border border-orange-500/10 flex items-center gap-1.5 font-bold mono"
>
<i data-lucide="refresh-cw" class="w-3 h-3"></i> Opus 4.5
</div>
<div
class="bg-slate-900/50 text-slate-500 text-[9px] px-2 py-1 rounded-lg border border-white/5 font-mono"
>
00:07
</div>
</div>
<p class="text-[13px] text-slate-300 leading-relaxed font-medium">
Create helper functions for selecting and querying DOM elements. Provide reusable
utilities for element...
</p>
<div class="flex gap-2 mt-2">
<button
class="flex-[4] bg-cyan-500/15 text-cyan-400 border border-cyan-500/20 py-3 rounded-xl text-[11px] font-bold flex items-center justify-center gap-2"
>
<i data-lucide="terminal" class="w-4 h-4"></i> LOGS
<span class="bg-cyan-500/10 px-1.5 rounded ml-1">2</span>
</button>
<button
class="flex-1 bg-rose-500/20 text-rose-500/50 border border-rose-500/20 py-3 rounded-xl flex items-center justify-center"
>
<i data-lucide="square" class="w-4 h-4"></i>
</button>
</div>
</div>
</div>
</div>
<!-- COLUMN: WAITING APPROVAL -->
<div class="w-80 flex flex-col gap-5 col-waiting rounded-[2.5rem] p-3 h-full">
<div class="flex items-center justify-between px-2 shrink-0">
<div class="flex items-center gap-3">
<span class="status-dot bg-orange-500 glow-orange"></span>
<h3 class="text-[11px] font-black text-slate-300 uppercase tracking-widest">
Waiting Approval
</h3>
</div>
<span
class="mono text-[10px] bg-white/5 px-2.5 py-0.5 rounded-full text-slate-500 border border-white/5"
>2</span
>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-4 pr-1">
<div class="glass kanban-card flex flex-col gap-4 group">
<div
class="flex justify-end gap-3.5 opacity-30 group-hover:opacity-100 transition-opacity"
>
<i
data-lucide="edit-3"
class="w-4 h-4 cursor-pointer hover:text-white transition"
></i>
<i
data-lucide="copy"
class="w-4 h-4 cursor-pointer hover:text-white transition"
></i>
<i
data-lucide="trash-2"
class="w-4 h-4 cursor-pointer hover:text-rose-400 transition"
></i>
</div>
<p class="text-[13px] text-slate-300 leading-relaxed font-medium italic">
Add descriptive labels and titles for each button animation style to identify them
visually. Help users...
</p>
<div class="flex gap-2.5 mt-2">
<button
class="flex-1 glass border-white/10 py-3 rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 hover:bg-white/10 transition"
>
<i data-lucide="wand-2" class="w-4 h-4"></i> Refine
</button>
<button
class="flex-1 btn-cyan py-3 rounded-xl text-[11px] font-black flex items-center justify-center gap-2 tracking-widest"
>
<i data-lucide="git-commit" class="w-4 h-4 stroke-[2.5px]"></i> COMMIT
</button>
</div>
</div>
</div>
</div>
<!-- COLUMN: VERIFIED -->
<div class="w-80 flex flex-col gap-5 col-verified rounded-[2.5rem] p-3 h-full">
<div class="flex items-center justify-between px-2 shrink-0">
<div class="flex items-center gap-3">
<span class="status-dot bg-emerald-500 glow-green"></span>
<h3 class="text-[11px] font-black text-slate-300 uppercase tracking-widest">
Verified
</h3>
<button
class="ml-2 text-[10px] text-rose-500 flex items-center gap-1 hover:underline font-black transition"
>
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> Delete All
</button>
</div>
<span
class="mono text-[10px] bg-emerald-500/10 px-2.5 py-0.5 rounded-full text-emerald-500 border border-emerald-500/20"
>4</span
>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-4 pr-1">
<div
class="glass kanban-card opacity-60 hover:opacity-100 flex flex-col gap-4 transition-all"
>
<div class="flex justify-end gap-3.5 opacity-20">
<i data-lucide="edit-3" class="w-4 h-4"></i>
<i data-lucide="trash-2" class="w-4 h-4"></i>
</div>
<p
class="text-[13px] text-slate-400 leading-relaxed line-through decoration-slate-600 font-medium"
>
Define foundational button styles with padding, borders, radius, and default
colors. Create...
</p>
<div class="flex gap-2.5 mt-2">
<button
class="px-7 glass border-white/10 py-3 rounded-xl text-[11px] font-bold text-slate-500 hover:text-slate-300 transition"
>
Logs
</button>
<button
class="flex-1 bg-emerald-500/15 text-emerald-400 border border-emerald-500/20 py-3 rounded-xl text-[11px] font-black flex items-center justify-center gap-2 tracking-widest"
>
<i data-lucide="check-circle" class="w-4 h-4 stroke-[2.5px]"></i> COMPLETE
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Floating UI -->
<div class="fixed bottom-8 right-8 flex flex-col gap-4 z-50">
<button
class="w-12 h-12 glass rounded-2xl flex items-center justify-center text-slate-400 hover:text-white transition shadow-2xl"
>
<i data-lucide="history" class="w-5 h-5"></i>
</button>
<button
class="w-14 h-14 bg-cyan-500 rounded-2xl flex items-center justify-center text-slate-950 shadow-2xl shadow-cyan-500/40 hover:scale-110 active:scale-95 transition-all"
>
<i data-lucide="message-square" class="w-7 h-7 stroke-[2.5px]"></i>
</button>
</div>
<script>
// Initialize Lucide Icons
lucide.createIcons();
// Smooth horizontal scroll for the board
const board = document.querySelector('.overflow-x-auto');
board.addEventListener(
'wheel',
(evt) => {
if (evt.deltaY !== 0) {
evt.preventDefault();
board.scrollLeft += evt.deltaY * 1.5;
}
},
{ passive: false }
);
// Keyboard shortcut for search
window.addEventListener('keydown', (e) => {
if (e.key === '/') {
const searchInput = document.querySelector('input[type="text"]');
if (document.activeElement !== searchInput) {
e.preventDefault();
searchInput.focus();
}
}
});
</script>
</body>
</html>

46
package-lock.json generated
View File

@@ -106,6 +106,7 @@
"cmdk": "^1.1.1",
"dagre": "^0.8.5",
"dotenv": "^17.2.3",
"framer-motion": "^12.23.26",
"geist": "^1.5.1",
"lucide-react": "^0.562.0",
"react": "19.2.3",
@@ -113,6 +114,7 @@
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.6",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.9"
@@ -1212,7 +1214,7 @@
},
"node_modules/@electron/node-gyp": {
"version": "10.2.0-electron.1",
"resolved": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
"resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
"integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==",
"dev": true,
"license": "MIT",
@@ -9712,6 +9714,33 @@
"node": ">= 0.6"
}
},
"node_modules/framer-motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
@@ -12744,6 +12773,21 @@
"node": ">= 0.8"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",