mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
Compare commits
8 Commits
v0.6.0
...
weird-side
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eb92a0402 | ||
|
|
524a9736b4 | ||
|
|
036a7d9d26 | ||
|
|
c4df2c141a | ||
|
|
7c75c24b5c | ||
|
|
2588ecaafa | ||
|
|
a071097c0d | ||
|
|
b930091c42 |
@@ -63,17 +63,21 @@
|
|||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"@xyflow/react": "^12.10.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"framer-motion": "^12.23.26",
|
||||||
"geist": "^1.5.1",
|
"geist": "^1.5.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"rehype-raw": "^7.0.0",
|
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
@@ -95,6 +99,7 @@
|
|||||||
"@playwright/test": "^1.57.0",
|
"@playwright/test": "^1.57.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/router-plugin": "^1.141.7",
|
"@tanstack/router-plugin": "^1.141.7",
|
||||||
|
"@types/dagre": "^0.7.53",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useSettingsMigration } from './hooks/use-settings-migration';
|
|||||||
import './styles/global.css';
|
import './styles/global.css';
|
||||||
import './styles/theme-imports';
|
import './styles/theme-imports';
|
||||||
|
|
||||||
|
import { Shell } from './components/layout/shell';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [showSplash, setShowSplash] = useState(() => {
|
const [showSplash, setShowSplash] = useState(() => {
|
||||||
// Only show splash once per session
|
// Only show splash once per session
|
||||||
@@ -27,9 +29,9 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Shell>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
{showSplash && <SplashScreen onComplete={handleSplashComplete} />}
|
{showSplash && <SplashScreen onComplete={handleSplashComplete} />}
|
||||||
</>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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,
|
ProjectActions,
|
||||||
SidebarNavigation,
|
SidebarNavigation,
|
||||||
ProjectSelectorWithOptions,
|
ProjectSelectorWithOptions,
|
||||||
SidebarFooter,
|
|
||||||
} from './sidebar/components';
|
} from './sidebar/components';
|
||||||
|
import { Hud } from './hud';
|
||||||
|
import { FloatingDock } from './floating-dock';
|
||||||
import { TrashDialog, OnboardingDialog } from './sidebar/dialogs';
|
import { TrashDialog, OnboardingDialog } from './sidebar/dialogs';
|
||||||
import { SIDEBAR_FEATURE_FLAGS } from './sidebar/constants';
|
import { SIDEBAR_FEATURE_FLAGS } from './sidebar/constants';
|
||||||
import {
|
import {
|
||||||
@@ -247,64 +248,27 @@ export function Sidebar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<>
|
||||||
className={cn(
|
{/* Heads-Up Display (Top Bar) */}
|
||||||
'flex-shrink-0 flex flex-col z-30 relative',
|
<Hud
|
||||||
// Glass morphism background with gradient
|
onOpenProjectPicker={() => setIsProjectPickerOpen(true)}
|
||||||
'bg-gradient-to-b from-sidebar/95 via-sidebar/85 to-sidebar/90 backdrop-blur-2xl',
|
onOpenFolder={handleOpenFolder}
|
||||||
// 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}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
{/* Floating Navigation Dock */}
|
||||||
<SidebarHeader sidebarOpen={sidebarOpen} navigate={navigate} />
|
<FloatingDock />
|
||||||
|
|
||||||
{/* Project Actions - Moved above project selector */}
|
|
||||||
{sidebarOpen && (
|
|
||||||
<ProjectActions
|
|
||||||
setShowNewProjectModal={setShowNewProjectModal}
|
|
||||||
handleOpenFolder={handleOpenFolder}
|
|
||||||
setShowTrashDialog={setShowTrashDialog}
|
|
||||||
trashedProjects={trashedProjects}
|
|
||||||
shortcuts={{ openProject: shortcuts.openProject }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{/* Project Selector Dialog (Hidden logic, controlled by state) */}
|
||||||
|
<div className="hidden">
|
||||||
<ProjectSelectorWithOptions
|
<ProjectSelectorWithOptions
|
||||||
sidebarOpen={sidebarOpen}
|
sidebarOpen={true}
|
||||||
isProjectPickerOpen={isProjectPickerOpen}
|
isProjectPickerOpen={isProjectPickerOpen}
|
||||||
setIsProjectPickerOpen={setIsProjectPickerOpen}
|
setIsProjectPickerOpen={setIsProjectPickerOpen}
|
||||||
setShowDeleteProjectDialog={setShowDeleteProjectDialog}
|
setShowDeleteProjectDialog={setShowDeleteProjectDialog}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SidebarNavigation
|
|
||||||
currentProject={currentProject}
|
|
||||||
sidebarOpen={sidebarOpen}
|
|
||||||
navSections={navSections}
|
|
||||||
isActiveRoute={isActiveRoute}
|
|
||||||
navigate={navigate}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SidebarFooter
|
{/* Dialogs & Modals - Preservation of Logic */}
|
||||||
sidebarOpen={sidebarOpen}
|
|
||||||
isActiveRoute={isActiveRoute}
|
|
||||||
navigate={navigate}
|
|
||||||
hideWiki={hideWiki}
|
|
||||||
hideRunningAgents={hideRunningAgents}
|
|
||||||
runningAgentsCount={runningAgentsCount}
|
|
||||||
shortcuts={{ settings: shortcuts.settings }}
|
|
||||||
/>
|
|
||||||
<TrashDialog
|
<TrashDialog
|
||||||
open={showTrashDialog}
|
open={showTrashDialog}
|
||||||
onOpenChange={setShowTrashDialog}
|
onOpenChange={setShowTrashDialog}
|
||||||
@@ -317,7 +281,6 @@ export function Sidebar() {
|
|||||||
isEmptyingTrash={isEmptyingTrash}
|
isEmptyingTrash={isEmptyingTrash}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* New Project Setup Dialog */}
|
|
||||||
<CreateSpecDialog
|
<CreateSpecDialog
|
||||||
open={showSetupDialog}
|
open={showSetupDialog}
|
||||||
onOpenChange={setShowSetupDialog}
|
onOpenChange={setShowSetupDialog}
|
||||||
@@ -345,7 +308,6 @@ export function Sidebar() {
|
|||||||
onGenerateSpec={handleOnboardingGenerateSpec}
|
onGenerateSpec={handleOnboardingGenerateSpec}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Delete Project Confirmation Dialog */}
|
|
||||||
<DeleteProjectDialog
|
<DeleteProjectDialog
|
||||||
open={showDeleteProjectDialog}
|
open={showDeleteProjectDialog}
|
||||||
onOpenChange={setShowDeleteProjectDialog}
|
onOpenChange={setShowDeleteProjectDialog}
|
||||||
@@ -353,7 +315,6 @@ export function Sidebar() {
|
|||||||
onConfirm={moveProjectToTrash}
|
onConfirm={moveProjectToTrash}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* New Project Modal */}
|
|
||||||
<NewProjectModal
|
<NewProjectModal
|
||||||
open={showNewProjectModal}
|
open={showNewProjectModal}
|
||||||
onOpenChange={setShowNewProjectModal}
|
onOpenChange={setShowNewProjectModal}
|
||||||
@@ -362,6 +323,6 @@ export function Sidebar() {
|
|||||||
onCreateFromCustomUrl={handleCreateFromCustomUrl}
|
onCreateFromCustomUrl={handleCreateFromCustomUrl}
|
||||||
isCreating={isCreatingProject}
|
isCreating={isCreatingProject}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ const badgeVariants = cva(
|
|||||||
// Muted variants for subtle indication
|
// Muted variants for subtle indication
|
||||||
muted: 'border-border/50 bg-muted/50 text-muted-foreground',
|
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',
|
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: {
|
size: {
|
||||||
default: 'px-2.5 py-0.5 text-xs',
|
default: 'px-2.5 py-0.5 text-xs',
|
||||||
|
|||||||
@@ -6,25 +6,32 @@ import { Loader2 } from 'lucide-react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all 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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
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:
|
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',
|
'bg-destructive text-white shadow-sm hover:bg-destructive/90 hover:shadow-md hover:shadow-destructive/25 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||||
outline:
|
outline:
|
||||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
'border 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-xs hover:bg-secondary/80',
|
secondary:
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
'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',
|
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',
|
'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: {
|
size: {
|
||||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
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',
|
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 text-xs',
|
||||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
lg: 'h-11 rounded-md px-8 has-[>svg]:px-5 text-base',
|
||||||
icon: 'size-9',
|
icon: 'size-9',
|
||||||
'icon-sm': 'size-8',
|
'icon-sm': 'size-8',
|
||||||
'icon-lg': 'size-10',
|
'icon-lg': 'size-10',
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ function Card({ className, gradient = false, ...props }: CardProps) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-card text-card-foreground flex flex-col gap-1 rounded-xl border border-white/10 backdrop-blur-md py-6',
|
'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',
|
||||||
// Premium layered shadow
|
// Prism hover effect
|
||||||
'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)]',
|
'hover:-translate-y-1 hover:bg-white/[0.06] hover:border-white/15',
|
||||||
// Gradient border option
|
// Gradient border option
|
||||||
gradient &&
|
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',
|
'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
|
<DialogOverlayPrimitive
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
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=open]:animate-in data-[state=closed]:animate-out',
|
||||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
'duration-200',
|
'duration-300',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -99,15 +99,15 @@ const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
|
|||||||
className={cn(
|
className={cn(
|
||||||
'fixed top-[50%] left-[50%] z-50 translate-x-[-50%] translate-y-[-50%]',
|
'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)]',
|
'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
|
// 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
|
// Animations - smoother with scale
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
'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]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
'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%]',
|
'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',
|
compact ? 'max-w-4xl p-4' : !hasCustomMaxWidth ? 'sm:max-w-2xl p-6' : 'p-6',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -15,17 +15,21 @@ function Input({ className, type, startAddon, endAddon, ...props }: InputProps)
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
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',
|
'file:text-foreground placeholder:text-muted-foreground/50 selection:bg-cyan-500/30 selection:text-cyan-100',
|
||||||
// Inner shadow for depth
|
'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',
|
||||||
'shadow-[inset_0_1px_2px_rgba(0,0,0,0.05)]',
|
'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium',
|
||||||
// Animated focus ring
|
'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
'transition-[color,box-shadow,border-color] duration-200 ease-out',
|
'backdrop-blur-sm',
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
// 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',
|
'aria-invalid:ring-destructive/20 aria-invalid:border-destructive',
|
||||||
// Adjust padding for addons
|
// Adjust padding for addons
|
||||||
startAddon && 'pl-0',
|
startAddon && 'pl-0',
|
||||||
endAddon && 'pr-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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -39,10 +43,10 @@ function Input({ className, type, startAddon, endAddon, ...props }: InputProps)
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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)]',
|
'shadow-[inset_0_1px_2px_rgba(0,0,0,0.05)]',
|
||||||
'transition-[box-shadow,border-color] duration-200 ease-out',
|
'focus-within:bg-input/80 focus-within:border-ring/50',
|
||||||
'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]',
|
'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:disabled]:opacity-50 has-[input:disabled]:cursor-not-allowed',
|
||||||
'has-[input[aria-invalid]]:ring-destructive/20 has-[input[aria-invalid]]:border-destructive'
|
'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)}
|
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderTrackPrimitive className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer">
|
<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-primary" />
|
<SliderRangePrimitive className="slider-range absolute h-full bg-cyan-400" />
|
||||||
</SliderTrackPrimitive>
|
</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>
|
</SliderRootPrimitive>
|
||||||
));
|
));
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
|
|||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
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>
|
</SwitchPrimitives.Root>
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import { RefreshCw } from 'lucide-react';
|
|||||||
import { useAutoMode } from '@/hooks/use-auto-mode';
|
import { useAutoMode } from '@/hooks/use-auto-mode';
|
||||||
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
||||||
import { useWindowState } from '@/hooks/use-window-state';
|
import { useWindowState } from '@/hooks/use-window-state';
|
||||||
|
import { PageShell } from '@/components/layout/page-shell';
|
||||||
// Board-view specific imports
|
// Board-view specific imports
|
||||||
import { BoardHeader } from './board-view/board-header';
|
import { BoardHeader } from './board-view/board-header';
|
||||||
import { BoardSearchBar } from './board-view/board-search-bar';
|
import { BoardSearchBar } from './board-view/board-search-bar';
|
||||||
import { BoardControls } from './board-view/board-controls';
|
import { BoardControls } from './board-view/board-controls';
|
||||||
import { KanbanBoard } from './board-view/kanban-board';
|
import { KanbanBoard } from './board-view/kanban-board';
|
||||||
|
import { GraphView } from './graph-view';
|
||||||
import {
|
import {
|
||||||
AddFeatureDialog,
|
AddFeatureDialog,
|
||||||
AgentOutputModal,
|
AgentOutputModal,
|
||||||
@@ -69,6 +71,8 @@ export function BoardView() {
|
|||||||
aiProfiles,
|
aiProfiles,
|
||||||
kanbanCardDetailLevel,
|
kanbanCardDetailLevel,
|
||||||
setKanbanCardDetailLevel,
|
setKanbanCardDetailLevel,
|
||||||
|
boardViewMode,
|
||||||
|
setBoardViewMode,
|
||||||
specCreatingForProject,
|
specCreatingForProject,
|
||||||
setSpecCreatingForProject,
|
setSpecCreatingForProject,
|
||||||
pendingPlanApproval,
|
pendingPlanApproval,
|
||||||
@@ -989,40 +993,54 @@ export function BoardView() {
|
|||||||
completedCount={completedFeatures.length}
|
completedCount={completedFeatures.length}
|
||||||
kanbanCardDetailLevel={kanbanCardDetailLevel}
|
kanbanCardDetailLevel={kanbanCardDetailLevel}
|
||||||
onDetailLevelChange={setKanbanCardDetailLevel}
|
onDetailLevelChange={setKanbanCardDetailLevel}
|
||||||
|
boardViewMode={boardViewMode}
|
||||||
|
onBoardViewModeChange={setBoardViewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Kanban Columns */}
|
{/* View Content - Kanban or Graph */}
|
||||||
<KanbanBoard
|
{boardViewMode === 'kanban' ? (
|
||||||
sensors={sensors}
|
<KanbanBoard
|
||||||
collisionDetectionStrategy={collisionDetectionStrategy}
|
sensors={sensors}
|
||||||
onDragStart={handleDragStart}
|
collisionDetectionStrategy={collisionDetectionStrategy}
|
||||||
onDragEnd={handleDragEnd}
|
onDragStart={handleDragStart}
|
||||||
activeFeature={activeFeature}
|
onDragEnd={handleDragEnd}
|
||||||
getColumnFeatures={getColumnFeatures}
|
activeFeature={activeFeature}
|
||||||
backgroundImageStyle={backgroundImageStyle}
|
getColumnFeatures={getColumnFeatures}
|
||||||
backgroundSettings={backgroundSettings}
|
backgroundImageStyle={backgroundImageStyle}
|
||||||
onEdit={(feature) => setEditingFeature(feature)}
|
backgroundSettings={backgroundSettings}
|
||||||
onDelete={(featureId) => handleDeleteFeature(featureId)}
|
onEdit={(feature) => setEditingFeature(feature)}
|
||||||
onViewOutput={handleViewOutput}
|
onDelete={(featureId) => handleDeleteFeature(featureId)}
|
||||||
onVerify={handleVerifyFeature}
|
onViewOutput={handleViewOutput}
|
||||||
onResume={handleResumeFeature}
|
onVerify={handleVerifyFeature}
|
||||||
onForceStop={handleForceStopFeature}
|
onResume={handleResumeFeature}
|
||||||
onManualVerify={handleManualVerify}
|
onForceStop={handleForceStopFeature}
|
||||||
onMoveBackToInProgress={handleMoveBackToInProgress}
|
onManualVerify={handleManualVerify}
|
||||||
onFollowUp={handleOpenFollowUp}
|
onMoveBackToInProgress={handleMoveBackToInProgress}
|
||||||
onCommit={handleCommitFeature}
|
onFollowUp={handleOpenFollowUp}
|
||||||
onComplete={handleCompleteFeature}
|
onCommit={handleCommitFeature}
|
||||||
onImplement={handleStartImplementation}
|
onComplete={handleCompleteFeature}
|
||||||
onViewPlan={(feature) => setViewPlanFeature(feature)}
|
onImplement={handleStartImplementation}
|
||||||
onApprovePlan={handleOpenApprovalDialog}
|
onViewPlan={(feature) => setViewPlanFeature(feature)}
|
||||||
featuresWithContext={featuresWithContext}
|
onApprovePlan={handleOpenApprovalDialog}
|
||||||
runningAutoTasks={runningAutoTasks}
|
featuresWithContext={featuresWithContext}
|
||||||
shortcuts={shortcuts}
|
runningAutoTasks={runningAutoTasks}
|
||||||
onStartNextFeatures={handleStartNextFeatures}
|
shortcuts={shortcuts}
|
||||||
onShowSuggestions={() => setShowSuggestionsDialog(true)}
|
onStartNextFeatures={handleStartNextFeatures}
|
||||||
suggestionsCount={suggestionsCount}
|
onShowSuggestions={() => setShowSuggestionsDialog(true)}
|
||||||
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
suggestionsCount={suggestionsCount}
|
||||||
/>
|
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GraphView
|
||||||
|
features={hookFeatures}
|
||||||
|
runningAutoTasks={runningAutoTasks}
|
||||||
|
currentWorktreePath={currentWorktreePath}
|
||||||
|
currentWorktreeBranch={currentWorktreeBranch}
|
||||||
|
projectPath={currentProject?.path || null}
|
||||||
|
onEditFeature={(feature) => setEditingFeature(feature)}
|
||||||
|
onViewOutput={handleViewOutput}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Board Background Modal */}
|
{/* Board Background Modal */}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { ImageIcon, Archive, Minimize2, Square, Maximize2 } from 'lucide-react';
|
import { ImageIcon, Archive, Minimize2, Square, Maximize2, Columns3, Network } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { BoardViewMode } from '@/store/app-store';
|
||||||
|
|
||||||
interface BoardControlsProps {
|
interface BoardControlsProps {
|
||||||
isMounted: boolean;
|
isMounted: boolean;
|
||||||
@@ -10,6 +11,8 @@ interface BoardControlsProps {
|
|||||||
completedCount: number;
|
completedCount: number;
|
||||||
kanbanCardDetailLevel: 'minimal' | 'standard' | 'detailed';
|
kanbanCardDetailLevel: 'minimal' | 'standard' | 'detailed';
|
||||||
onDetailLevelChange: (level: 'minimal' | 'standard' | 'detailed') => void;
|
onDetailLevelChange: (level: 'minimal' | 'standard' | 'detailed') => void;
|
||||||
|
boardViewMode: BoardViewMode;
|
||||||
|
onBoardViewModeChange: (mode: BoardViewMode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BoardControls({
|
export function BoardControls({
|
||||||
@@ -19,12 +22,59 @@ export function BoardControls({
|
|||||||
completedCount,
|
completedCount,
|
||||||
kanbanCardDetailLevel,
|
kanbanCardDetailLevel,
|
||||||
onDetailLevelChange,
|
onDetailLevelChange,
|
||||||
|
boardViewMode,
|
||||||
|
onBoardViewModeChange,
|
||||||
}: BoardControlsProps) {
|
}: BoardControlsProps) {
|
||||||
if (!isMounted) return null;
|
if (!isMounted) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<div className="flex items-center gap-2 ml-4">
|
||||||
|
{/* View Mode Toggle - Kanban / Graph */}
|
||||||
|
<div
|
||||||
|
className="flex items-center rounded-lg bg-secondary border border-border"
|
||||||
|
data-testid="view-mode-toggle"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={() => onBoardViewModeChange('kanban')}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-l-lg transition-colors',
|
||||||
|
boardViewMode === 'kanban'
|
||||||
|
? 'bg-brand-500/20 text-brand-500'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||||
|
)}
|
||||||
|
data-testid="view-mode-kanban"
|
||||||
|
>
|
||||||
|
<Columns3 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Kanban Board View</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={() => onBoardViewModeChange('graph')}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-r-lg transition-colors',
|
||||||
|
boardViewMode === 'graph'
|
||||||
|
? 'bg-brand-500/20 text-brand-500'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||||
|
)}
|
||||||
|
data-testid="view-mode-graph"
|
||||||
|
>
|
||||||
|
<Network className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Dependency Graph View</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Board Background Button */}
|
{/* Board Background Button */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -39,23 +40,20 @@ export function BoardHeader({
|
|||||||
const showUsageTracking = !apiKeys.anthropic && !isWindows;
|
const showUsageTracking = !apiKeys.anthropic && !isWindows;
|
||||||
|
|
||||||
return (
|
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>
|
<div>
|
||||||
<h1 className="text-xl font-bold">Kanban Board</h1>
|
<h2 className="text-lg font-bold text-white tracking-tight">Kanban Board</h2>
|
||||||
<p className="text-sm text-muted-foreground">{projectName}</p>
|
<p className="text-[10px] text-slate-500 uppercase tracking-[0.2em] font-bold mono">
|
||||||
|
{projectName}
|
||||||
|
</p>
|
||||||
</div>
|
</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 && (
|
{isMounted && (
|
||||||
<div
|
<div className="flex items-center bg-white/5 border border-white/10 rounded-full px-4 py-1.5 gap-3">
|
||||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border"
|
<Bot className="w-4 h-4 text-slate-500" />
|
||||||
data-testid="concurrency-slider-container"
|
{/* We keep the slider for functionality, but could style it to look like the toggle or just use the slider cleanly */}
|
||||||
>
|
|
||||||
<Bot className="w-4 h-4 text-muted-foreground" />
|
|
||||||
<span className="text-sm font-medium">Agents</span>
|
|
||||||
<Slider
|
<Slider
|
||||||
value={[maxConcurrency]}
|
value={[maxConcurrency]}
|
||||||
onValueChange={(value) => onConcurrencyChange(value[0])}
|
onValueChange={(value) => onConcurrencyChange(value[0])}
|
||||||
@@ -63,43 +61,43 @@ export function BoardHeader({
|
|||||||
max={10}
|
max={10}
|
||||||
step={1}
|
step={1}
|
||||||
className="w-20"
|
className="w-20"
|
||||||
data-testid="concurrency-slider"
|
|
||||||
/>
|
/>
|
||||||
<span
|
<span className="mono text-xs font-bold text-slate-400">
|
||||||
className="text-sm text-muted-foreground min-w-[5ch] text-center"
|
|
||||||
data-testid="concurrency-value"
|
|
||||||
>
|
|
||||||
{runningAgentsCount} / {maxConcurrency}
|
{runningAgentsCount} / {maxConcurrency}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
|
{/* Auto Mode Button */}
|
||||||
{isMounted && (
|
{isMounted && (
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border">
|
<button
|
||||||
<Label htmlFor="auto-mode-toggle" className="text-sm font-medium cursor-pointer">
|
onClick={() => onAutoModeToggle(!isAutoModeRunning)}
|
||||||
Auto Mode
|
className={cn(
|
||||||
</Label>
|
'flex items-center gap-2 px-5 py-2 rounded-xl text-xs font-bold transition',
|
||||||
<Switch
|
isAutoModeRunning
|
||||||
id="auto-mode-toggle"
|
? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/20'
|
||||||
checked={isAutoModeRunning}
|
: 'glass hover:bg-white/10'
|
||||||
onCheckedChange={onAutoModeToggle}
|
)}
|
||||||
data-testid="auto-mode-toggle"
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
isAutoModeRunning ? 'bg-cyan-400 animate-pulse' : 'bg-slate-500'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
Auto Mode
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HotkeyButton
|
{/* Add Feature Button */}
|
||||||
size="sm"
|
<button
|
||||||
onClick={onAddFeature}
|
onClick={onAddFeature}
|
||||||
hotkey={addFeatureShortcut}
|
className="btn-cyan px-6 py-2 rounded-xl text-xs font-black flex items-center gap-2 shadow-lg shadow-cyan-500/20"
|
||||||
hotkeyActive={false}
|
|
||||||
data-testid="add-feature-button"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 stroke-[3.5px]" />
|
||||||
Add Feature
|
ADD FEATURE
|
||||||
</HotkeyButton>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface KanbanColumnProps {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
colorClass: string;
|
colorClass: string;
|
||||||
|
columnClass?: string;
|
||||||
count: number;
|
count: number;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
headerAction?: ReactNode;
|
headerAction?: ReactNode;
|
||||||
@@ -21,6 +22,7 @@ export const KanbanColumn = memo(function KanbanColumn({
|
|||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
colorClass,
|
colorClass,
|
||||||
|
columnClass,
|
||||||
count,
|
count,
|
||||||
children,
|
children,
|
||||||
headerAction,
|
headerAction,
|
||||||
@@ -43,7 +45,8 @@ export const KanbanColumn = memo(function KanbanColumn({
|
|||||||
'transition-[box-shadow,ring] duration-200',
|
'transition-[box-shadow,ring] duration-200',
|
||||||
!width && 'w-72', // Only apply w-72 if no custom width
|
!width && 'w-72', // Only apply w-72 if no custom width
|
||||||
showBorder && 'border border-border/60',
|
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}
|
style={widthStyle}
|
||||||
data-testid={`kanban-column-${id}`}
|
data-testid={`kanban-column-${id}`}
|
||||||
|
|||||||
@@ -2,21 +2,25 @@ import { Feature } from '@/store/app-store';
|
|||||||
|
|
||||||
export type ColumnId = Feature['status'];
|
export type ColumnId = Feature['status'];
|
||||||
|
|
||||||
export const COLUMNS: { id: ColumnId; title: string; colorClass: string }[] = [
|
export const COLUMNS: { id: ColumnId; title: string; colorClass: string; columnClass?: string }[] =
|
||||||
{ id: 'backlog', title: 'Backlog', colorClass: 'bg-[var(--status-backlog)]' },
|
[
|
||||||
{
|
{ id: 'backlog', title: 'Backlog', colorClass: 'bg-white/20', columnClass: '' },
|
||||||
id: 'in_progress',
|
{
|
||||||
title: 'In Progress',
|
id: 'in_progress',
|
||||||
colorClass: 'bg-[var(--status-in-progress)]',
|
title: 'In Progress',
|
||||||
},
|
colorClass: 'bg-cyan-400',
|
||||||
{
|
columnClass: 'col-in-progress',
|
||||||
id: 'waiting_approval',
|
},
|
||||||
title: 'Waiting Approval',
|
{
|
||||||
colorClass: 'bg-[var(--status-waiting)]',
|
id: 'waiting_approval',
|
||||||
},
|
title: 'Waiting Approval',
|
||||||
{
|
colorClass: 'bg-amber-500',
|
||||||
id: 'verified',
|
columnClass: 'col-waiting',
|
||||||
title: 'Verified',
|
},
|
||||||
colorClass: 'bg-[var(--status-success)]',
|
{
|
||||||
},
|
id: 'verified',
|
||||||
];
|
title: 'Verified',
|
||||||
|
colorClass: 'bg-emerald-500',
|
||||||
|
columnClass: 'col-verified',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export function KanbanBoard({
|
|||||||
id={column.id}
|
id={column.id}
|
||||||
title={column.title}
|
title={column.title}
|
||||||
colorClass={column.colorClass}
|
colorClass={column.colorClass}
|
||||||
|
columnClass={column.columnClass}
|
||||||
count={columnFeatures.length}
|
count={columnFeatures.length}
|
||||||
width={columnWidth}
|
width={columnWidth}
|
||||||
opacity={backgroundSettings.columnOpacity}
|
opacity={backgroundSettings.columnOpacity}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { BaseEdge, getBezierPath, EdgeLabelRenderer } from '@xyflow/react';
|
||||||
|
import type { EdgeProps } from '@xyflow/react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Feature } from '@/store/app-store';
|
||||||
|
|
||||||
|
export interface DependencyEdgeData {
|
||||||
|
sourceStatus: Feature['status'];
|
||||||
|
targetStatus: Feature['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEdgeColor = (sourceStatus?: Feature['status'], targetStatus?: Feature['status']) => {
|
||||||
|
// If source is completed/verified, the dependency is satisfied
|
||||||
|
if (sourceStatus === 'completed' || sourceStatus === 'verified') {
|
||||||
|
return 'var(--status-success)';
|
||||||
|
}
|
||||||
|
// If target is in progress, show active color
|
||||||
|
if (targetStatus === 'in_progress') {
|
||||||
|
return 'var(--status-in-progress)';
|
||||||
|
}
|
||||||
|
// If target is blocked (in backlog with incomplete deps)
|
||||||
|
if (targetStatus === 'backlog') {
|
||||||
|
return 'var(--border)';
|
||||||
|
}
|
||||||
|
// Default
|
||||||
|
return 'var(--border)';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DependencyEdge = memo(function DependencyEdge(props: EdgeProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
data,
|
||||||
|
selected,
|
||||||
|
animated,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const edgeData = data as DependencyEdgeData | undefined;
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
curvature: 0.25,
|
||||||
|
});
|
||||||
|
|
||||||
|
const edgeColor = edgeData
|
||||||
|
? getEdgeColor(edgeData.sourceStatus, edgeData.targetStatus)
|
||||||
|
: 'var(--border)';
|
||||||
|
|
||||||
|
const isCompleted = edgeData?.sourceStatus === 'completed' || edgeData?.sourceStatus === 'verified';
|
||||||
|
const isInProgress = edgeData?.targetStatus === 'in_progress';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Background edge for better visibility */}
|
||||||
|
<BaseEdge
|
||||||
|
id={`${id}-bg`}
|
||||||
|
path={edgePath}
|
||||||
|
style={{
|
||||||
|
strokeWidth: 4,
|
||||||
|
stroke: 'var(--background)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main edge */}
|
||||||
|
<BaseEdge
|
||||||
|
id={id}
|
||||||
|
path={edgePath}
|
||||||
|
className={cn(
|
||||||
|
'transition-all duration-300',
|
||||||
|
animated && 'animated-edge',
|
||||||
|
isInProgress && 'edge-flowing'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
strokeWidth: selected ? 3 : 2,
|
||||||
|
stroke: edgeColor,
|
||||||
|
strokeDasharray: isCompleted ? 'none' : '5 5',
|
||||||
|
filter: selected ? 'drop-shadow(0 0 3px var(--brand-500))' : 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Animated particles for in-progress edges */}
|
||||||
|
{animated && (
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
className="edge-particle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
isInProgress
|
||||||
|
? 'bg-[var(--status-in-progress)] animate-ping'
|
||||||
|
: 'bg-brand-500 animate-pulse'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { useReactFlow, Panel } from '@xyflow/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import {
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Maximize2,
|
||||||
|
Lock,
|
||||||
|
Unlock,
|
||||||
|
GitBranch,
|
||||||
|
ArrowRight,
|
||||||
|
ArrowDown,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface GraphControlsProps {
|
||||||
|
isLocked: boolean;
|
||||||
|
onToggleLock: () => void;
|
||||||
|
onRunLayout: (direction: 'LR' | 'TB') => void;
|
||||||
|
layoutDirection: 'LR' | 'TB';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GraphControls({
|
||||||
|
isLocked,
|
||||||
|
onToggleLock,
|
||||||
|
onRunLayout,
|
||||||
|
layoutDirection,
|
||||||
|
}: GraphControlsProps) {
|
||||||
|
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel position="bottom-left" className="flex flex-col gap-2">
|
||||||
|
<TooltipProvider delayDuration={200}>
|
||||||
|
<div className="flex flex-col gap-1 p-1.5 rounded-lg bg-popover/90 backdrop-blur-sm border border-border shadow-lg">
|
||||||
|
{/* Zoom controls */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => zoomIn({ duration: 200 })}
|
||||||
|
>
|
||||||
|
<ZoomIn className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">Zoom In</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => zoomOut({ duration: 200 })}
|
||||||
|
>
|
||||||
|
<ZoomOut className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">Zoom Out</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => fitView({ padding: 0.2, duration: 300 })}
|
||||||
|
>
|
||||||
|
<Maximize2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">Fit View</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className="h-px bg-border my-1" />
|
||||||
|
|
||||||
|
{/* Layout controls */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 p-0',
|
||||||
|
layoutDirection === 'LR' && 'bg-brand-500/20 text-brand-500'
|
||||||
|
)}
|
||||||
|
onClick={() => onRunLayout('LR')}
|
||||||
|
>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">Horizontal Layout</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 p-0',
|
||||||
|
layoutDirection === 'TB' && 'bg-brand-500/20 text-brand-500'
|
||||||
|
)}
|
||||||
|
onClick={() => onRunLayout('TB')}
|
||||||
|
>
|
||||||
|
<ArrowDown className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">Vertical Layout</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className="h-px bg-border my-1" />
|
||||||
|
|
||||||
|
{/* Lock toggle */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 p-0',
|
||||||
|
isLocked && 'bg-brand-500/20 text-brand-500'
|
||||||
|
)}
|
||||||
|
onClick={onToggleLock}
|
||||||
|
>
|
||||||
|
{isLocked ? (
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Unlock className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
{isLocked ? 'Unlock Nodes' : 'Lock Nodes'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Panel } from '@xyflow/react';
|
||||||
|
import {
|
||||||
|
Clock,
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
CheckCircle2,
|
||||||
|
Lock,
|
||||||
|
AlertCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const legendItems = [
|
||||||
|
{
|
||||||
|
icon: Clock,
|
||||||
|
label: 'Backlog',
|
||||||
|
colorClass: 'text-muted-foreground',
|
||||||
|
bgClass: 'bg-muted/50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Play,
|
||||||
|
label: 'In Progress',
|
||||||
|
colorClass: 'text-[var(--status-in-progress)]',
|
||||||
|
bgClass: 'bg-[var(--status-in-progress)]/20',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Pause,
|
||||||
|
label: 'Waiting',
|
||||||
|
colorClass: 'text-[var(--status-waiting)]',
|
||||||
|
bgClass: 'bg-[var(--status-warning)]/20',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: CheckCircle2,
|
||||||
|
label: 'Verified',
|
||||||
|
colorClass: 'text-[var(--status-success)]',
|
||||||
|
bgClass: 'bg-[var(--status-success)]/20',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Lock,
|
||||||
|
label: 'Blocked',
|
||||||
|
colorClass: 'text-orange-500',
|
||||||
|
bgClass: 'bg-orange-500/20',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: AlertCircle,
|
||||||
|
label: 'Error',
|
||||||
|
colorClass: 'text-[var(--status-error)]',
|
||||||
|
bgClass: 'bg-[var(--status-error)]/20',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function GraphLegend() {
|
||||||
|
return (
|
||||||
|
<Panel position="bottom-right" className="pointer-events-none">
|
||||||
|
<div className="flex flex-wrap gap-3 p-2 rounded-lg bg-popover/90 backdrop-blur-sm border border-border shadow-lg pointer-events-auto">
|
||||||
|
{legendItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<div key={item.label} className="flex items-center gap-1.5">
|
||||||
|
<div className={cn('p-1 rounded', item.bgClass)}>
|
||||||
|
<Icon className={cn('w-3 h-3', item.colorClass)} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { TaskNode } from './task-node';
|
||||||
|
export { DependencyEdge } from './dependency-edge';
|
||||||
|
export { GraphControls } from './graph-controls';
|
||||||
|
export { GraphLegend } from './graph-legend';
|
||||||
248
apps/ui/src/components/views/graph-view/components/task-node.tsx
Normal file
248
apps/ui/src/components/views/graph-view/components/task-node.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import type { NodeProps } from '@xyflow/react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
Lock,
|
||||||
|
CheckCircle2,
|
||||||
|
Clock,
|
||||||
|
AlertCircle,
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
Eye,
|
||||||
|
MoreHorizontal,
|
||||||
|
GitBranch,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { TaskNodeData } from '../hooks/use-graph-nodes';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
|
type TaskNodeProps = NodeProps & {
|
||||||
|
data: TaskNodeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
backlog: {
|
||||||
|
icon: Clock,
|
||||||
|
label: 'Backlog',
|
||||||
|
colorClass: 'text-muted-foreground',
|
||||||
|
borderClass: 'border-border',
|
||||||
|
bgClass: 'bg-card',
|
||||||
|
},
|
||||||
|
in_progress: {
|
||||||
|
icon: Play,
|
||||||
|
label: 'In Progress',
|
||||||
|
colorClass: 'text-[var(--status-in-progress)]',
|
||||||
|
borderClass: 'border-[var(--status-in-progress)]',
|
||||||
|
bgClass: 'bg-[var(--status-in-progress-bg)]',
|
||||||
|
},
|
||||||
|
waiting_approval: {
|
||||||
|
icon: Pause,
|
||||||
|
label: 'Waiting Approval',
|
||||||
|
colorClass: 'text-[var(--status-waiting)]',
|
||||||
|
borderClass: 'border-[var(--status-waiting)]',
|
||||||
|
bgClass: 'bg-[var(--status-warning-bg)]',
|
||||||
|
},
|
||||||
|
verified: {
|
||||||
|
icon: CheckCircle2,
|
||||||
|
label: 'Verified',
|
||||||
|
colorClass: 'text-[var(--status-success)]',
|
||||||
|
borderClass: 'border-[var(--status-success)]',
|
||||||
|
bgClass: 'bg-[var(--status-success-bg)]',
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
icon: CheckCircle2,
|
||||||
|
label: 'Completed',
|
||||||
|
colorClass: 'text-[var(--status-success)]',
|
||||||
|
borderClass: 'border-[var(--status-success)]/50',
|
||||||
|
bgClass: 'bg-[var(--status-success-bg)]/50',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityConfig = {
|
||||||
|
1: { label: 'High', colorClass: 'bg-[var(--status-error)] text-white' },
|
||||||
|
2: { label: 'Medium', colorClass: 'bg-[var(--status-warning)] text-black' },
|
||||||
|
3: { label: 'Low', colorClass: 'bg-[var(--status-info)] text-white' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TaskNode = memo(function TaskNode({
|
||||||
|
data,
|
||||||
|
selected,
|
||||||
|
}: TaskNodeProps) {
|
||||||
|
const config = statusConfig[data.status] || statusConfig.backlog;
|
||||||
|
const StatusIcon = config.icon;
|
||||||
|
const priorityConf = data.priority ? priorityConfig[data.priority as 1 | 2 | 3] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Target handle (left side - receives dependencies) */}
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
className={cn(
|
||||||
|
'w-3 h-3 !bg-border border-2 border-background',
|
||||||
|
'transition-colors duration-200',
|
||||||
|
'hover:!bg-brand-500'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-w-[240px] max-w-[280px] rounded-xl border-2 bg-card shadow-md',
|
||||||
|
'transition-all duration-200',
|
||||||
|
config.borderClass,
|
||||||
|
selected && 'ring-2 ring-brand-500 ring-offset-2 ring-offset-background',
|
||||||
|
data.isRunning && 'animate-pulse-subtle',
|
||||||
|
data.error && 'border-[var(--status-error)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header with status and actions */}
|
||||||
|
<div className={cn(
|
||||||
|
'flex items-center justify-between px-3 py-2 rounded-t-[10px]',
|
||||||
|
config.bgClass
|
||||||
|
)}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StatusIcon className={cn('w-4 h-4', config.colorClass)} />
|
||||||
|
<span className={cn('text-xs font-medium', config.colorClass)}>
|
||||||
|
{config.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{/* Priority badge */}
|
||||||
|
{priorityConf && (
|
||||||
|
<span className={cn(
|
||||||
|
'text-[10px] font-bold px-1.5 py-0.5 rounded',
|
||||||
|
priorityConf.colorClass
|
||||||
|
)}>
|
||||||
|
{data.priority === 1 ? 'H' : data.priority === 2 ? 'M' : 'L'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Blocked indicator */}
|
||||||
|
{data.isBlocked && !data.error && data.status === 'backlog' && (
|
||||||
|
<TooltipProvider delayDuration={200}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="p-1 rounded bg-orange-500/20">
|
||||||
|
<Lock className="w-3 h-3 text-orange-500" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="text-xs max-w-[200px]">
|
||||||
|
<p>Blocked by {data.blockingDependencies.length} dependencies</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error indicator */}
|
||||||
|
{data.error && (
|
||||||
|
<TooltipProvider delayDuration={200}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="p-1 rounded bg-[var(--status-error-bg)]">
|
||||||
|
<AlertCircle className="w-3 h-3 text-[var(--status-error)]" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="text-xs max-w-[250px]">
|
||||||
|
<p>{data.error}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions dropdown */}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-background/50"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-40">
|
||||||
|
<DropdownMenuItem className="text-xs">
|
||||||
|
<Eye className="w-3 h-3 mr-2" />
|
||||||
|
View Details
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{data.status === 'backlog' && !data.isBlocked && (
|
||||||
|
<DropdownMenuItem className="text-xs">
|
||||||
|
<Play className="w-3 h-3 mr-2" />
|
||||||
|
Start Task
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{data.isRunning && (
|
||||||
|
<DropdownMenuItem className="text-xs text-[var(--status-error)]">
|
||||||
|
<Pause className="w-3 h-3 mr-2" />
|
||||||
|
Stop Task
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem className="text-xs">
|
||||||
|
<GitBranch className="w-3 h-3 mr-2" />
|
||||||
|
View Branch
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="px-3 py-2">
|
||||||
|
{/* Category */}
|
||||||
|
<span className="text-[10px] text-muted-foreground font-medium uppercase tracking-wide">
|
||||||
|
{data.category}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h3 className="text-sm font-medium mt-1 line-clamp-2 text-foreground">
|
||||||
|
{data.description}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Progress indicator for in-progress tasks */}
|
||||||
|
{data.isRunning && (
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-[var(--status-in-progress)] rounded-full animate-progress-indeterminate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground">Running...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Branch name if assigned */}
|
||||||
|
{data.branchName && (
|
||||||
|
<div className="mt-2 flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||||
|
<GitBranch className="w-3 h-3" />
|
||||||
|
<span className="truncate">{data.branchName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Source handle (right side - provides to dependents) */}
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
className={cn(
|
||||||
|
'w-3 h-3 !bg-border border-2 border-background',
|
||||||
|
'transition-colors duration-200',
|
||||||
|
'hover:!bg-brand-500',
|
||||||
|
data.status === 'completed' || data.status === 'verified'
|
||||||
|
? '!bg-[var(--status-success)]'
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
174
apps/ui/src/components/views/graph-view/graph-canvas.tsx
Normal file
174
apps/ui/src/components/views/graph-view/graph-canvas.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
MiniMap,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
ReactFlowProvider,
|
||||||
|
SelectionMode,
|
||||||
|
ConnectionMode,
|
||||||
|
Node,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import '@xyflow/react/dist/style.css';
|
||||||
|
|
||||||
|
import { Feature } from '@/store/app-store';
|
||||||
|
import { TaskNode, DependencyEdge, GraphControls, GraphLegend } from './components';
|
||||||
|
import { useGraphNodes, useGraphLayout, type TaskNodeData } from './hooks';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
// Define custom node and edge types - using any to avoid React Flow's strict typing
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const nodeTypes: any = {
|
||||||
|
task: TaskNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const edgeTypes: any = {
|
||||||
|
dependency: DependencyEdge,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GraphCanvasProps {
|
||||||
|
features: Feature[];
|
||||||
|
runningAutoTasks: string[];
|
||||||
|
onNodeClick?: (featureId: string) => void;
|
||||||
|
onNodeDoubleClick?: (featureId: string) => void;
|
||||||
|
backgroundStyle?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GraphCanvasInner({
|
||||||
|
features,
|
||||||
|
runningAutoTasks,
|
||||||
|
onNodeClick,
|
||||||
|
onNodeDoubleClick,
|
||||||
|
backgroundStyle,
|
||||||
|
className,
|
||||||
|
}: GraphCanvasProps) {
|
||||||
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
|
const [layoutDirection, setLayoutDirection] = useState<'LR' | 'TB'>('LR');
|
||||||
|
|
||||||
|
// Transform features to nodes and edges
|
||||||
|
const { nodes: initialNodes, edges: initialEdges } = useGraphNodes({
|
||||||
|
features,
|
||||||
|
runningAutoTasks,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply layout
|
||||||
|
const { layoutedNodes, layoutedEdges, runLayout } = useGraphLayout({
|
||||||
|
nodes: initialNodes,
|
||||||
|
edges: initialEdges,
|
||||||
|
});
|
||||||
|
|
||||||
|
// React Flow state
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);
|
||||||
|
|
||||||
|
// Update nodes/edges when features change
|
||||||
|
useEffect(() => {
|
||||||
|
setNodes(layoutedNodes);
|
||||||
|
setEdges(layoutedEdges);
|
||||||
|
}, [layoutedNodes, layoutedEdges, setNodes, setEdges]);
|
||||||
|
|
||||||
|
// Handle layout direction change
|
||||||
|
const handleRunLayout = useCallback(
|
||||||
|
(direction: 'LR' | 'TB') => {
|
||||||
|
setLayoutDirection(direction);
|
||||||
|
runLayout(direction);
|
||||||
|
},
|
||||||
|
[runLayout]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle node click
|
||||||
|
const handleNodeClick = useCallback(
|
||||||
|
(_event: React.MouseEvent, node: Node<TaskNodeData>) => {
|
||||||
|
onNodeClick?.(node.id);
|
||||||
|
},
|
||||||
|
[onNodeClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle node double click
|
||||||
|
const handleNodeDoubleClick = useCallback(
|
||||||
|
(_event: React.MouseEvent, node: Node<TaskNodeData>) => {
|
||||||
|
onNodeDoubleClick?.(node.id);
|
||||||
|
},
|
||||||
|
[onNodeDoubleClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
// MiniMap node color based on status
|
||||||
|
const minimapNodeColor = useCallback((node: Node<TaskNodeData>) => {
|
||||||
|
const data = node.data as TaskNodeData | undefined;
|
||||||
|
const status = data?.status;
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
case 'verified':
|
||||||
|
return 'var(--status-success)';
|
||||||
|
case 'in_progress':
|
||||||
|
return 'var(--status-in-progress)';
|
||||||
|
case 'waiting_approval':
|
||||||
|
return 'var(--status-waiting)';
|
||||||
|
default:
|
||||||
|
if (data?.isBlocked) return 'rgb(249, 115, 22)'; // orange-500
|
||||||
|
if (data?.error) return 'var(--status-error)';
|
||||||
|
return 'var(--muted-foreground)';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('w-full h-full', className)} style={backgroundStyle}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={isLocked ? undefined : onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onNodeClick={handleNodeClick}
|
||||||
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
fitView
|
||||||
|
fitViewOptions={{ padding: 0.2 }}
|
||||||
|
minZoom={0.1}
|
||||||
|
maxZoom={2}
|
||||||
|
selectionMode={SelectionMode.Partial}
|
||||||
|
connectionMode={ConnectionMode.Loose}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
className="graph-canvas"
|
||||||
|
>
|
||||||
|
<Background
|
||||||
|
variant={BackgroundVariant.Dots}
|
||||||
|
gap={20}
|
||||||
|
size={1}
|
||||||
|
color="var(--border)"
|
||||||
|
className="opacity-50"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={minimapNodeColor}
|
||||||
|
nodeStrokeWidth={3}
|
||||||
|
zoomable
|
||||||
|
pannable
|
||||||
|
className="!bg-popover/90 !border-border rounded-lg shadow-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GraphControls
|
||||||
|
isLocked={isLocked}
|
||||||
|
onToggleLock={() => setIsLocked(!isLocked)}
|
||||||
|
onRunLayout={handleRunLayout}
|
||||||
|
layoutDirection={layoutDirection}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GraphLegend />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with provider for hooks to work
|
||||||
|
export function GraphCanvas(props: GraphCanvasProps) {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<GraphCanvasInner {...props} />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
apps/ui/src/components/views/graph-view/graph-view.tsx
Normal file
89
apps/ui/src/components/views/graph-view/graph-view.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { useMemo, useCallback } from 'react';
|
||||||
|
import { Feature, useAppStore } from '@/store/app-store';
|
||||||
|
import { GraphCanvas } from './graph-canvas';
|
||||||
|
import { useBoardBackground } from '../board-view/hooks';
|
||||||
|
|
||||||
|
interface GraphViewProps {
|
||||||
|
features: Feature[];
|
||||||
|
runningAutoTasks: string[];
|
||||||
|
currentWorktreePath: string | null;
|
||||||
|
currentWorktreeBranch: string | null;
|
||||||
|
projectPath: string | null;
|
||||||
|
onEditFeature: (feature: Feature) => void;
|
||||||
|
onViewOutput: (feature: Feature) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GraphView({
|
||||||
|
features,
|
||||||
|
runningAutoTasks,
|
||||||
|
currentWorktreePath,
|
||||||
|
currentWorktreeBranch,
|
||||||
|
projectPath,
|
||||||
|
onEditFeature,
|
||||||
|
onViewOutput,
|
||||||
|
}: GraphViewProps) {
|
||||||
|
const { currentProject } = useAppStore();
|
||||||
|
|
||||||
|
// Use the same background hook as the board view
|
||||||
|
const { backgroundImageStyle } = useBoardBackground({ currentProject });
|
||||||
|
|
||||||
|
// Filter features by current worktree (same logic as board view)
|
||||||
|
const filteredFeatures = useMemo(() => {
|
||||||
|
const effectiveBranch = currentWorktreeBranch;
|
||||||
|
|
||||||
|
return features.filter((f) => {
|
||||||
|
// Skip completed features (they're in archive)
|
||||||
|
if (f.status === 'completed') return false;
|
||||||
|
|
||||||
|
const featureBranch = f.branchName;
|
||||||
|
|
||||||
|
if (!featureBranch) {
|
||||||
|
// No branch assigned - show only on primary worktree
|
||||||
|
return currentWorktreePath === null;
|
||||||
|
} else if (effectiveBranch === null) {
|
||||||
|
// Viewing main but branch not initialized
|
||||||
|
return projectPath
|
||||||
|
? useAppStore.getState().isPrimaryWorktreeBranch(projectPath, featureBranch)
|
||||||
|
: false;
|
||||||
|
} else {
|
||||||
|
// Match by branch name
|
||||||
|
return featureBranch === effectiveBranch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [features, currentWorktreePath, currentWorktreeBranch, projectPath]);
|
||||||
|
|
||||||
|
// Handle node click - view details
|
||||||
|
const handleNodeClick = useCallback(
|
||||||
|
(featureId: string) => {
|
||||||
|
const feature = features.find((f) => f.id === featureId);
|
||||||
|
if (feature) {
|
||||||
|
onViewOutput(feature);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[features, onViewOutput]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle node double click - edit
|
||||||
|
const handleNodeDoubleClick = useCallback(
|
||||||
|
(featureId: string) => {
|
||||||
|
const feature = features.find((f) => f.id === featureId);
|
||||||
|
if (feature) {
|
||||||
|
onEditFeature(feature);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[features, onEditFeature]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 overflow-hidden relative">
|
||||||
|
<GraphCanvas
|
||||||
|
features={filteredFeatures}
|
||||||
|
runningAutoTasks={runningAutoTasks}
|
||||||
|
onNodeClick={handleNodeClick}
|
||||||
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
|
backgroundStyle={backgroundImageStyle}
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
apps/ui/src/components/views/graph-view/hooks/index.ts
Normal file
2
apps/ui/src/components/views/graph-view/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { useGraphNodes, type TaskNode, type DependencyEdge, type TaskNodeData } from './use-graph-nodes';
|
||||||
|
export { useGraphLayout } from './use-graph-layout';
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import dagre from 'dagre';
|
||||||
|
import { Node, Edge, useReactFlow } from '@xyflow/react';
|
||||||
|
import { TaskNode, DependencyEdge } from './use-graph-nodes';
|
||||||
|
|
||||||
|
const NODE_WIDTH = 280;
|
||||||
|
const NODE_HEIGHT = 120;
|
||||||
|
|
||||||
|
interface UseGraphLayoutProps {
|
||||||
|
nodes: TaskNode[];
|
||||||
|
edges: DependencyEdge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies dagre layout to position nodes in a hierarchical DAG
|
||||||
|
* Dependencies flow left-to-right
|
||||||
|
*/
|
||||||
|
export function useGraphLayout({ nodes, edges }: UseGraphLayoutProps) {
|
||||||
|
const { fitView, setNodes } = useReactFlow();
|
||||||
|
|
||||||
|
const getLayoutedElements = useCallback(
|
||||||
|
(
|
||||||
|
inputNodes: TaskNode[],
|
||||||
|
inputEdges: DependencyEdge[],
|
||||||
|
direction: 'LR' | 'TB' = 'LR'
|
||||||
|
): { nodes: TaskNode[]; edges: DependencyEdge[] } => {
|
||||||
|
const dagreGraph = new dagre.graphlib.Graph();
|
||||||
|
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||||
|
|
||||||
|
const isHorizontal = direction === 'LR';
|
||||||
|
dagreGraph.setGraph({
|
||||||
|
rankdir: direction,
|
||||||
|
nodesep: 50,
|
||||||
|
ranksep: 100,
|
||||||
|
marginx: 50,
|
||||||
|
marginy: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
inputNodes.forEach((node) => {
|
||||||
|
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||||
|
});
|
||||||
|
|
||||||
|
inputEdges.forEach((edge) => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
dagre.layout(dagreGraph);
|
||||||
|
|
||||||
|
const layoutedNodes = inputNodes.map((node) => {
|
||||||
|
const nodeWithPosition = dagreGraph.node(node.id);
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
position: {
|
||||||
|
x: nodeWithPosition.x - NODE_WIDTH / 2,
|
||||||
|
y: nodeWithPosition.y - NODE_HEIGHT / 2,
|
||||||
|
},
|
||||||
|
targetPosition: isHorizontal ? 'left' : 'top',
|
||||||
|
sourcePosition: isHorizontal ? 'right' : 'bottom',
|
||||||
|
} as TaskNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { nodes: layoutedNodes, edges: inputEdges };
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial layout
|
||||||
|
const layoutedElements = useMemo(() => {
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
return { nodes: [], edges: [] };
|
||||||
|
}
|
||||||
|
return getLayoutedElements(nodes, edges, 'LR');
|
||||||
|
}, [nodes, edges, getLayoutedElements]);
|
||||||
|
|
||||||
|
// Manual re-layout function
|
||||||
|
const runLayout = useCallback(
|
||||||
|
(direction: 'LR' | 'TB' = 'LR') => {
|
||||||
|
const { nodes: layoutedNodes } = getLayoutedElements(nodes, edges, direction);
|
||||||
|
setNodes(layoutedNodes);
|
||||||
|
// Fit view after layout with a small delay to allow DOM updates
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({ padding: 0.2, duration: 300 });
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
[nodes, edges, getLayoutedElements, setNodes, fitView]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
layoutedNodes: layoutedElements.nodes,
|
||||||
|
layoutedEdges: layoutedElements.edges,
|
||||||
|
runLayout,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Node, Edge } from '@xyflow/react';
|
||||||
|
import { Feature } from '@/store/app-store';
|
||||||
|
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||||
|
|
||||||
|
export interface TaskNodeData extends Feature {
|
||||||
|
isBlocked: boolean;
|
||||||
|
isRunning: boolean;
|
||||||
|
blockingDependencies: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaskNode = Node<TaskNodeData, 'task'>;
|
||||||
|
export type DependencyEdge = Edge<{ sourceStatus: Feature['status']; targetStatus: Feature['status'] }>;
|
||||||
|
|
||||||
|
interface UseGraphNodesProps {
|
||||||
|
features: Feature[];
|
||||||
|
runningAutoTasks: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms features into React Flow nodes and edges
|
||||||
|
* Creates dependency edges based on feature.dependencies array
|
||||||
|
*/
|
||||||
|
export function useGraphNodes({ features, runningAutoTasks }: UseGraphNodesProps) {
|
||||||
|
const { nodes, edges } = useMemo(() => {
|
||||||
|
const nodeList: TaskNode[] = [];
|
||||||
|
const edgeList: DependencyEdge[] = [];
|
||||||
|
const featureMap = new Map<string, Feature>();
|
||||||
|
|
||||||
|
// Create feature map for quick lookups
|
||||||
|
features.forEach((f) => featureMap.set(f.id, f));
|
||||||
|
|
||||||
|
// Create nodes
|
||||||
|
features.forEach((feature) => {
|
||||||
|
const isRunning = runningAutoTasks.includes(feature.id);
|
||||||
|
const blockingDeps = getBlockingDependencies(feature, features);
|
||||||
|
|
||||||
|
const node: TaskNode = {
|
||||||
|
id: feature.id,
|
||||||
|
type: 'task',
|
||||||
|
position: { x: 0, y: 0 }, // Will be set by layout
|
||||||
|
data: {
|
||||||
|
...feature,
|
||||||
|
isBlocked: blockingDeps.length > 0,
|
||||||
|
isRunning,
|
||||||
|
blockingDependencies: blockingDeps,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeList.push(node);
|
||||||
|
|
||||||
|
// Create edges for dependencies
|
||||||
|
if (feature.dependencies && feature.dependencies.length > 0) {
|
||||||
|
feature.dependencies.forEach((depId: string) => {
|
||||||
|
// Only create edge if the dependency exists in current view
|
||||||
|
if (featureMap.has(depId)) {
|
||||||
|
const sourceFeature = featureMap.get(depId)!;
|
||||||
|
const edge: DependencyEdge = {
|
||||||
|
id: `${depId}->${feature.id}`,
|
||||||
|
source: depId,
|
||||||
|
target: feature.id,
|
||||||
|
type: 'dependency',
|
||||||
|
animated: isRunning || runningAutoTasks.includes(depId),
|
||||||
|
data: {
|
||||||
|
sourceStatus: sourceFeature.status,
|
||||||
|
targetStatus: feature.status,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
edgeList.push(edge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { nodes: nodeList, edges: edgeList };
|
||||||
|
}, [features, runningAutoTasks]);
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
}
|
||||||
4
apps/ui/src/components/views/graph-view/index.ts
Normal file
4
apps/ui/src/components/views/graph-view/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { GraphView } from './graph-view';
|
||||||
|
export { GraphCanvas } from './graph-canvas';
|
||||||
|
export { TaskNode, DependencyEdge, GraphControls, GraphLegend } from './components';
|
||||||
|
export { useGraphNodes, useGraphLayout, type TaskNode as TaskNodeType, type DependencyEdge as DependencyEdgeType } from './hooks';
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { PageShell } from '@/components/layout/page-shell';
|
||||||
|
|
||||||
import { useCliStatus, useSettingsView } from './settings-view/hooks';
|
import { useCliStatus, useSettingsView } from './settings-view/hooks';
|
||||||
import { NAV_ITEMS } from './settings-view/config/navigation';
|
import { NAV_ITEMS } from './settings-view/config/navigation';
|
||||||
@@ -156,36 +157,38 @@ export function SettingsView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="settings-view">
|
<PageShell>
|
||||||
{/* Header Section */}
|
<div className="flex-1 flex flex-col overflow-hidden h-full" data-testid="settings-view">
|
||||||
<SettingsHeader />
|
{/* Header Section */}
|
||||||
|
<SettingsHeader />
|
||||||
|
|
||||||
{/* Content Area with Sidebar */}
|
{/* Content Area with Sidebar */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
{/* Side Navigation - No longer scrolls, just switches views */}
|
{/* Side Navigation - No longer scrolls, just switches views */}
|
||||||
<SettingsNavigation
|
<SettingsNavigation
|
||||||
navItems={NAV_ITEMS}
|
navItems={NAV_ITEMS}
|
||||||
activeSection={activeView}
|
activeSection={activeView}
|
||||||
currentProject={currentProject}
|
currentProject={currentProject}
|
||||||
onNavigate={navigateTo}
|
onNavigate={navigateTo}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Content Panel - Shows only the active section */}
|
{/* Content Panel - Shows only the active section */}
|
||||||
<div className="flex-1 overflow-y-auto p-8">
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
<div className="max-w-4xl mx-auto">{renderActiveSection()}</div>
|
<div className="max-w-4xl mx-auto">{renderActiveSection()}</div>
|
||||||
|
</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>
|
</div>
|
||||||
|
</PageShell>
|
||||||
{/* Keyboard Map Dialog */}
|
|
||||||
<KeyboardMapDialog open={showKeyboardMapDialog} onOpenChange={setShowKeyboardMapDialog} />
|
|
||||||
|
|
||||||
{/* Delete Project Confirmation Dialog */}
|
|
||||||
<DeleteProjectDialog
|
|
||||||
open={showDeleteDialog}
|
|
||||||
onOpenChange={setShowDeleteDialog}
|
|
||||||
project={currentProject}
|
|
||||||
onConfirm={moveProjectToTrash}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,7 @@ export function SettingsHeader({
|
|||||||
description = 'Configure your API keys and preferences',
|
description = 'Configure your API keys and preferences',
|
||||||
}: SettingsHeaderProps) {
|
}: SettingsHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('shrink-0', 'border-b border-white/10', 'bg-white/5 backdrop-blur-xl')}>
|
||||||
className={cn(
|
|
||||||
'shrink-0',
|
|
||||||
'border-b border-border/50',
|
|
||||||
'bg-gradient-to-r from-card/90 via-card/70 to-card/80 backdrop-blur-xl'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="px-8 py-6">
|
<div className="px-8 py-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export function SpecView() {
|
|||||||
|
|
||||||
// Main view - spec exists
|
// Main view - spec exists
|
||||||
return (
|
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
|
<SpecHeader
|
||||||
projectPath={currentProject.path}
|
projectPath={currentProject.path}
|
||||||
isRegenerating={isRegenerating}
|
isRegenerating={isRegenerating}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function SpecHeader({
|
|||||||
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
|
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-3">
|
||||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
defaultDropAnimationSideEffects,
|
defaultDropAnimationSideEffects,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { PageShell } from '@/components/layout/page-shell';
|
||||||
|
|
||||||
interface TerminalStatus {
|
interface TerminalStatus {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -141,11 +142,11 @@ function TerminalTabButton({
|
|||||||
{...dragAttributes}
|
{...dragAttributes}
|
||||||
{...dragListeners}
|
{...dragListeners}
|
||||||
className={cn(
|
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
|
isActive
|
||||||
? 'bg-background border-brand-500 text-foreground'
|
? 'bg-white/10 border-cyan-500 text-cyan-100 shadow-[0_-1px_10px_rgba(6,182,212,0.1)]'
|
||||||
: 'bg-muted border-transparent text-muted-foreground hover:text-foreground hover:bg-accent',
|
: 'bg-white/5 border-transparent text-muted-foreground hover:text-cyan-50 hover:bg-white/10',
|
||||||
isOver && isDropTarget && isDraggingTab && 'ring-2 ring-blue-500 bg-blue-500/10',
|
isOver && isDropTarget && isDraggingTab && 'ring-2 ring-cyan-500/50 bg-cyan-500/10',
|
||||||
isDragging && 'opacity-50'
|
isDragging && 'opacity-50'
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -192,8 +193,8 @@ function NewTabDropZone({ isDropTarget }: { isDropTarget: boolean }) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center px-3 py-1.5 rounded-t-md border-2 border-dashed transition-all',
|
'flex items-center justify-center px-3 py-1.5 rounded-t-md border-2 border-dashed transition-all',
|
||||||
isOver && isDropTarget
|
isOver && isDropTarget
|
||||||
? 'border-green-500 bg-green-500/10 text-green-500'
|
? 'border-cyan-500/50 bg-cyan-500/10 text-cyan-400'
|
||||||
: 'border-transparent text-muted-foreground hover:border-border'
|
: 'border-transparent text-muted-foreground hover:border-white/10'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SquarePlus className="h-4 w-4" />
|
<SquarePlus className="h-4 w-4" />
|
||||||
@@ -1414,252 +1415,256 @@ export function TerminalView() {
|
|||||||
|
|
||||||
// Terminal view with tabs
|
// Terminal view with tabs
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<PageShell fullWidth>
|
||||||
sensors={sensors}
|
<DndContext
|
||||||
collisionDetection={closestCenter}
|
sensors={sensors}
|
||||||
onDragStart={handleDragStart}
|
collisionDetection={closestCenter}
|
||||||
onDragOver={handleDragOver}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragOver={handleDragOver}
|
||||||
onDragCancel={handleDragCancel}
|
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}
|
|
||||||
>
|
>
|
||||||
{activeDragId ? (
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<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">
|
{/* Tab bar */}
|
||||||
<TerminalIcon className="h-4 w-4 text-brand-500 shrink-0" />
|
<div className="flex items-center bg-card border-b border-border px-2">
|
||||||
<span className="text-sm font-medium text-foreground whitespace-nowrap">
|
{/* Tabs */}
|
||||||
{dragOverTabId === 'new' ? 'New tab' : dragOverTabId ? 'Move to tab' : 'Terminal'}
|
<div className="flex items-center gap-1 flex-1 overflow-x-auto py-1">
|
||||||
</span>
|
{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>
|
</div>
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
{/* Active tab content */}
|
||||||
</DndContext>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import type { Project, TrashedProject } from '@/lib/electron';
|
|||||||
import type {
|
import type {
|
||||||
Feature as BaseFeature,
|
Feature as BaseFeature,
|
||||||
FeatureImagePath,
|
FeatureImagePath,
|
||||||
|
FeatureTextFilePath, // Import missing type
|
||||||
AgentModel,
|
AgentModel,
|
||||||
PlanningMode,
|
PlanningMode,
|
||||||
AIProfile,
|
AIProfile,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
|
|
||||||
// Re-export ThemeMode for convenience
|
|
||||||
export type { ThemeMode };
|
|
||||||
|
|
||||||
export type ViewMode =
|
export type ViewMode =
|
||||||
| 'welcome'
|
| 'welcome'
|
||||||
| 'setup'
|
| 'setup'
|
||||||
@@ -43,10 +41,30 @@ export type ThemeMode =
|
|||||||
| 'red'
|
| 'red'
|
||||||
| 'cream'
|
| 'cream'
|
||||||
| 'sunset'
|
| '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';
|
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
|
||||||
|
|
||||||
|
export type BoardViewMode = 'kanban' | 'graph';
|
||||||
|
|
||||||
export interface ApiKeys {
|
export interface ApiKeys {
|
||||||
anthropic: string;
|
anthropic: string;
|
||||||
google: string;
|
google: string;
|
||||||
@@ -435,6 +453,7 @@ export interface AppState {
|
|||||||
|
|
||||||
// Kanban Card Display Settings
|
// Kanban Card Display Settings
|
||||||
kanbanCardDetailLevel: KanbanCardDetailLevel; // Level of detail shown on kanban cards
|
kanbanCardDetailLevel: KanbanCardDetailLevel; // Level of detail shown on kanban cards
|
||||||
|
boardViewMode: BoardViewMode; // Whether to show kanban or dependency graph view
|
||||||
|
|
||||||
// Feature Default Settings
|
// Feature Default Settings
|
||||||
defaultSkipTests: boolean; // Default value for skip tests when creating new features
|
defaultSkipTests: boolean; // Default value for skip tests when creating new features
|
||||||
@@ -698,6 +717,7 @@ export interface AppActions {
|
|||||||
|
|
||||||
// Kanban Card Settings actions
|
// Kanban Card Settings actions
|
||||||
setKanbanCardDetailLevel: (level: KanbanCardDetailLevel) => void;
|
setKanbanCardDetailLevel: (level: KanbanCardDetailLevel) => void;
|
||||||
|
setBoardViewMode: (mode: BoardViewMode) => void;
|
||||||
|
|
||||||
// Feature Default Settings actions
|
// Feature Default Settings actions
|
||||||
setDefaultSkipTests: (skip: boolean) => void;
|
setDefaultSkipTests: (skip: boolean) => void;
|
||||||
@@ -899,18 +919,19 @@ const initialState: AppState = {
|
|||||||
chatHistoryOpen: false,
|
chatHistoryOpen: false,
|
||||||
autoModeByProject: {},
|
autoModeByProject: {},
|
||||||
autoModeActivityLog: [],
|
autoModeActivityLog: [],
|
||||||
maxConcurrency: 3, // Default to 3 concurrent agents
|
maxConcurrency: 3,
|
||||||
kanbanCardDetailLevel: 'standard', // Default to standard detail level
|
kanbanCardDetailLevel: 'standard',
|
||||||
defaultSkipTests: true, // Default to manual verification (tests disabled)
|
boardViewMode: 'kanban',
|
||||||
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
|
defaultSkipTests: false,
|
||||||
useWorktrees: false, // Default to disabled (worktree feature is experimental)
|
enableDependencyBlocking: true,
|
||||||
|
useWorktrees: false,
|
||||||
currentWorktreeByProject: {},
|
currentWorktreeByProject: {},
|
||||||
worktreesByProject: {},
|
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,
|
aiProfiles: DEFAULT_AI_PROFILES,
|
||||||
|
showProfilesOnly: false,
|
||||||
|
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
|
||||||
|
muteDoneSound: false,
|
||||||
|
enhancementModel: 'sonnet',
|
||||||
projectAnalysis: null,
|
projectAnalysis: null,
|
||||||
isAnalyzing: false,
|
isAnalyzing: false,
|
||||||
boardBackgroundByProject: {},
|
boardBackgroundByProject: {},
|
||||||
@@ -923,19 +944,23 @@ const initialState: AppState = {
|
|||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
maximizedSessionId: null,
|
maximizedSessionId: null,
|
||||||
defaultFontSize: 14,
|
defaultFontSize: 14,
|
||||||
defaultRunScript: '',
|
defaultRunScript: '', // Empty string = standard shell
|
||||||
screenReaderMode: false,
|
screenReaderMode: false,
|
||||||
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
|
fontFamily: 'monospace',
|
||||||
scrollbackLines: 5000,
|
scrollbackLines: 1000,
|
||||||
lineHeight: 1.0,
|
lineHeight: 1.2,
|
||||||
maxSessions: 100,
|
maxSessions: 20,
|
||||||
},
|
},
|
||||||
terminalLayoutByProject: {},
|
terminalLayoutByProject: {},
|
||||||
specCreatingForProject: null,
|
specCreatingForProject: null,
|
||||||
defaultPlanningMode: 'skip' as PlanningMode,
|
defaultPlanningMode: 'lite',
|
||||||
defaultRequirePlanApproval: false,
|
defaultRequirePlanApproval: true,
|
||||||
defaultAIProfileId: null,
|
defaultAIProfileId: null,
|
||||||
pendingPlanApproval: null,
|
pendingPlanApproval: null,
|
||||||
|
// Claude Usage Defaults
|
||||||
|
claudeRefreshInterval: 60,
|
||||||
|
claudeUsage: null,
|
||||||
|
claudeUsageLastUpdated: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAppStore = create<AppState & AppActions>()(
|
export const useAppStore = create<AppState & AppActions>()(
|
||||||
@@ -1451,6 +1476,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
|
|
||||||
// Kanban Card Settings actions
|
// Kanban Card Settings actions
|
||||||
setKanbanCardDetailLevel: (level) => set({ kanbanCardDetailLevel: level }),
|
setKanbanCardDetailLevel: (level) => set({ kanbanCardDetailLevel: level }),
|
||||||
|
setBoardViewMode: (mode) => set({ boardViewMode: mode }),
|
||||||
|
|
||||||
// Feature Default Settings actions
|
// Feature Default Settings actions
|
||||||
setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }),
|
setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }),
|
||||||
@@ -2658,6 +2684,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
sidebarOpen: state.sidebarOpen,
|
sidebarOpen: state.sidebarOpen,
|
||||||
chatHistoryOpen: state.chatHistoryOpen,
|
chatHistoryOpen: state.chatHistoryOpen,
|
||||||
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
|
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
|
||||||
|
boardViewMode: state.boardViewMode,
|
||||||
// Settings
|
// Settings
|
||||||
apiKeys: state.apiKeys,
|
apiKeys: state.apiKeys,
|
||||||
maxConcurrency: state.maxConcurrency,
|
maxConcurrency: state.maxConcurrency,
|
||||||
|
|||||||
@@ -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 'tailwindcss';
|
||||||
@import 'tw-animate-css';
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@
|
|||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-foreground-secondary: var(--foreground-secondary);
|
--color-foreground-secondary: var(--foreground-secondary);
|
||||||
--color-foreground-muted: var(--foreground-muted);
|
--color-foreground-muted: var(--foreground-muted);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace;
|
||||||
|
|
||||||
/* Sidebar colors */
|
/* Sidebar colors */
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
@@ -119,29 +120,94 @@
|
|||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--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 {
|
:root {
|
||||||
/* Default to light mode */
|
/* Default to light mode (overridden by Prism values below for now as we pivot) */
|
||||||
--radius: 0.625rem;
|
--radius: 0.75rem;
|
||||||
--background: oklch(1 0 0);
|
|
||||||
--foreground: oklch(0.145 0 0);
|
/* PRISM THEME VALUES (Base) */
|
||||||
--card: oklch(1 0 0);
|
--bg-deep: #0b101a;
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--sidebar-bg: rgba(13, 17, 26, 0.7);
|
||||||
--popover: oklch(1 0 0);
|
--glass-bg: rgba(255, 255, 255, 0.03);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--glass-border: rgba(255, 255, 255, 0.07);
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
/* Accents */
|
||||||
--secondary: oklch(0.97 0 0);
|
--accent-cyan: #22d3ee;
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--accent-orange: #f59e0b;
|
||||||
--muted: oklch(0.97 0 0);
|
--accent-green: #10b981;
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--accent-red: #ef4444;
|
||||||
--accent: oklch(0.97 0 0);
|
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
/* Mapping to existing variables where possible, or overriding for the visual appearance */
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--background: oklch(0.08 0.03 260); /* Approximation of #0b101a */
|
||||||
--border: oklch(0.922 0 0);
|
--foreground: oklch(0.95 0 0);
|
||||||
--input: oklch(0.922 0 0);
|
--card: oklch(1 0 0 / 0.03); /* Glass card default */
|
||||||
--ring: oklch(0.708 0 0);
|
--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-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
@@ -159,7 +225,7 @@
|
|||||||
--background-80: oklch(1 0 0 / 0.8);
|
--background-80: oklch(1 0 0 / 0.8);
|
||||||
--foreground-secondary: oklch(0.4 0 0);
|
--foreground-secondary: oklch(0.4 0 0);
|
||||||
--foreground-muted: oklch(0.556 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-400: oklch(0.6 0.22 265);
|
||||||
--brand-500: oklch(0.55 0.25 265);
|
--brand-500: oklch(0.55 0.25 265);
|
||||||
--brand-600: oklch(0.5 0.28 270);
|
--brand-600: oklch(0.5 0.28 270);
|
||||||
@@ -191,17 +257,17 @@
|
|||||||
--status-in-progress: oklch(0.7 0.15 70);
|
--status-in-progress: oklch(0.7 0.15 70);
|
||||||
--status-waiting: oklch(0.65 0.18 50);
|
--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-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-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.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
--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.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
--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.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
--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 tokens */
|
||||||
--transition-fast: 150ms ease;
|
--transition-fast: 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
--transition-normal: 200ms ease;
|
--transition-normal: 250ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
--transition-slow: 300ms ease-out;
|
--transition-slow: 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply dark mode immediately based on system preference (before JS runs) */
|
/* Apply dark mode immediately based on system preference (before JS runs) */
|
||||||
@@ -214,13 +280,13 @@
|
|||||||
|
|
||||||
/* Text colors following hierarchy */
|
/* Text colors following hierarchy */
|
||||||
--foreground: oklch(1 0 0); /* text-white */
|
--foreground: oklch(1 0 0); /* text-white */
|
||||||
--foreground-secondary: oklch(0.588 0 0); /* text-zinc-400 */
|
--foreground-secondary: oklch(0.65 0 0); /* lighter for better readability */
|
||||||
--foreground-muted: oklch(0.525 0 0); /* text-zinc-500 */
|
--foreground-muted: oklch(0.525 0 0);
|
||||||
|
|
||||||
/* Card and popover backgrounds */
|
/* 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);
|
--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);
|
--popover-foreground: oklch(1 0 0);
|
||||||
|
|
||||||
/* Brand colors - purple/violet theme */
|
/* Brand colors - purple/violet theme */
|
||||||
@@ -231,18 +297,18 @@
|
|||||||
--brand-600: oklch(0.5 0.28 270);
|
--brand-600: oklch(0.5 0.28 270);
|
||||||
|
|
||||||
/* Glass morphism borders and accents */
|
/* 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);
|
--secondary-foreground: oklch(1 0 0);
|
||||||
--muted: oklch(0.176 0 0);
|
--muted: oklch(0.176 0 0);
|
||||||
--muted-foreground: oklch(0.588 0 0);
|
--muted-foreground: oklch(0.6 0 0);
|
||||||
--accent: oklch(1 0 0 / 0.1);
|
--accent: oklch(1 0 0 / 0.12);
|
||||||
--accent-foreground: oklch(1 0 0);
|
--accent-foreground: oklch(1 0 0);
|
||||||
|
|
||||||
/* Borders with transparency for glass effect */
|
/* Borders with transparency for glass effect */
|
||||||
--border: oklch(0.176 0 0);
|
--border: oklch(1 0 0 / 0.08);
|
||||||
--border-glass: oklch(1 0 0 / 0.1);
|
--border-glass: oklch(1 0 0 / 0.12);
|
||||||
--destructive: oklch(0.6 0.25 25);
|
--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);
|
--ring: oklch(0.55 0.25 265);
|
||||||
|
|
||||||
/* Chart colors with brand theme */
|
/* Chart colors with brand theme */
|
||||||
@@ -253,13 +319,13 @@
|
|||||||
--chart-5: oklch(0.6 0.25 20);
|
--chart-5: oklch(0.6 0.25 20);
|
||||||
|
|
||||||
/* Sidebar with glass morphism */
|
/* 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-foreground: oklch(1 0 0);
|
||||||
--sidebar-primary: oklch(0.55 0.25 265);
|
--sidebar-primary: oklch(0.55 0.25 265);
|
||||||
--sidebar-primary-foreground: oklch(1 0 0);
|
--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-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);
|
--sidebar-ring: oklch(0.55 0.25 265);
|
||||||
|
|
||||||
/* Action button colors */
|
/* Action button colors */
|
||||||
@@ -292,67 +358,21 @@
|
|||||||
/* Shadow tokens - darker for dark mode */
|
/* Shadow tokens - darker for dark mode */
|
||||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
|
--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-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-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.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
--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.5), 0 10px 10px -5px rgba(0, 0, 0, 0.3);
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.7), 0 10px 10px -5px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.light {
|
.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: oklch(1 0 0); /* White */
|
||||||
--background-50: oklch(1 0 0 / 0.5);
|
--font-sans: var(--font-geist-sans);
|
||||||
--background-80: oklch(1 0 0 / 0.8);
|
--font-mono: var(--font-geist-mono);
|
||||||
--foreground: oklch(0.145 0 0); /* Dark text */
|
/* Re-declare all light vars to ensure priority if needed,
|
||||||
--foreground-secondary: oklch(0.4 0 0);
|
but :root typically handles this.
|
||||||
--foreground-muted: oklch(0.556 0 0);
|
Just ensuring important overrides. */
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0 / 0.8);
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -360,10 +380,13 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
@apply bg-background;
|
@apply bg-background antialiased;
|
||||||
|
font-feature-settings:
|
||||||
|
'rlig' 1,
|
||||||
|
'calt' 1;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground tracking-tight;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,6 +409,7 @@
|
|||||||
select:disabled,
|
select:disabled,
|
||||||
textarea:disabled {
|
textarea:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,8 +431,8 @@
|
|||||||
.gray
|
.gray
|
||||||
)
|
)
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(
|
:is(
|
||||||
@@ -428,110 +452,35 @@
|
|||||||
.gray
|
.gray
|
||||||
)
|
)
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: var(--muted);
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.dark, .retro) ::-webkit-scrollbar-thumb {
|
:is(.dark, .retro) ::-webkit-scrollbar-thumb {
|
||||||
background: oklch(0.3 0 0);
|
background: oklch(1 0 0 / 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 99px;
|
||||||
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.dark, .retro) ::-webkit-scrollbar-thumb:hover {
|
:is(.dark, .retro) ::-webkit-scrollbar-thumb:hover {
|
||||||
background: oklch(0.4 0 0);
|
background: oklch(1 0 0 / 0.3);
|
||||||
}
|
|
||||||
|
|
||||||
/* 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glass morphism utilities */
|
/* Glass morphism utilities */
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.glass {
|
.glass {
|
||||||
@apply backdrop-blur-md border-white/10;
|
@apply backdrop-blur-xl border border-white/10 bg-white/5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-subtle {
|
.glass-subtle {
|
||||||
@apply backdrop-blur-sm border-white/5;
|
@apply backdrop-blur-md border border-white/5 bg-white/3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-strong {
|
.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 */
|
/* Text color hierarchy utilities */
|
||||||
@@ -540,55 +489,73 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-secondary {
|
.text-secondary {
|
||||||
color: oklch(0.588 0 0); /* zinc-400 equivalent */
|
color: var(--foreground-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-muted {
|
.text-muted {
|
||||||
color: oklch(0.525 0 0); /* zinc-500 equivalent */
|
color: var(--foreground-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Brand gradient utilities */
|
/* Brand gradient utilities */
|
||||||
.gradient-brand {
|
.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 {
|
.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 */
|
.gradient-glass-border {
|
||||||
.bg-glass {
|
background: linear-gradient(
|
||||||
background: var(--card);
|
to bottom right,
|
||||||
backdrop-filter: blur(12px);
|
rgba(255, 255, 255, 0.2),
|
||||||
-webkit-backdrop-filter: blur(12px);
|
rgba(255, 255, 255, 0.05)
|
||||||
}
|
);
|
||||||
|
|
||||||
.bg-glass-80 {
|
|
||||||
background: var(--popover);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover state utilities */
|
/* Hover state utilities */
|
||||||
.hover-glass {
|
.hover-glass {
|
||||||
transition: background-color 0.2s ease;
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-glass:hover {
|
.hover-glass:hover {
|
||||||
background: oklch(1 0 0 / 0.05);
|
background: color-mix(in oklch, var(--foreground), transparent 95%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-glass-strong {
|
.hover-lift {
|
||||||
transition: background-color 0.2s ease;
|
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-glass-strong:hover {
|
.hover-lift:hover {
|
||||||
background: oklch(1 0 0 / 0.1);
|
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 area background */
|
||||||
.content-bg {
|
.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 */
|
/* Action button utilities */
|
||||||
@@ -644,7 +611,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Animated border for in-progress cards */
|
/* Animated border for in-progress cards */
|
||||||
/* Using a subtle pulse animation instead of continuous gradient rotation for GPU efficiency */
|
|
||||||
@keyframes border-pulse {
|
@keyframes border-pulse {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
@@ -657,8 +623,8 @@
|
|||||||
|
|
||||||
.animated-border-wrapper {
|
.animated-border-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 0.75rem;
|
border-radius: var(--radius);
|
||||||
padding: 2px;
|
padding: 1px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
var(--running-indicator),
|
var(--running-indicator),
|
||||||
@@ -677,7 +643,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.animated-border-wrapper > * {
|
.animated-border-wrapper > * {
|
||||||
border-radius: calc(0.75rem - 2px);
|
border-radius: calc(var(--radius) - 1px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,6 +651,7 @@
|
|||||||
|
|
||||||
.retro * {
|
.retro * {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
--radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animated Outline Button Styles */
|
/* Animated Outline Button Styles */
|
||||||
@@ -693,12 +660,6 @@
|
|||||||
background: conic-gradient(from 90deg at 50% 50%, #a855f7 0%, #3b82f6 50%, #a855f7 100%);
|
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 {
|
@keyframes retro-glow {
|
||||||
from {
|
from {
|
||||||
filter: brightness(1) drop-shadow(0 0 2px #00ff41);
|
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 {
|
@keyframes synthwave-glow {
|
||||||
from {
|
from {
|
||||||
filter: brightness(1) drop-shadow(0 0 3px #f97e72);
|
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 utilities for text overflow prevention */
|
||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
@@ -802,30 +725,6 @@
|
|||||||
Theme-aware colors for XML editor
|
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 container styles */
|
||||||
.xml-editor {
|
.xml-editor {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -889,3 +788,257 @@
|
|||||||
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--muted-foreground);
|
background: var(--muted-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
DEPENDENCY GRAPH STYLES
|
||||||
|
Theme-aware styling for React Flow graph
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* React Flow base theme overrides */
|
||||||
|
.graph-canvas {
|
||||||
|
--xy-background-color: transparent;
|
||||||
|
--xy-node-background-color: var(--card);
|
||||||
|
--xy-node-border-color: var(--border);
|
||||||
|
--xy-node-border-radius: 0.75rem;
|
||||||
|
--xy-edge-stroke-default: var(--border);
|
||||||
|
--xy-edge-stroke-selected: var(--brand-500);
|
||||||
|
--xy-minimap-background-color: var(--popover);
|
||||||
|
--xy-minimap-mask-background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
--xy-controls-background-color: var(--popover);
|
||||||
|
--xy-controls-border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MiniMap styling */
|
||||||
|
.graph-canvas .react-flow__minimap {
|
||||||
|
background-color: var(--popover) !important;
|
||||||
|
border: 1px solid var(--border) !important;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-canvas .react-flow__minimap-mask {
|
||||||
|
fill: var(--background);
|
||||||
|
fill-opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge animations */
|
||||||
|
@keyframes flow-dash {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes edge-glow {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
filter: drop-shadow(0 0 2px var(--status-in-progress));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: drop-shadow(0 0 6px var(--status-in-progress));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-canvas .animated-edge path {
|
||||||
|
animation: flow-dash 0.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-canvas .edge-flowing path {
|
||||||
|
animation:
|
||||||
|
flow-dash 0.5s linear infinite,
|
||||||
|
edge-glow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge particle animation */
|
||||||
|
.edge-particle {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node animations */
|
||||||
|
@keyframes pulse-subtle {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 var(--status-in-progress);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 15px 3px var(--status-in-progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-subtle {
|
||||||
|
animation: pulse-subtle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bar indeterminate animation */
|
||||||
|
@keyframes progress-indeterminate {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(50%);
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(200%);
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-progress-indeterminate {
|
||||||
|
animation: progress-indeterminate 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle styling */
|
||||||
|
.graph-canvas .react-flow__handle {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--border);
|
||||||
|
border: 2px solid var(--background);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-canvas .react-flow__handle:hover {
|
||||||
|
background-color: var(--brand-500);
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-canvas .react-flow__handle-left {
|
||||||
|
left: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-canvas .react-flow__handle-right {
|
||||||
|
right: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node base styling - override React Flow defaults */
|
||||||
|
.graph-canvas .react-flow__node {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection styles */
|
||||||
|
.graph-canvas .react-flow__node.selected {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-canvas .react-flow__edge.selected path {
|
||||||
|
stroke: var(--brand-500);
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribution removal (requires pro license) */
|
||||||
|
.graph-canvas .react-flow__attribution {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panel styling */
|
||||||
|
.graph-canvas .react-flow__panel {
|
||||||
|
margin: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retro theme overrides */
|
||||||
|
.retro .graph-canvas .react-flow__handle,
|
||||||
|
.retro .graph-canvas .react-flow__minimap {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retro .graph-canvas .react-flow__node {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce motion preference */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.graph-canvas .animated-edge path,
|
||||||
|
.graph-canvas .edge-flowing path,
|
||||||
|
.animate-pulse-subtle,
|
||||||
|
.animate-progress-indeterminate {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,13 +89,26 @@ await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout
|
|||||||
await page.waitForSelector('[data-testid="welcome-view"]');
|
await page.waitForSelector('[data-testid="welcome-view"]');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Wait for network idle after navigation
|
### Wait for page load after navigation
|
||||||
|
|
||||||
|
**Important:** Use `load` state, NOT `networkidle`. This app has persistent connections (websockets, polling) that prevent the network from ever becoming "idle", causing `networkidle` to timeout.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
|
// Then wait for specific elements to verify the page is ready
|
||||||
|
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Why not `networkidle`?**
|
||||||
|
|
||||||
|
- `networkidle` requires no network activity for 500ms
|
||||||
|
- Modern SPAs with real-time features (websockets, polling, SSE) never reach this state
|
||||||
|
- Using `networkidle` causes 30+ second timeouts and flaky tests
|
||||||
|
- The `load` state fires when the page finishes loading, which is sufficient
|
||||||
|
- Always follow up with element visibility checks for reliability
|
||||||
|
|
||||||
### Use appropriate timeouts
|
### Use appropriate timeouts
|
||||||
|
|
||||||
- Quick UI updates: 5000ms (default)
|
- Quick UI updates: 5000ms (default)
|
||||||
@@ -267,6 +280,29 @@ npm run test -- project-creation.spec.ts --repeat-each=5
|
|||||||
3. Run with `--headed` to watch the test
|
3. Run with `--headed` to watch the test
|
||||||
4. Add `await page.pause()` to pause execution at a specific point
|
4. Add `await page.pause()` to pause execution at a specific point
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Timeout on `waitForLoadState('networkidle')`
|
||||||
|
|
||||||
|
If tests timeout waiting for network idle, the app likely has persistent connections. Use `load` state instead:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad - will timeout with persistent connections
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Good - completes when page loads
|
||||||
|
await page.waitForLoadState('load');
|
||||||
|
await expect(page.locator('[data-testid="my-element"]')).toBeVisible();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port conflicts
|
||||||
|
|
||||||
|
If you see "Port 3008 is already in use", kill the process:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lsof -ti:3008 | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
## Available Test Utilities
|
## Available Test Utilities
|
||||||
|
|
||||||
Import from `./utils`:
|
Import from `./utils`:
|
||||||
@@ -290,8 +326,9 @@ Import from `./utils`:
|
|||||||
|
|
||||||
### Waiting Utilities
|
### Waiting Utilities
|
||||||
|
|
||||||
- `waitForNetworkIdle(page)` - Wait for network to be idle
|
- `waitForNetworkIdle(page)` - Wait for page to load (uses `load` state, not `networkidle`)
|
||||||
- `waitForElement(page, testId)` - Wait for element by test ID
|
- `waitForElement(page, testId)` - Wait for element by test ID
|
||||||
|
- `waitForBoardView(page)` - Navigate to board and wait for it to be visible
|
||||||
|
|
||||||
### Async File Verification
|
### Async File Verification
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ test.describe('Project Creation', () => {
|
|||||||
|
|
||||||
await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR });
|
await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR });
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ test.describe('Open Project', () => {
|
|||||||
|
|
||||||
// Navigate to the app
|
// Navigate to the app
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
// Wait for welcome view to be visible
|
// Wait for welcome view to be visible
|
||||||
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Page, Locator } from '@playwright/test';
|
import { Page, Locator } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for the page to reach network idle state
|
* Wait for the page to load
|
||||||
* This is commonly used after navigation or page reload to ensure all network requests have completed
|
* Uses 'load' state instead of 'networkidle' because the app has persistent
|
||||||
|
* connections (websockets/polling) that prevent network from ever being idle.
|
||||||
|
* Tests should wait for specific elements to verify page is ready.
|
||||||
*/
|
*/
|
||||||
export async function waitForNetworkIdle(page: Page): Promise<void> {
|
export async function waitForNetworkIdle(page: Page): Promise<void> {
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ export async function waitForBoardView(page: Page): Promise<void> {
|
|||||||
const currentUrl = page.url();
|
const currentUrl = page.url();
|
||||||
if (!currentUrl.includes('/board')) {
|
if (!currentUrl.includes('/board')) {
|
||||||
await page.goto('/board');
|
await page.goto('/board');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for either board-view (success) or board-view-no-project (store not hydrated yet)
|
// Wait for either board-view (success) or board-view-no-project (store not hydrated yet)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { waitForElement } from '../core/waiting';
|
|||||||
export async function navigateToBoard(page: Page): Promise<void> {
|
export async function navigateToBoard(page: Page): Promise<void> {
|
||||||
// Navigate directly to /board route
|
// Navigate directly to /board route
|
||||||
await page.goto('/board');
|
await page.goto('/board');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
// Wait for the board view to be visible
|
// Wait for the board view to be visible
|
||||||
await waitForElement(page, 'board-view', { timeout: 10000 });
|
await waitForElement(page, 'board-view', { timeout: 10000 });
|
||||||
@@ -22,7 +22,7 @@ export async function navigateToBoard(page: Page): Promise<void> {
|
|||||||
export async function navigateToContext(page: Page): Promise<void> {
|
export async function navigateToContext(page: Page): Promise<void> {
|
||||||
// Navigate directly to /context route
|
// Navigate directly to /context route
|
||||||
await page.goto('/context');
|
await page.goto('/context');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
// Wait for loading to complete (if present)
|
// Wait for loading to complete (if present)
|
||||||
const loadingElement = page.locator('[data-testid="context-view-loading"]');
|
const loadingElement = page.locator('[data-testid="context-view-loading"]');
|
||||||
@@ -47,7 +47,7 @@ export async function navigateToContext(page: Page): Promise<void> {
|
|||||||
export async function navigateToSpec(page: Page): Promise<void> {
|
export async function navigateToSpec(page: Page): Promise<void> {
|
||||||
// Navigate directly to /spec route
|
// Navigate directly to /spec route
|
||||||
await page.goto('/spec');
|
await page.goto('/spec');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
// Wait for loading state to complete first (if present)
|
// Wait for loading state to complete first (if present)
|
||||||
const loadingElement = page.locator('[data-testid="spec-view-loading"]');
|
const loadingElement = page.locator('[data-testid="spec-view-loading"]');
|
||||||
@@ -77,7 +77,7 @@ export async function navigateToSpec(page: Page): Promise<void> {
|
|||||||
export async function navigateToAgent(page: Page): Promise<void> {
|
export async function navigateToAgent(page: Page): Promise<void> {
|
||||||
// Navigate directly to /agent route
|
// Navigate directly to /agent route
|
||||||
await page.goto('/agent');
|
await page.goto('/agent');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
// Wait for the agent view to be visible
|
// Wait for the agent view to be visible
|
||||||
await waitForElement(page, 'agent-view', { timeout: 10000 });
|
await waitForElement(page, 'agent-view', { timeout: 10000 });
|
||||||
@@ -90,7 +90,7 @@ export async function navigateToAgent(page: Page): Promise<void> {
|
|||||||
export async function navigateToSettings(page: Page): Promise<void> {
|
export async function navigateToSettings(page: Page): Promise<void> {
|
||||||
// Navigate directly to /settings route
|
// Navigate directly to /settings route
|
||||||
await page.goto('/settings');
|
await page.goto('/settings');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
// Wait for the settings view to be visible
|
// Wait for the settings view to be visible
|
||||||
await waitForElement(page, 'settings-view', { timeout: 10000 });
|
await waitForElement(page, 'settings-view', { timeout: 10000 });
|
||||||
@@ -105,7 +105,7 @@ export async function navigateToSetup(page: Page): Promise<void> {
|
|||||||
const { setupFirstRun } = await import('../project/setup');
|
const { setupFirstRun } = await import('../project/setup');
|
||||||
await setupFirstRun(page);
|
await setupFirstRun(page);
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
await waitForElement(page, 'setup-view', { timeout: 10000 });
|
await waitForElement(page, 'setup-view', { timeout: 10000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ export async function navigateToSetup(page: Page): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
export async function navigateToWelcome(page: Page): Promise<void> {
|
export async function navigateToWelcome(page: Page): Promise<void> {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('load');
|
||||||
await waitForElement(page, 'welcome-view', { timeout: 10000 });
|
await waitForElement(page, 'welcome-view', { timeout: 10000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
723
index (25).html
Normal file
723
index (25).html
Normal 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>
|
||||||
296
package-lock.json
generated
296
package-lock.json
generated
@@ -100,10 +100,13 @@
|
|||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"@xyflow/react": "^12.10.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"framer-motion": "^12.23.26",
|
||||||
"geist": "^1.5.1",
|
"geist": "^1.5.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
@@ -111,6 +114,7 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
@@ -121,6 +125,7 @@
|
|||||||
"@playwright/test": "^1.57.0",
|
"@playwright/test": "^1.57.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/router-plugin": "^1.141.7",
|
"@tanstack/router-plugin": "^1.141.7",
|
||||||
|
"@types/dagre": "^0.7.53",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -1209,7 +1214,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@electron/node-gyp": {
|
"node_modules/@electron/node-gyp": {
|
||||||
"version": "10.2.0-electron.1",
|
"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==",
|
"integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -5740,6 +5745,62 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-drag": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-selection": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-transition": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-zoom": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/dagre": {
|
||||||
|
"version": "0.7.53",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz",
|
||||||
|
"integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -6529,6 +6590,66 @@
|
|||||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@xyflow/react": {
|
||||||
|
"version": "12.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.0.tgz",
|
||||||
|
"integrity": "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@xyflow/system": "0.0.74",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"zustand": "^4.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xyflow/react/node_modules/zustand": {
|
||||||
|
"version": "4.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||||
|
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16.8",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=16.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xyflow/system": {
|
||||||
|
"version": "0.0.74",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.74.tgz",
|
||||||
|
"integrity": "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-drag": "^3.0.7",
|
||||||
|
"@types/d3-interpolate": "^3.0.4",
|
||||||
|
"@types/d3-selection": "^3.0.10",
|
||||||
|
"@types/d3-transition": "^3.0.8",
|
||||||
|
"@types/d3-zoom": "^3.0.8",
|
||||||
|
"d3-drag": "^3.0.0",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"d3-zoom": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/7zip-bin": {
|
"node_modules/7zip-bin": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
|
||||||
@@ -7651,6 +7772,12 @@
|
|||||||
"url": "https://polar.sh/cva"
|
"url": "https://polar.sh/cva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classcat": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/clean-stack": {
|
"node_modules/clean-stack": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
@@ -8037,6 +8164,121 @@
|
|||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dispatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-drag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-selection": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-selection": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-transition": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-ease": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"d3-selection": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-zoom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-transition": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dagre": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graphlib": "^2.1.8",
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -9472,6 +9714,33 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/fresh": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
@@ -9811,6 +10080,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/graphlib": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -11210,7 +11488,6 @@
|
|||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
@@ -12496,6 +12773,21 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user