mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
feat: Introduce new UI layout with floating dock, visual effects, and expanded theme options.
This commit is contained in:
118
apps/ui/src/components/layout/floating-dock.tsx
Normal file
118
apps/ui/src/components/layout/floating-dock.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
70
apps/ui/src/components/layout/hud.tsx
Normal file
70
apps/ui/src/components/layout/hud.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
apps/ui/src/components/layout/noise-overlay.tsx
Normal file
17
apps/ui/src/components/layout/noise-overlay.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
apps/ui/src/components/layout/page-shell.tsx
Normal file
30
apps/ui/src/components/layout/page-shell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
69
apps/ui/src/components/layout/prism-field.tsx
Normal file
69
apps/ui/src/components/layout/prism-field.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
apps/ui/src/components/layout/shell.tsx
Normal file
32
apps/ui/src/components/layout/shell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user