mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
refactor: Create global TooltipProvider in app.tsx to eliminate duplication
- Add global TooltipProvider wrapper in app.tsx for entire application - Remove 36 duplicate TooltipProvider instances across 20 UI component files - Clean up imports by removing TooltipProvider from component imports - Follow Radix UI best practices for TooltipProvider placement - Reduce code by 62 lines while maintaining all tooltip functionality Closes #694 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { useSettingsSync } from './hooks/use-settings-sync';
|
|||||||
import { useCursorStatusInit } from './hooks/use-cursor-status-init';
|
import { useCursorStatusInit } from './hooks/use-cursor-status-init';
|
||||||
import { useProviderAuthInit } from './hooks/use-provider-auth-init';
|
import { useProviderAuthInit } from './hooks/use-provider-auth-init';
|
||||||
import { useAppStore } from './store/app-store';
|
import { useAppStore } from './store/app-store';
|
||||||
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import './styles/global.css';
|
import './styles/global.css';
|
||||||
import './styles/theme-imports';
|
import './styles/theme-imports';
|
||||||
import './styles/font-imports';
|
import './styles/font-imports';
|
||||||
@@ -75,9 +76,9 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<TooltipProvider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
{showSplash && !disableSplashScreen && <SplashScreen onComplete={handleSplashComplete} />}
|
{showSplash && !disableSplashScreen && <SplashScreen onComplete={handleSplashComplete} />}
|
||||||
</>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { formatShortcut } from '@/store/app-store';
|
|||||||
import { Activity, Settings, BookOpen, MessageSquare, ExternalLink } from 'lucide-react';
|
import { Activity, Settings, BookOpen, MessageSquare, ExternalLink } from 'lucide-react';
|
||||||
import { useOSDetection } from '@/hooks/use-os-detection';
|
import { useOSDetection } from '@/hooks/use-os-detection';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
function getOSAbbreviation(os: string): string {
|
function getOSAbbreviation(os: string): string {
|
||||||
switch (os) {
|
switch (os) {
|
||||||
@@ -72,68 +72,14 @@ export function SidebarFooter({
|
|||||||
<div className="flex flex-col items-center py-2 px-2 gap-1">
|
<div className="flex flex-col items-center py-2 px-2 gap-1">
|
||||||
{/* Running Agents */}
|
{/* Running Agents */}
|
||||||
{!hideRunningAgents && (
|
{!hideRunningAgents && (
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate({ to: '/running-agents' })}
|
|
||||||
className={cn(
|
|
||||||
'relative flex items-center justify-center w-10 h-10 rounded-xl',
|
|
||||||
'transition-all duration-200 ease-out titlebar-no-drag',
|
|
||||||
isActiveRoute('running-agents')
|
|
||||||
? [
|
|
||||||
'bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10',
|
|
||||||
'text-foreground border border-brand-500/30',
|
|
||||||
'shadow-md shadow-brand-500/10',
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
'text-muted-foreground hover:text-foreground',
|
|
||||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
data-testid="running-agents-link"
|
|
||||||
>
|
|
||||||
<Activity
|
|
||||||
className={cn(
|
|
||||||
'w-[18px] h-[18px]',
|
|
||||||
isActiveRoute('running-agents') && 'text-brand-500'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{runningAgentsCount > 0 && (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'absolute -top-1 -right-1 flex items-center justify-center',
|
|
||||||
'min-w-4 h-4 px-1 text-[9px] font-bold rounded-full',
|
|
||||||
'bg-brand-500 text-white shadow-sm'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{runningAgentsCount > 99 ? '99' : runningAgentsCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
|
||||||
Running Agents
|
|
||||||
{runningAgentsCount > 0 && (
|
|
||||||
<span className="ml-2 px-1.5 py-0.5 bg-brand-500 text-white rounded-full text-[10px]">
|
|
||||||
{runningAgentsCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Settings */}
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate({ to: '/settings' })}
|
onClick={() => navigate({ to: '/running-agents' })}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center w-10 h-10 rounded-xl',
|
'relative flex items-center justify-center w-10 h-10 rounded-xl',
|
||||||
'transition-all duration-200 ease-out titlebar-no-drag',
|
'transition-all duration-200 ease-out titlebar-no-drag',
|
||||||
isActiveRoute('settings')
|
isActiveRoute('running-agents')
|
||||||
? [
|
? [
|
||||||
'bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10',
|
'bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10',
|
||||||
'text-foreground border border-brand-500/30',
|
'text-foreground border border-brand-500/30',
|
||||||
@@ -144,72 +90,115 @@ export function SidebarFooter({
|
|||||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||||
]
|
]
|
||||||
)}
|
)}
|
||||||
data-testid="settings-button"
|
data-testid="running-agents-link"
|
||||||
>
|
>
|
||||||
<Settings
|
<Activity
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-[18px] h-[18px]',
|
'w-[18px] h-[18px]',
|
||||||
isActiveRoute('settings') && 'text-brand-500'
|
isActiveRoute('running-agents') && 'text-brand-500'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{runningAgentsCount > 0 && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'absolute -top-1 -right-1 flex items-center justify-center',
|
||||||
|
'min-w-4 h-4 px-1 text-[9px] font-bold rounded-full',
|
||||||
|
'bg-brand-500 text-white shadow-sm'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{runningAgentsCount > 99 ? '99' : runningAgentsCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
Global Settings
|
Running Agents
|
||||||
<span className="ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
|
{runningAgentsCount > 0 && (
|
||||||
{formatShortcut(shortcuts.settings, true)}
|
<span className="ml-2 px-1.5 py-0.5 bg-brand-500 text-white rounded-full text-[10px]">
|
||||||
</span>
|
{runningAgentsCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
)}
|
||||||
|
|
||||||
|
{/* Settings */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate({ to: '/settings' })}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center w-10 h-10 rounded-xl',
|
||||||
|
'transition-all duration-200 ease-out titlebar-no-drag',
|
||||||
|
isActiveRoute('settings')
|
||||||
|
? [
|
||||||
|
'bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10',
|
||||||
|
'text-foreground border border-brand-500/30',
|
||||||
|
'shadow-md shadow-brand-500/10',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'text-muted-foreground hover:text-foreground',
|
||||||
|
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
data-testid="settings-button"
|
||||||
|
>
|
||||||
|
<Settings
|
||||||
|
className={cn('w-[18px] h-[18px]', isActiveRoute('settings') && 'text-brand-500')}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
Global Settings
|
||||||
|
<span className="ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
|
||||||
|
{formatShortcut(shortcuts.settings, true)}
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{/* Documentation */}
|
{/* Documentation */}
|
||||||
{!hideWiki && (
|
{!hideWiki && (
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
onClick={handleWikiClick}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center justify-center w-10 h-10 rounded-xl',
|
|
||||||
'text-muted-foreground hover:text-foreground',
|
|
||||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
|
||||||
'transition-all duration-200 ease-out titlebar-no-drag'
|
|
||||||
)}
|
|
||||||
data-testid="documentation-button"
|
|
||||||
>
|
|
||||||
<BookOpen className="w-[18px] h-[18px]" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
|
||||||
Documentation
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Feedback */}
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={handleFeedbackClick}
|
onClick={handleWikiClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center w-10 h-10 rounded-xl',
|
'flex items-center justify-center w-10 h-10 rounded-xl',
|
||||||
'text-muted-foreground hover:text-foreground',
|
'text-muted-foreground hover:text-foreground',
|
||||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||||
'transition-all duration-200 ease-out titlebar-no-drag'
|
'transition-all duration-200 ease-out titlebar-no-drag'
|
||||||
)}
|
)}
|
||||||
data-testid="feedback-button"
|
data-testid="documentation-button"
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-[18px] h-[18px]" />
|
<BookOpen className="w-[18px] h-[18px]" />
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
Feedback
|
Documentation
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
)}
|
||||||
|
|
||||||
|
{/* Feedback */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={handleFeedbackClick}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center w-10 h-10 rounded-xl',
|
||||||
|
'text-muted-foreground hover:text-foreground',
|
||||||
|
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||||
|
'transition-all duration-200 ease-out titlebar-no-drag'
|
||||||
|
)}
|
||||||
|
data-testid="feedback-button"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-[18px] h-[18px]" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
Feedback
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
interface SidebarHeaderProps {
|
interface SidebarHeaderProps {
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
@@ -92,78 +92,74 @@ export function SidebarHeader({
|
|||||||
isMac && isElectron() && 'pt-[10px]'
|
isMac && isElectron() && 'pt-[10px]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TooltipProvider delayDuration={0}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<button
|
||||||
<button
|
onClick={handleLogoClick}
|
||||||
onClick={handleLogoClick}
|
className="group flex flex-col items-center"
|
||||||
className="group flex flex-col items-center"
|
data-testid="logo-button"
|
||||||
data-testid="logo-button"
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
role="img"
|
||||||
|
aria-label="Automaker Logo"
|
||||||
|
className="size-8 group-hover:rotate-12 transition-transform duration-300 ease-out"
|
||||||
>
|
>
|
||||||
<svg
|
<defs>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<linearGradient
|
||||||
viewBox="0 0 256 256"
|
id="bg-collapsed"
|
||||||
role="img"
|
x1="0"
|
||||||
aria-label="Automaker Logo"
|
y1="0"
|
||||||
className="size-8 group-hover:rotate-12 transition-transform duration-300 ease-out"
|
x2="256"
|
||||||
>
|
y2="256"
|
||||||
<defs>
|
gradientUnits="userSpaceOnUse"
|
||||||
<linearGradient
|
|
||||||
id="bg-collapsed"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="256"
|
|
||||||
y2="256"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop offset="0%" style={{ stopColor: 'var(--brand-400)' }} />
|
|
||||||
<stop offset="100%" style={{ stopColor: 'var(--brand-600)' }} />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#bg-collapsed)" />
|
|
||||||
<g
|
|
||||||
fill="none"
|
|
||||||
stroke="#FFFFFF"
|
|
||||||
strokeWidth="20"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
>
|
||||||
<path d="M92 92 L52 128 L92 164" />
|
<stop offset="0%" style={{ stopColor: 'var(--brand-400)' }} />
|
||||||
<path d="M144 72 L116 184" />
|
<stop offset="100%" style={{ stopColor: 'var(--brand-600)' }} />
|
||||||
<path d="M164 92 L204 128 L164 164" />
|
</linearGradient>
|
||||||
</g>
|
</defs>
|
||||||
</svg>
|
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#bg-collapsed)" />
|
||||||
</button>
|
<g
|
||||||
</TooltipTrigger>
|
fill="none"
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
stroke="#FFFFFF"
|
||||||
Go to Dashboard
|
strokeWidth="20"
|
||||||
</TooltipContent>
|
strokeLinecap="round"
|
||||||
</Tooltip>
|
strokeLinejoin="round"
|
||||||
</TooltipProvider>
|
>
|
||||||
|
<path d="M92 92 L52 128 L92 164" />
|
||||||
|
<path d="M144 72 L116 184" />
|
||||||
|
<path d="M164 92 L204 128 L164 164" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
Go to Dashboard
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{/* Collapsed project icon with dropdown */}
|
{/* Collapsed project icon with dropdown */}
|
||||||
{currentProject && (
|
{currentProject && (
|
||||||
<>
|
<>
|
||||||
<div className="w-full h-px bg-border/40 my-2" />
|
<div className="w-full h-px bg-border/40 my-2" />
|
||||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<button
|
||||||
<button
|
onContextMenu={(e) => onProjectContextMenu(currentProject, e)}
|
||||||
onContextMenu={(e) => onProjectContextMenu(currentProject, e)}
|
className="p-1 rounded-lg hover:bg-accent/50 transition-colors"
|
||||||
className="p-1 rounded-lg hover:bg-accent/50 transition-colors"
|
data-testid="collapsed-project-button"
|
||||||
data-testid="collapsed-project-button"
|
>
|
||||||
>
|
{renderProjectIcon(currentProject)}
|
||||||
{renderProjectIcon(currentProject)}
|
</button>
|
||||||
</button>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
{currentProject.name}
|
||||||
{currentProject.name}
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="start"
|
align="start"
|
||||||
side="right"
|
side="right"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
// Map section labels to icons
|
// Map section labels to icons
|
||||||
const sectionIcons: Record<string, React.ComponentType<{ className?: string }>> = {
|
const sectionIcons: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||||
@@ -158,27 +158,25 @@ export function SidebarNavigation({
|
|||||||
{/* Section icon with dropdown (collapsed sidebar) */}
|
{/* Section icon with dropdown (collapsed sidebar) */}
|
||||||
{section.label && !sidebarOpen && SectionIcon && section.collapsible && isCollapsed && (
|
{section.label && !sidebarOpen && SectionIcon && section.collapsible && isCollapsed && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<TooltipProvider delayDuration={0}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<button
|
||||||
<button
|
className={cn(
|
||||||
className={cn(
|
'group flex items-center justify-center w-full py-2 rounded-lg',
|
||||||
'group flex items-center justify-center w-full py-2 rounded-lg',
|
'text-muted-foreground hover:text-foreground',
|
||||||
'text-muted-foreground hover:text-foreground',
|
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
'transition-all duration-200 ease-out'
|
||||||
'transition-all duration-200 ease-out'
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<SectionIcon className="w-[18px] h-[18px]" />
|
||||||
<SectionIcon className="w-[18px] h-[18px]" />
|
</button>
|
||||||
</button>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
{section.label}
|
||||||
{section.label}
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<DropdownMenuContent side="right" align="start" sideOffset={8} className="w-48">
|
<DropdownMenuContent side="right" align="start" sideOffset={8} className="w-48">
|
||||||
{section.items.map((item) => {
|
{section.items.map((item) => {
|
||||||
const ItemIcon = item.icon;
|
const ItemIcon = item.icon;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@/store/app-store';
|
} from '@/store/app-store';
|
||||||
import type { KeyboardShortcuts } from '@/store/app-store';
|
import type { KeyboardShortcuts } from '@/store/app-store';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CheckCircle2, X, RotateCcw, Edit2 } from 'lucide-react';
|
import { CheckCircle2, X, RotateCcw, Edit2 } from 'lucide-react';
|
||||||
@@ -305,54 +305,52 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<div className={cn('space-y-4', className)} data-testid="keyboard-map">
|
||||||
<div className={cn('space-y-4', className)} data-testid="keyboard-map">
|
{/* Legend */}
|
||||||
{/* Legend */}
|
<div className="flex flex-wrap gap-4 justify-center text-xs">
|
||||||
<div className="flex flex-wrap gap-4 justify-center text-xs">
|
{Object.entries(CATEGORY_COLORS).map(([key, colors]) => (
|
||||||
{Object.entries(CATEGORY_COLORS).map(([key, colors]) => (
|
<div key={key} className="flex items-center gap-2">
|
||||||
<div key={key} className="flex items-center gap-2">
|
<div className={cn('w-4 h-4 rounded border', colors.bg, colors.border)} />
|
||||||
<div className={cn('w-4 h-4 rounded border', colors.bg, colors.border)} />
|
<span className={colors.text}>{colors.label}</span>
|
||||||
<span className={colors.text}>{colors.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 rounded bg-sidebar-accent/10 border border-sidebar-border" />
|
|
||||||
<span className="text-muted-foreground">Available</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-yellow-500" />
|
|
||||||
<span className="text-yellow-400">Modified</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded bg-sidebar-accent/10 border border-sidebar-border" />
|
||||||
|
<span className="text-muted-foreground">Available</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
{/* Keyboard layout */}
|
<div className="w-2 h-2 rounded-full bg-yellow-500" />
|
||||||
<div className="flex flex-col items-center gap-1.5 p-4 rounded-xl bg-sidebar-accent/5 border border-sidebar-border">
|
<span className="text-yellow-400">Modified</span>
|
||||||
{KEYBOARD_ROWS.map((row, rowIndex) => (
|
|
||||||
<div key={rowIndex} className="flex gap-1.5 justify-center">
|
|
||||||
{row.map(renderKey)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="flex justify-center gap-6 text-xs text-muted-foreground">
|
|
||||||
<span>
|
|
||||||
<strong className="text-foreground">{Object.keys(keyboardShortcuts).length}</strong>{' '}
|
|
||||||
shortcuts configured
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong className="text-foreground">{Object.keys(keyToShortcuts).length}</strong> keys
|
|
||||||
in use
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong className="text-foreground">
|
|
||||||
{KEYBOARD_ROWS.flat().length - Object.keys(keyToShortcuts).length}
|
|
||||||
</strong>{' '}
|
|
||||||
keys available
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
|
||||||
|
{/* Keyboard layout */}
|
||||||
|
<div className="flex flex-col items-center gap-1.5 p-4 rounded-xl bg-sidebar-accent/5 border border-sidebar-border">
|
||||||
|
{KEYBOARD_ROWS.map((row, rowIndex) => (
|
||||||
|
<div key={rowIndex} className="flex gap-1.5 justify-center">
|
||||||
|
{row.map(renderKey)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="flex justify-center gap-6 text-xs text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
<strong className="text-foreground">{Object.keys(keyboardShortcuts).length}</strong>{' '}
|
||||||
|
shortcuts configured
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong className="text-foreground">{Object.keys(keyToShortcuts).length}</strong> keys in
|
||||||
|
use
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong className="text-foreground">
|
||||||
|
{KEYBOARD_ROWS.flat().length - Object.keys(keyToShortcuts).length}
|
||||||
|
</strong>{' '}
|
||||||
|
keys available
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,196 +506,194 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<div className="space-y-4" data-testid="shortcut-reference-panel">
|
||||||
<div className="space-y-4" data-testid="shortcut-reference-panel">
|
{editable && (
|
||||||
{editable && (
|
<div className="flex justify-end">
|
||||||
<div className="flex justify-end">
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => resetKeyboardShortcuts()}
|
||||||
onClick={() => resetKeyboardShortcuts()}
|
className="gap-2 text-xs"
|
||||||
className="gap-2 text-xs"
|
data-testid="reset-all-shortcuts-button"
|
||||||
data-testid="reset-all-shortcuts-button"
|
>
|
||||||
>
|
<RotateCcw className="w-3 h-3" />
|
||||||
<RotateCcw className="w-3 h-3" />
|
Reset All to Defaults
|
||||||
Reset All to Defaults
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{Object.entries(groupedShortcuts).map(([category, shortcuts]) => {
|
||||||
{Object.entries(groupedShortcuts).map(([category, shortcuts]) => {
|
const colors = CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS];
|
||||||
const colors = CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS];
|
return (
|
||||||
return (
|
<div key={category} className="space-y-2">
|
||||||
<div key={category} className="space-y-2">
|
<h4 className={cn('text-sm font-semibold', colors.text)}>{colors.label}</h4>
|
||||||
<h4 className={cn('text-sm font-semibold', colors.text)}>{colors.label}</h4>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
{shortcuts.map(({ key, label, value }) => {
|
||||||
{shortcuts.map(({ key, label, value }) => {
|
const isModified = mergedShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key];
|
||||||
const isModified = mergedShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key];
|
const isEditing = editingShortcut === key;
|
||||||
const isEditing = editingShortcut === key;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-between p-2 rounded-lg bg-sidebar-accent/10 border transition-colors',
|
'flex items-center justify-between p-2 rounded-lg bg-sidebar-accent/10 border transition-colors',
|
||||||
isEditing ? 'border-brand-500' : 'border-sidebar-border',
|
isEditing ? 'border-brand-500' : 'border-sidebar-border',
|
||||||
editable && !isEditing && 'hover:bg-sidebar-accent/20 cursor-pointer'
|
editable && !isEditing && 'hover:bg-sidebar-accent/20 cursor-pointer'
|
||||||
)}
|
)}
|
||||||
onClick={() => editable && !isEditing && handleStartEdit(key)}
|
onClick={() => editable && !isEditing && handleStartEdit(key)}
|
||||||
data-testid={`shortcut-row-${key}`}
|
data-testid={`shortcut-row-${key}`}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-foreground">{label}</span>
|
<span className="text-sm text-foreground">{label}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Modifier checkboxes */}
|
{/* Modifier checkboxes */}
|
||||||
<div className="flex items-center gap-1.5 text-xs">
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`mod-cmd-${key}`}
|
id={`mod-cmd-${key}`}
|
||||||
checked={modifiers.cmdCtrl}
|
checked={modifiers.cmdCtrl}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleModifierChange('cmdCtrl', !!checked, key)
|
handleModifierChange('cmdCtrl', !!checked, key)
|
||||||
}
|
}
|
||||||
className="h-3.5 w-3.5"
|
className="h-3.5 w-3.5"
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`mod-cmd-${key}`}
|
htmlFor={`mod-cmd-${key}`}
|
||||||
className="text-xs text-muted-foreground cursor-pointer"
|
className="text-xs text-muted-foreground cursor-pointer"
|
||||||
>
|
>
|
||||||
{isMac ? '⌘' : 'Ctrl'}
|
{isMac ? '⌘' : 'Ctrl'}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`mod-alt-${key}`}
|
id={`mod-alt-${key}`}
|
||||||
checked={modifiers.alt}
|
checked={modifiers.alt}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleModifierChange('alt', !!checked, key)
|
handleModifierChange('alt', !!checked, key)
|
||||||
}
|
}
|
||||||
className="h-3.5 w-3.5"
|
className="h-3.5 w-3.5"
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`mod-alt-${key}`}
|
htmlFor={`mod-alt-${key}`}
|
||||||
className="text-xs text-muted-foreground cursor-pointer"
|
className="text-xs text-muted-foreground cursor-pointer"
|
||||||
>
|
>
|
||||||
{isMac ? '⌥' : 'Alt'}
|
{isMac ? '⌥' : 'Alt'}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`mod-shift-${key}`}
|
id={`mod-shift-${key}`}
|
||||||
checked={modifiers.shift}
|
checked={modifiers.shift}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleModifierChange('shift', !!checked, key)
|
handleModifierChange('shift', !!checked, key)
|
||||||
}
|
}
|
||||||
className="h-3.5 w-3.5"
|
className="h-3.5 w-3.5"
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`mod-shift-${key}`}
|
htmlFor={`mod-shift-${key}`}
|
||||||
className="text-xs text-muted-foreground cursor-pointer"
|
className="text-xs text-muted-foreground cursor-pointer"
|
||||||
>
|
>
|
||||||
⇧
|
⇧
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground">+</span>
|
|
||||||
<Input
|
|
||||||
value={keyValue}
|
|
||||||
onChange={(e) => handleKeyChange(e.target.value, key)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
className={cn(
|
|
||||||
'w-12 h-7 text-center font-mono text-xs uppercase',
|
|
||||||
shortcutError && 'border-red-500 focus-visible:ring-red-500'
|
|
||||||
)}
|
|
||||||
placeholder="Key"
|
|
||||||
maxLength={1}
|
|
||||||
autoFocus
|
|
||||||
data-testid={`edit-shortcut-input-${key}`}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-7 w-7 p-0 hover:bg-green-500/20 hover:text-green-400"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleSaveShortcut();
|
|
||||||
}}
|
|
||||||
disabled={!!shortcutError || !keyValue}
|
|
||||||
data-testid={`save-shortcut-${key}`}
|
|
||||||
>
|
|
||||||
<CheckCircle2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-7 w-7 p-0 hover:bg-red-500/20 hover:text-red-400"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCancelEdit();
|
|
||||||
}}
|
|
||||||
data-testid={`cancel-shortcut-${key}`}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<span className="text-muted-foreground">+</span>
|
||||||
<>
|
<Input
|
||||||
<kbd
|
value={keyValue}
|
||||||
className={cn(
|
onChange={(e) => handleKeyChange(e.target.value, key)}
|
||||||
'px-2 py-1 text-xs font-mono rounded border',
|
onKeyDown={handleKeyDown}
|
||||||
colors.bg,
|
className={cn(
|
||||||
colors.border,
|
'w-12 h-7 text-center font-mono text-xs uppercase',
|
||||||
colors.text
|
shortcutError && 'border-red-500 focus-visible:ring-red-500'
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatShortcut(value, true)}
|
|
||||||
</kbd>
|
|
||||||
{isModified && editable && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-yellow-500/20 hover:text-yellow-400"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleResetShortcut(key);
|
|
||||||
}}
|
|
||||||
data-testid={`reset-shortcut-${key}`}
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top">
|
|
||||||
Reset to default ({DEFAULT_KEYBOARD_SHORTCUTS[key]})
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
{isModified && !editable && (
|
placeholder="Key"
|
||||||
<span className="w-2 h-2 rounded-full bg-yellow-500" />
|
maxLength={1}
|
||||||
|
autoFocus
|
||||||
|
data-testid={`edit-shortcut-input-${key}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 p-0 hover:bg-green-500/20 hover:text-green-400"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSaveShortcut();
|
||||||
|
}}
|
||||||
|
disabled={!!shortcutError || !keyValue}
|
||||||
|
data-testid={`save-shortcut-${key}`}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 p-0 hover:bg-red-500/20 hover:text-red-400"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCancelEdit();
|
||||||
|
}}
|
||||||
|
data-testid={`cancel-shortcut-${key}`}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<kbd
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 text-xs font-mono rounded border',
|
||||||
|
colors.bg,
|
||||||
|
colors.border,
|
||||||
|
colors.text
|
||||||
)}
|
)}
|
||||||
{editable && !isModified && (
|
>
|
||||||
<Edit2 className="w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100" />
|
{formatShortcut(value, true)}
|
||||||
)}
|
</kbd>
|
||||||
</>
|
{isModified && editable && (
|
||||||
)}
|
<Tooltip>
|
||||||
</div>
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-yellow-500/20 hover:text-yellow-400"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleResetShortcut(key);
|
||||||
|
}}
|
||||||
|
data-testid={`reset-shortcut-${key}`}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
Reset to default ({DEFAULT_KEYBOARD_SHORTCUTS[key]})
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{isModified && !editable && (
|
||||||
|
<span className="w-2 h-2 rounded-full bg-yellow-500" />
|
||||||
|
)}
|
||||||
|
{editable && !isModified && (
|
||||||
|
<Edit2 className="w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
</div>
|
})}
|
||||||
{editingShortcut &&
|
|
||||||
shortcutError &&
|
|
||||||
SHORTCUT_CATEGORIES[editingShortcut] === category && (
|
|
||||||
<p className="text-xs text-red-400 mt-1">{shortcutError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
{editingShortcut &&
|
||||||
})}
|
shortcutError &&
|
||||||
</div>
|
SHORTCUT_CATEGORIES[editingShortcut] === category && (
|
||||||
</TooltipProvider>
|
<p className="text-xs text-red-400 mt-1">{shortcutError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { ImageIcon } from 'lucide-react';
|
import { ImageIcon } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -18,24 +18,22 @@ export function BoardControls({ isMounted, onShowBoardBackground }: BoardControl
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
{/* Board Background Button */}
|
||||||
{/* Board Background Button */}
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<button
|
||||||
<button
|
onClick={onShowBoardBackground}
|
||||||
onClick={onShowBoardBackground}
|
className={buttonClass}
|
||||||
className={buttonClass}
|
data-testid="board-background-button"
|
||||||
data-testid="board-background-button"
|
>
|
||||||
>
|
<ImageIcon className="w-4 h-4" />
|
||||||
<ImageIcon className="w-4 h-4" />
|
</button>
|
||||||
</button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>Board Background Settings</p>
|
||||||
<p>Board Background Settings</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
</div>
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { memo, useEffect, useMemo, useState } from 'react';
|
import { memo, useEffect, useMemo, useState } from 'react';
|
||||||
import { Feature, useAppStore } from '@/store/app-store';
|
import { Feature, useAppStore } from '@/store/app-store';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { AlertCircle, Lock, Hand, Sparkles, SkipForward } from 'lucide-react';
|
import { AlertCircle, Lock, Hand, Sparkles, SkipForward } from 'lucide-react';
|
||||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
@@ -28,24 +28,22 @@ export const CardBadges = memo(function CardBadges({ feature }: CardBadgesProps)
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-1.5 px-3 pt-1.5 min-h-[24px]">
|
<div className="flex flex-wrap items-center gap-1.5 px-3 pt-1.5 min-h-[24px]">
|
||||||
{/* Error badge */}
|
{/* Error badge */}
|
||||||
<TooltipProvider delayDuration={200}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
uniformBadgeClass,
|
||||||
uniformBadgeClass,
|
'bg-[var(--status-error-bg)] border-[var(--status-error)]/40 text-[var(--status-error)]'
|
||||||
'bg-[var(--status-error-bg)] border-[var(--status-error)]/40 text-[var(--status-error)]'
|
)}
|
||||||
)}
|
data-testid={`error-badge-${feature.id}`}
|
||||||
data-testid={`error-badge-${feature.id}`}
|
>
|
||||||
>
|
<AlertCircle className="w-3.5 h-3.5" />
|
||||||
<AlertCircle className="w-3.5 h-3.5" />
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
|
||||||
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
|
<p>{feature.error}</p>
|
||||||
<p>{feature.error}</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -138,147 +136,137 @@ export const PriorityBadges = memo(function PriorityBadges({
|
|||||||
<div className="absolute top-2 left-2 flex items-center gap-1">
|
<div className="absolute top-2 left-2 flex items-center gap-1">
|
||||||
{/* Priority badge */}
|
{/* Priority badge */}
|
||||||
{feature.priority && (
|
{feature.priority && (
|
||||||
<TooltipProvider delayDuration={200}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
uniformBadgeClass,
|
||||||
uniformBadgeClass,
|
feature.priority === 1 &&
|
||||||
feature.priority === 1 &&
|
'bg-[var(--status-error-bg)] border-[var(--status-error)]/40 text-[var(--status-error)]',
|
||||||
'bg-[var(--status-error-bg)] border-[var(--status-error)]/40 text-[var(--status-error)]',
|
feature.priority === 2 &&
|
||||||
feature.priority === 2 &&
|
'bg-[var(--status-warning-bg)] border-[var(--status-warning)]/40 text-[var(--status-warning)]',
|
||||||
'bg-[var(--status-warning-bg)] border-[var(--status-warning)]/40 text-[var(--status-warning)]',
|
feature.priority === 3 &&
|
||||||
feature.priority === 3 &&
|
'bg-[var(--status-info-bg)] border-[var(--status-info)]/40 text-[var(--status-info)]'
|
||||||
'bg-[var(--status-info-bg)] border-[var(--status-info)]/40 text-[var(--status-info)]'
|
)}
|
||||||
)}
|
data-testid={`priority-badge-${feature.id}`}
|
||||||
data-testid={`priority-badge-${feature.id}`}
|
>
|
||||||
>
|
<span className="font-bold text-xs">
|
||||||
<span className="font-bold text-xs">
|
{feature.priority === 1 ? 'H' : feature.priority === 2 ? 'M' : 'L'}
|
||||||
{feature.priority === 1 ? 'H' : feature.priority === 2 ? 'M' : 'L'}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="bottom" className="text-xs">
|
||||||
<TooltipContent side="bottom" className="text-xs">
|
<p>
|
||||||
<p>
|
{feature.priority === 1
|
||||||
{feature.priority === 1
|
? 'High Priority'
|
||||||
? 'High Priority'
|
: feature.priority === 2
|
||||||
: feature.priority === 2
|
? 'Medium Priority'
|
||||||
? 'Medium Priority'
|
: 'Low Priority'}
|
||||||
: 'Low Priority'}
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Manual verification badge */}
|
{/* Manual verification badge */}
|
||||||
{showManualVerification && (
|
{showManualVerification && (
|
||||||
<TooltipProvider delayDuration={200}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
uniformBadgeClass,
|
||||||
uniformBadgeClass,
|
'bg-[var(--status-warning-bg)] border-[var(--status-warning)]/40 text-[var(--status-warning)]'
|
||||||
'bg-[var(--status-warning-bg)] border-[var(--status-warning)]/40 text-[var(--status-warning)]'
|
)}
|
||||||
)}
|
data-testid={`skip-tests-badge-${feature.id}`}
|
||||||
data-testid={`skip-tests-badge-${feature.id}`}
|
>
|
||||||
>
|
<Hand className="w-3.5 h-3.5" />
|
||||||
<Hand className="w-3.5 h-3.5" />
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="bottom" className="text-xs">
|
||||||
<TooltipContent side="bottom" className="text-xs">
|
<p>Manual verification required</p>
|
||||||
<p>Manual verification required</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Blocked badge */}
|
{/* Blocked badge */}
|
||||||
{isBlocked && (
|
{isBlocked && (
|
||||||
<TooltipProvider delayDuration={200}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
uniformBadgeClass,
|
||||||
uniformBadgeClass,
|
'bg-orange-500/20 border-orange-500/50 text-orange-500'
|
||||||
'bg-orange-500/20 border-orange-500/50 text-orange-500'
|
)}
|
||||||
)}
|
data-testid={`blocked-badge-${feature.id}`}
|
||||||
data-testid={`blocked-badge-${feature.id}`}
|
>
|
||||||
>
|
<Lock className="w-3.5 h-3.5" />
|
||||||
<Lock className="w-3.5 h-3.5" />
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
|
||||||
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
|
<p className="font-medium mb-1">
|
||||||
<p className="font-medium mb-1">
|
Blocked by {blockingDependencies.length} incomplete{' '}
|
||||||
Blocked by {blockingDependencies.length} incomplete{' '}
|
{blockingDependencies.length === 1 ? 'dependency' : 'dependencies'}
|
||||||
{blockingDependencies.length === 1 ? 'dependency' : 'dependencies'}
|
</p>
|
||||||
</p>
|
<p className="text-muted-foreground">
|
||||||
<p className="text-muted-foreground">
|
{blockingDependencies
|
||||||
{blockingDependencies
|
.map((depId) => {
|
||||||
.map((depId) => {
|
const dep = features.find((f) => f.id === depId);
|
||||||
const dep = features.find((f) => f.id === depId);
|
return dep?.description || depId;
|
||||||
return dep?.description || depId;
|
})
|
||||||
})
|
.join(', ')}
|
||||||
.join(', ')}
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Just Finished badge */}
|
{/* Just Finished badge */}
|
||||||
{isJustFinished && (
|
{isJustFinished && (
|
||||||
<TooltipProvider delayDuration={200}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
uniformBadgeClass,
|
||||||
uniformBadgeClass,
|
'bg-[var(--status-success-bg)] border-[var(--status-success)]/40 text-[var(--status-success)] animate-pulse'
|
||||||
'bg-[var(--status-success-bg)] border-[var(--status-success)]/40 text-[var(--status-success)] animate-pulse'
|
)}
|
||||||
)}
|
data-testid={`just-finished-badge-${feature.id}`}
|
||||||
data-testid={`just-finished-badge-${feature.id}`}
|
>
|
||||||
>
|
<Sparkles className="w-3.5 h-3.5" />
|
||||||
<Sparkles className="w-3.5 h-3.5" />
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="bottom" className="text-xs">
|
||||||
<TooltipContent side="bottom" className="text-xs">
|
<p>Agent just finished working on this feature</p>
|
||||||
<p>Agent just finished working on this feature</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pipeline exclusion badge */}
|
{/* Pipeline exclusion badge */}
|
||||||
{hasPipelineExclusions && (
|
{hasPipelineExclusions && (
|
||||||
<TooltipProvider delayDuration={200}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
uniformBadgeClass,
|
||||||
uniformBadgeClass,
|
allPipelinesExcluded
|
||||||
allPipelinesExcluded
|
? 'bg-violet-500/20 border-violet-500/50 text-violet-500'
|
||||||
? 'bg-violet-500/20 border-violet-500/50 text-violet-500'
|
: 'bg-violet-500/10 border-violet-500/30 text-violet-400'
|
||||||
: 'bg-violet-500/10 border-violet-500/30 text-violet-400'
|
)}
|
||||||
)}
|
data-testid={`pipeline-exclusion-badge-${feature.id}`}
|
||||||
data-testid={`pipeline-exclusion-badge-${feature.id}`}
|
>
|
||||||
>
|
<SkipForward className="w-3.5 h-3.5" />
|
||||||
<SkipForward className="w-3.5 h-3.5" />
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
|
||||||
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
|
<p className="font-medium mb-1">
|
||||||
<p className="font-medium mb-1">
|
{allPipelinesExcluded
|
||||||
{allPipelinesExcluded
|
? 'All pipelines skipped'
|
||||||
? 'All pipelines skipped'
|
: `${excludedStepCount} of ${totalPipelineSteps} pipeline${totalPipelineSteps !== 1 ? 's' : ''} skipped`}
|
||||||
: `${excludedStepCount} of ${totalPipelineSteps} pipeline${totalPipelineSteps !== 1 ? 's' : ''} skipped`}
|
</p>
|
||||||
</p>
|
<p className="text-muted-foreground">
|
||||||
<p className="text-muted-foreground">
|
{allPipelinesExcluded
|
||||||
{allPipelinesExcluded
|
? 'This feature will skip all custom pipeline steps'
|
||||||
? 'This feature will skip all custom pipeline steps'
|
: 'Some custom pipeline steps will be skipped for this feature'}
|
||||||
: 'Some custom pipeline steps will be skipped for this feature'}
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { memo, useCallback, useState, useEffect } from 'react';
|
import { memo, useCallback, useState, useEffect } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { AlertCircle, Lock, Hand, Sparkles, FileText } from 'lucide-react';
|
import { AlertCircle, Lock, Hand, Sparkles, FileText } from 'lucide-react';
|
||||||
import type { Feature } from '@/store/app-store';
|
import type { Feature } from '@/store/app-store';
|
||||||
import { RowActions, type RowActionHandlers } from './row-actions';
|
import { RowActions, type RowActionHandlers } from './row-actions';
|
||||||
@@ -149,29 +149,27 @@ const IndicatorBadges = memo(function IndicatorBadges({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 ml-2">
|
<div className="flex items-center gap-1 ml-2">
|
||||||
<TooltipProvider delayDuration={200}>
|
{badges.map((badge) => (
|
||||||
{badges.map((badge) => (
|
<Tooltip key={badge.key}>
|
||||||
<Tooltip key={badge.key}>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
'inline-flex items-center justify-center w-5 h-5 rounded border',
|
||||||
'inline-flex items-center justify-center w-5 h-5 rounded border',
|
badge.colorClass,
|
||||||
badge.colorClass,
|
badge.bgClass,
|
||||||
badge.bgClass,
|
badge.borderClass,
|
||||||
badge.borderClass,
|
badge.animate && 'animate-pulse'
|
||||||
badge.animate && 'animate-pulse'
|
)}
|
||||||
)}
|
data-testid={`list-row-badge-${badge.key}`}
|
||||||
data-testid={`list-row-badge-${badge.key}`}
|
>
|
||||||
>
|
<badge.icon className="w-3 h-3" />
|
||||||
<badge.icon className="w-3 h-3" />
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="top" className="text-xs max-w-[250px]">
|
||||||
<TooltipContent side="top" className="text-xs max-w-[250px]">
|
<p>{badge.tooltip}</p>
|
||||||
<p>{badge.tooltip}</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
))}
|
||||||
))}
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import {
|
|||||||
} from '../shared';
|
} from '../shared';
|
||||||
import type { WorkMode } from '../shared';
|
import type { WorkMode } from '../shared';
|
||||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import {
|
import {
|
||||||
getAncestors,
|
getAncestors,
|
||||||
formatAncestorContextForPrompt,
|
formatAncestorContextForPrompt,
|
||||||
@@ -528,26 +528,24 @@ export function AddFeatureDialog({
|
|||||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||||
<span>AI & Execution</span>
|
<span>AI & Execution</span>
|
||||||
</div>
|
</div>
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => {
|
||||||
onClick={() => {
|
onOpenChange(false);
|
||||||
onOpenChange(false);
|
navigate({ to: '/settings', search: { view: 'defaults' } });
|
||||||
navigate({ to: '/settings', search: { view: 'defaults' } });
|
}}
|
||||||
}}
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
>
|
||||||
>
|
<Settings2 className="w-3.5 h-3.5" />
|
||||||
<Settings2 className="w-3.5 h-3.5" />
|
<span>Edit Defaults</span>
|
||||||
<span>Edit Defaults</span>
|
</button>
|
||||||
</button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>Change default model and planning settings for new features</p>
|
||||||
<p>Change default model and planning settings for new features</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@@ -578,24 +576,22 @@ export function AddFeatureDialog({
|
|||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div>
|
||||||
<div>
|
<PlanningModeSelect
|
||||||
<PlanningModeSelect
|
mode="skip"
|
||||||
mode="skip"
|
onModeChange={() => {}}
|
||||||
onModeChange={() => {}}
|
testIdPrefix="add-feature-planning"
|
||||||
testIdPrefix="add-feature-planning"
|
compact
|
||||||
compact
|
disabled
|
||||||
disabled
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>Planning modes are only available for Claude Provider</p>
|
||||||
<p>Planning modes are only available for Claude Provider</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import {
|
|||||||
} from '../shared';
|
} from '../shared';
|
||||||
import type { WorkMode } from '../shared';
|
import type { WorkMode } from '../shared';
|
||||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
||||||
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
|
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
|
||||||
|
|
||||||
@@ -420,26 +420,24 @@ export function EditFeatureDialog({
|
|||||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||||
<span>AI & Execution</span>
|
<span>AI & Execution</span>
|
||||||
</div>
|
</div>
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => {
|
||||||
onClick={() => {
|
onClose();
|
||||||
onClose();
|
navigate({ to: '/settings', search: { view: 'defaults' } });
|
||||||
navigate({ to: '/settings', search: { view: 'defaults' } });
|
}}
|
||||||
}}
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
>
|
||||||
>
|
<Settings2 className="w-3.5 h-3.5" />
|
||||||
<Settings2 className="w-3.5 h-3.5" />
|
<span>Edit Defaults</span>
|
||||||
<span>Edit Defaults</span>
|
</button>
|
||||||
</button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>Change default model and planning settings for new features</p>
|
||||||
<p>Change default model and planning settings for new features</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@@ -470,24 +468,22 @@ export function EditFeatureDialog({
|
|||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div>
|
||||||
<div>
|
<PlanningModeSelect
|
||||||
<PlanningModeSelect
|
mode="skip"
|
||||||
mode="skip"
|
onModeChange={() => {}}
|
||||||
onModeChange={() => {}}
|
testIdPrefix="edit-feature-planning"
|
||||||
testIdPrefix="edit-feature-planning"
|
compact
|
||||||
compact
|
disabled
|
||||||
disabled
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>Planning modes are only available for Claude Provider</p>
|
||||||
<p>Planning modes are only available for Claude Provider</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import type { WorkMode } from '../shared';
|
|||||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||||
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
|
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
interface MassEditDialogProps {
|
interface MassEditDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -302,37 +302,35 @@ export function MassEditDialog({
|
|||||||
/>
|
/>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
) : (
|
) : (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
'p-3 rounded-lg border transition-colors border-border bg-muted/20 opacity-50 cursor-not-allowed'
|
||||||
'p-3 rounded-lg border transition-colors border-border bg-muted/20 opacity-50 cursor-not-allowed'
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Checkbox checked={false} disabled className="opacity-50" />
|
||||||
<Checkbox checked={false} disabled className="opacity-50" />
|
<Label className="text-sm font-medium text-muted-foreground">
|
||||||
<Label className="text-sm font-medium text-muted-foreground">
|
Planning Mode
|
||||||
Planning Mode
|
</Label>
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="opacity-50 pointer-events-none">
|
|
||||||
<PlanningModeSelect
|
|
||||||
mode="skip"
|
|
||||||
onModeChange={() => {}}
|
|
||||||
testIdPrefix="mass-edit-planning"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
<div className="opacity-50 pointer-events-none">
|
||||||
<TooltipContent>
|
<PlanningModeSelect
|
||||||
<p>Planning modes are only available for Claude Provider</p>
|
mode="skip"
|
||||||
</TooltipContent>
|
onModeChange={() => {}}
|
||||||
</Tooltip>
|
testIdPrefix="mass-edit-planning"
|
||||||
</TooltipProvider>
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Planning modes are only available for Claude Provider</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Priority */}
|
{/* Priority */}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { KanbanColumn, KanbanCard, EmptyStateCard } from './components';
|
import { KanbanColumn, KanbanCard, EmptyStateCard } from './components';
|
||||||
import { Feature, useAppStore, formatShortcut } from '@/store/app-store';
|
import { Feature, useAppStore, formatShortcut } from '@/store/app-store';
|
||||||
import { Archive, Settings2, CheckSquare, GripVertical, Plus, CheckCircle2 } from 'lucide-react';
|
import { Archive, Settings2, CheckSquare, GripVertical, Plus, CheckCircle2 } from 'lucide-react';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
||||||
import { getColumnsWithPipeline, type ColumnId } from './constants';
|
import { getColumnsWithPipeline, type ColumnId } from './constants';
|
||||||
import type { PipelineConfig } from '@automaker/types';
|
import type { PipelineConfig } from '@automaker/types';
|
||||||
@@ -358,49 +358,47 @@ export function KanbanBoard({
|
|||||||
contentClassName="perf-contain"
|
contentClassName="perf-contain"
|
||||||
headerAction={
|
headerAction={
|
||||||
column.id === 'verified' ? (
|
column.id === 'verified' ? (
|
||||||
<TooltipProvider>
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-1">
|
{columnFeatures.length > 0 && (
|
||||||
{columnFeatures.length > 0 && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={onArchiveAllVerified}
|
|
||||||
data-testid="archive-all-verified-button"
|
|
||||||
>
|
|
||||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Complete All</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 w-6 p-0 relative"
|
className="h-6 w-6 p-0"
|
||||||
onClick={onShowCompletedModal}
|
onClick={onArchiveAllVerified}
|
||||||
data-testid="completed-features-button"
|
data-testid="archive-all-verified-button"
|
||||||
>
|
>
|
||||||
<Archive className="w-3.5 h-3.5 text-muted-foreground" />
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||||
{completedCount > 0 && (
|
|
||||||
<span className="absolute -top-1 -right-1 bg-brand-500 text-white text-[8px] font-bold rounded-full w-3.5 h-3.5 flex items-center justify-center">
|
|
||||||
{completedCount > 99 ? '99+' : completedCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Completed Features ({completedCount})</p>
|
<p>Complete All</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
)}
|
||||||
</TooltipProvider>
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 relative"
|
||||||
|
onClick={onShowCompletedModal}
|
||||||
|
data-testid="completed-features-button"
|
||||||
|
>
|
||||||
|
<Archive className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
{completedCount > 0 && (
|
||||||
|
<span className="absolute -top-1 -right-1 bg-brand-500 text-white text-[8px] font-bold rounded-full w-3.5 h-3.5 flex items-center justify-center">
|
||||||
|
{completedCount > 99 ? '99+' : completedCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Completed Features ({completedCount})</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
) : column.id === 'backlog' ? (
|
) : column.id === 'backlog' ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ReactElement, ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
interface TooltipWrapperProps {
|
interface TooltipWrapperProps {
|
||||||
/** The element to wrap with a tooltip */
|
/** The element to wrap with a tooltip */
|
||||||
@@ -29,16 +29,14 @@ export function TooltipWrapper({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
{/* The div wrapper is necessary for tooltips to work on disabled elements */}
|
||||||
{/* The div wrapper is necessary for tooltips to work on disabled elements */}
|
<div>{children}</div>
|
||||||
<div>{children}</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side={side}>
|
||||||
<TooltipContent side={side}>
|
<p>{tooltipContent}</p>
|
||||||
<p>{tooltipContent}</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { Check, CircleDot, Globe, GitPullRequest, FlaskConical } from 'lucide-react';
|
import { Check, CircleDot, Globe, GitPullRequest, FlaskConical } from 'lucide-react';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -101,14 +101,12 @@ export function WorktreeDropdownItem({
|
|||||||
|
|
||||||
{/* Branch name with optional tooltip */}
|
{/* Branch name with optional tooltip */}
|
||||||
{isBranchNameTruncated ? (
|
{isBranchNameTruncated ? (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>{branchNameElement}</TooltipTrigger>
|
||||||
<TooltipTrigger asChild>{branchNameElement}</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p className="font-mono text-xs">{worktree.branch}</p>
|
||||||
<p className="font-mono text-xs">{worktree.branch}</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
) : (
|
||||||
branchNameElement
|
branchNameElement
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import {
|
import {
|
||||||
GitBranch,
|
GitBranch,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -335,14 +335,12 @@ export function WorktreeDropdown({
|
|||||||
const dropdownTrigger = <DropdownMenuTrigger asChild>{triggerButton}</DropdownMenuTrigger>;
|
const dropdownTrigger = <DropdownMenuTrigger asChild>{triggerButton}</DropdownMenuTrigger>;
|
||||||
|
|
||||||
const triggerWithTooltip = isBranchNameTruncated ? (
|
const triggerWithTooltip = isBranchNameTruncated ? (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>{dropdownTrigger}</TooltipTrigger>
|
||||||
<TooltipTrigger asChild>{dropdownTrigger}</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p className="font-mono text-xs">{displayBranch}</p>
|
||||||
<p className="font-mono text-xs">{displayBranch}</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
) : (
|
||||||
dropdownTrigger
|
dropdownTrigger
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Globe, CircleDot, GitPullRequest } from 'lucide-react';
|
import { Globe, CircleDot, GitPullRequest } from 'lucide-react';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
import type {
|
import type {
|
||||||
WorktreeInfo,
|
WorktreeInfo,
|
||||||
@@ -271,29 +271,27 @@ export function WorktreeTab({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<span
|
||||||
<span
|
className={cn(
|
||||||
className={cn(
|
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
|
||||||
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
|
isSelected
|
||||||
isSelected
|
? 'bg-amber-500 text-amber-950 border-amber-400'
|
||||||
? 'bg-amber-500 text-amber-950 border-amber-400'
|
: 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30'
|
||||||
: 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30'
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
|
||||||
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
|
{changedFilesCount ?? '!'}
|
||||||
{changedFilesCount ?? '!'}
|
</span>
|
||||||
</span>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>
|
||||||
<p>
|
{changedFilesCount ?? 'Some'} uncommitted file
|
||||||
{changedFilesCount ?? 'Some'} uncommitted file
|
{changedFilesCount !== 1 ? 's' : ''}
|
||||||
{changedFilesCount !== 1 ? 's' : ''}
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
{prBadge}
|
{prBadge}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -340,78 +338,72 @@ export function WorktreeTab({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<span
|
||||||
<span
|
className={cn(
|
||||||
className={cn(
|
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
|
||||||
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
|
isSelected
|
||||||
isSelected
|
? 'bg-amber-500 text-amber-950 border-amber-400'
|
||||||
? 'bg-amber-500 text-amber-950 border-amber-400'
|
: 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30'
|
||||||
: 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30'
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
|
||||||
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
|
{changedFilesCount ?? '!'}
|
||||||
{changedFilesCount ?? '!'}
|
</span>
|
||||||
</span>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>
|
||||||
<p>
|
{changedFilesCount ?? 'Some'} uncommitted file
|
||||||
{changedFilesCount ?? 'Some'} uncommitted file
|
{changedFilesCount !== 1 ? 's' : ''}
|
||||||
{changedFilesCount !== 1 ? 's' : ''}
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
{prBadge}
|
{prBadge}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDevServerRunning && (
|
{isDevServerRunning && (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
variant={isSelected ? 'default' : 'outline'}
|
||||||
variant={isSelected ? 'default' : 'outline'}
|
size="sm"
|
||||||
size="sm"
|
className={cn(
|
||||||
className={cn(
|
'h-7 w-7 p-0 rounded-none border-r-0',
|
||||||
'h-7 w-7 p-0 rounded-none border-r-0',
|
isSelected && 'bg-primary text-primary-foreground',
|
||||||
isSelected && 'bg-primary text-primary-foreground',
|
!isSelected && 'bg-secondary/50 hover:bg-secondary',
|
||||||
!isSelected && 'bg-secondary/50 hover:bg-secondary',
|
'text-green-500'
|
||||||
'text-green-500'
|
)}
|
||||||
)}
|
onClick={() => onOpenDevServerUrl(worktree)}
|
||||||
onClick={() => onOpenDevServerUrl(worktree)}
|
aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
|
||||||
aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
|
>
|
||||||
>
|
<Globe className="w-3 h-3" aria-hidden="true" />
|
||||||
<Globe className="w-3 h-3" aria-hidden="true" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>Open dev server (:{devServerInfo?.port})</p>
|
||||||
<p>Open dev server (:{devServerInfo?.port})</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAutoModeRunning && (
|
{isAutoModeRunning && (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<span
|
||||||
<span
|
className={cn(
|
||||||
className={cn(
|
'flex items-center justify-center h-7 px-1.5 rounded-none border-r-0',
|
||||||
'flex items-center justify-center h-7 px-1.5 rounded-none border-r-0',
|
isSelected ? 'bg-primary text-primary-foreground' : 'bg-secondary/50'
|
||||||
isSelected ? 'bg-primary text-primary-foreground' : 'bg-secondary/50'
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||||
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
</span>
|
||||||
</span>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>Auto Mode Running</p>
|
||||||
<p>Auto Mode Running</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<WorktreeActionsDropdown
|
<WorktreeActionsDropdown
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useReactFlow, Panel } from '@xyflow/react';
|
import { useReactFlow, Panel } from '@xyflow/react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import {
|
import {
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
@@ -30,109 +30,107 @@ export function GraphControls({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel position="bottom-left" className="flex flex-col gap-2">
|
<Panel position="bottom-left" className="flex flex-col gap-2">
|
||||||
<TooltipProvider delayDuration={200}>
|
<div
|
||||||
<div
|
className="flex flex-col gap-1 p-1.5 rounded-lg backdrop-blur-sm border border-border shadow-lg text-popover-foreground"
|
||||||
className="flex flex-col gap-1 p-1.5 rounded-lg backdrop-blur-sm border border-border shadow-lg text-popover-foreground"
|
style={{ backgroundColor: 'color-mix(in oklch, var(--popover) 90%, transparent)' }}
|
||||||
style={{ backgroundColor: 'color-mix(in oklch, var(--popover) 90%, transparent)' }}
|
>
|
||||||
>
|
{/* Zoom controls */}
|
||||||
{/* Zoom controls */}
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
className="h-8 w-8 p-0"
|
||||||
className="h-8 w-8 p-0"
|
onClick={() => zoomIn({ duration: 200 })}
|
||||||
onClick={() => zoomIn({ duration: 200 })}
|
>
|
||||||
>
|
<ZoomIn className="w-4 h-4" />
|
||||||
<ZoomIn className="w-4 h-4" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="right">Zoom In</TooltipContent>
|
||||||
<TooltipContent side="right">Zoom In</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
onClick={() => zoomOut({ duration: 200 })}
|
onClick={() => zoomOut({ duration: 200 })}
|
||||||
>
|
>
|
||||||
<ZoomOut className="w-4 h-4" />
|
<ZoomOut className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">Zoom Out</TooltipContent>
|
<TooltipContent side="right">Zoom Out</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
onClick={() => fitView({ padding: 0.2, duration: 300 })}
|
onClick={() => fitView({ padding: 0.2, duration: 300 })}
|
||||||
>
|
>
|
||||||
<Maximize2 className="w-4 h-4" />
|
<Maximize2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">Fit View</TooltipContent>
|
<TooltipContent side="right">Fit View</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
|
|
||||||
{/* Layout controls */}
|
{/* Layout controls */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-8 w-8 p-0',
|
'h-8 w-8 p-0',
|
||||||
layoutDirection === 'LR' && 'bg-brand-500/20 text-brand-500'
|
layoutDirection === 'LR' && 'bg-brand-500/20 text-brand-500'
|
||||||
)}
|
)}
|
||||||
onClick={() => onRunLayout('LR')}
|
onClick={() => onRunLayout('LR')}
|
||||||
>
|
>
|
||||||
<ArrowRight className="w-4 h-4" />
|
<ArrowRight className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">Horizontal Layout</TooltipContent>
|
<TooltipContent side="right">Horizontal Layout</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-8 w-8 p-0',
|
'h-8 w-8 p-0',
|
||||||
layoutDirection === 'TB' && 'bg-brand-500/20 text-brand-500'
|
layoutDirection === 'TB' && 'bg-brand-500/20 text-brand-500'
|
||||||
)}
|
)}
|
||||||
onClick={() => onRunLayout('TB')}
|
onClick={() => onRunLayout('TB')}
|
||||||
>
|
>
|
||||||
<ArrowDown className="w-4 h-4" />
|
<ArrowDown className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">Vertical Layout</TooltipContent>
|
<TooltipContent side="right">Vertical Layout</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
|
|
||||||
{/* Lock toggle */}
|
{/* Lock toggle */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn('h-8 w-8 p-0', isLocked && 'bg-brand-500/20 text-brand-500')}
|
className={cn('h-8 w-8 p-0', isLocked && 'bg-brand-500/20 text-brand-500')}
|
||||||
onClick={onToggleLock}
|
onClick={onToggleLock}
|
||||||
>
|
>
|
||||||
{isLocked ? <Lock className="w-4 h-4" /> : <Unlock className="w-4 h-4" />}
|
{isLocked ? <Lock className="w-4 h-4" /> : <Unlock className="w-4 h-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">{isLocked ? 'Unlock Nodes' : 'Lock Nodes'}</TooltipContent>
|
<TooltipContent side="right">{isLocked ? 'Unlock Nodes' : 'Lock Nodes'}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Checkbox } from '@/components/ui/checkbox';
|
|||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import {
|
import {
|
||||||
Filter,
|
Filter,
|
||||||
X,
|
X,
|
||||||
@@ -115,248 +115,244 @@ export function GraphFilterControls({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel position="top-left" className="flex items-center gap-2">
|
<Panel position="top-left" className="flex items-center gap-2">
|
||||||
<TooltipProvider delayDuration={200}>
|
<div
|
||||||
<div
|
className="flex items-center gap-2 p-2 rounded-lg backdrop-blur-sm border border-border shadow-lg text-popover-foreground"
|
||||||
className="flex items-center gap-2 p-2 rounded-lg backdrop-blur-sm border border-border shadow-lg text-popover-foreground"
|
style={{ backgroundColor: 'color-mix(in oklch, var(--popover) 90%, transparent)' }}
|
||||||
style={{ backgroundColor: 'color-mix(in oklch, var(--popover) 90%, transparent)' }}
|
>
|
||||||
>
|
{/* Search Input */}
|
||||||
{/* Search Input */}
|
<div className="relative">
|
||||||
<div className="relative">
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
<Input
|
||||||
<Input
|
type="text"
|
||||||
type="text"
|
placeholder="Search tasks..."
|
||||||
placeholder="Search tasks..."
|
value={searchQuery}
|
||||||
value={searchQuery}
|
onChange={(e) => onSearchQueryChange(e.target.value)}
|
||||||
onChange={(e) => onSearchQueryChange(e.target.value)}
|
className="h-8 w-48 pl-8 pr-8 text-sm bg-background/50"
|
||||||
className="h-8 w-48 pl-8 pr-8 text-sm bg-background/50"
|
/>
|
||||||
/>
|
{searchQuery && (
|
||||||
{searchQuery && (
|
<button
|
||||||
<button
|
onClick={() => onSearchQueryChange('')}
|
||||||
onClick={() => onSearchQueryChange('')}
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
aria-label="Clear search"
|
||||||
aria-label="Clear search"
|
>
|
||||||
>
|
<X className="w-3.5 h-3.5" />
|
||||||
<X className="w-3.5 h-3.5" />
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="h-6 w-px bg-border" />
|
|
||||||
|
|
||||||
{/* Category Filter Dropdown */}
|
|
||||||
<Popover>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={cn(
|
|
||||||
'h-8 px-2 gap-1.5',
|
|
||||||
selectedCategories.length > 0 && 'bg-brand-500/20 text-brand-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Filter className="w-4 h-4" />
|
|
||||||
<span className="text-xs max-w-[100px] truncate">{categoryButtonLabel}</span>
|
|
||||||
<ChevronDown className="w-3 h-3 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Filter by Category</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<PopoverContent align="start" className="w-56 p-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs font-medium text-muted-foreground px-2 py-1">
|
|
||||||
Categories
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Select All option */}
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
|
||||||
onClick={handleSelectAllCategories}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
selectedCategories.length === availableCategories.length &&
|
|
||||||
availableCategories.length > 0
|
|
||||||
}
|
|
||||||
onCheckedChange={handleSelectAllCategories}
|
|
||||||
/>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{selectedCategories.length === availableCategories.length
|
|
||||||
? 'Deselect All'
|
|
||||||
: 'Select All'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-px bg-border" />
|
|
||||||
|
|
||||||
{/* Category list */}
|
|
||||||
<div className="max-h-48 overflow-y-auto space-y-0.5">
|
|
||||||
{availableCategories.length === 0 ? (
|
|
||||||
<div className="text-xs text-muted-foreground px-2 py-2">
|
|
||||||
No categories available
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
availableCategories.map((category) => (
|
|
||||||
<div
|
|
||||||
key={category}
|
|
||||||
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
|
||||||
onClick={() => handleCategoryToggle(category)}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedCategories.includes(category)}
|
|
||||||
onCheckedChange={() => handleCategoryToggle(category)}
|
|
||||||
/>
|
|
||||||
<span className="text-sm truncate">{category}</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
{/* Status Filter Dropdown */}
|
|
||||||
<Popover>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={cn(
|
|
||||||
'h-8 px-2 gap-1.5',
|
|
||||||
selectedStatuses.length > 0 && 'bg-brand-500/20 text-brand-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CircleDot className="w-4 h-4" />
|
|
||||||
<span className="text-xs max-w-[120px] truncate">{statusButtonLabel}</span>
|
|
||||||
<ChevronDown className="w-3 h-3 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Filter by Status</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<PopoverContent align="start" className="w-56 p-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs font-medium text-muted-foreground px-2 py-1">Status</div>
|
|
||||||
|
|
||||||
{/* Select All option */}
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
|
||||||
onClick={handleSelectAllStatuses}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedStatuses.length === STATUS_FILTER_OPTIONS.length}
|
|
||||||
onCheckedChange={handleSelectAllStatuses}
|
|
||||||
/>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{selectedStatuses.length === STATUS_FILTER_OPTIONS.length
|
|
||||||
? 'Deselect All'
|
|
||||||
: 'Select All'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-px bg-border" />
|
|
||||||
|
|
||||||
{/* Status list */}
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
{STATUS_FILTER_OPTIONS.map((status) => {
|
|
||||||
const config = statusDisplayConfig[status];
|
|
||||||
const StatusIcon = config.icon;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={status}
|
|
||||||
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
|
||||||
onClick={() => handleStatusToggle(status)}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedStatuses.includes(status)}
|
|
||||||
onCheckedChange={() => handleStatusToggle(status)}
|
|
||||||
/>
|
|
||||||
<StatusIcon className={cn('w-3.5 h-3.5', config.colorClass)} />
|
|
||||||
<span className="text-sm">{config.label}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="h-6 w-px bg-border" />
|
|
||||||
|
|
||||||
{/* Positive/Negative Filter Toggle */}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => onNegativeFilterChange(!isNegativeFilter)}
|
|
||||||
aria-label={
|
|
||||||
isNegativeFilter
|
|
||||||
? 'Switch to show matching nodes'
|
|
||||||
: 'Switch to hide matching nodes'
|
|
||||||
}
|
|
||||||
aria-pressed={isNegativeFilter}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-1.5 px-2 py-1 rounded text-xs transition-colors',
|
|
||||||
isNegativeFilter
|
|
||||||
? 'bg-orange-500/20 text-orange-500'
|
|
||||||
: 'hover:bg-accent text-muted-foreground hover:text-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isNegativeFilter ? (
|
|
||||||
<>
|
|
||||||
<EyeOff className="w-3.5 h-3.5" />
|
|
||||||
<span>Hide</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Eye className="w-3.5 h-3.5" />
|
|
||||||
<span>Show</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<Switch
|
|
||||||
checked={isNegativeFilter}
|
|
||||||
onCheckedChange={onNegativeFilterChange}
|
|
||||||
aria-label="Toggle between show and hide filter modes"
|
|
||||||
className="h-5 w-9 data-[state=checked]:bg-orange-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{isNegativeFilter
|
|
||||||
? 'Negative filter: Highlighting non-matching nodes'
|
|
||||||
: 'Positive filter: Highlighting matching nodes'}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{/* Clear Filters Button - only show when filters are active */}
|
|
||||||
{hasActiveFilter && (
|
|
||||||
<>
|
|
||||||
<div className="h-6 w-px bg-border" />
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
|
|
||||||
onClick={onClearFilters}
|
|
||||||
aria-label="Clear all filters"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Clear All Filters</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="h-6 w-px bg-border" />
|
||||||
|
|
||||||
|
{/* Category Filter Dropdown */}
|
||||||
|
<Popover>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-8 px-2 gap-1.5',
|
||||||
|
selectedCategories.length > 0 && 'bg-brand-500/20 text-brand-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Filter className="w-4 h-4" />
|
||||||
|
<span className="text-xs max-w-[100px] truncate">{categoryButtonLabel}</span>
|
||||||
|
<ChevronDown className="w-3 h-3 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Filter by Category</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent align="start" className="w-56 p-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground px-2 py-1">Categories</div>
|
||||||
|
|
||||||
|
{/* Select All option */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
||||||
|
onClick={handleSelectAllCategories}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
selectedCategories.length === availableCategories.length &&
|
||||||
|
availableCategories.length > 0
|
||||||
|
}
|
||||||
|
onCheckedChange={handleSelectAllCategories}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{selectedCategories.length === availableCategories.length
|
||||||
|
? 'Deselect All'
|
||||||
|
: 'Select All'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-px bg-border" />
|
||||||
|
|
||||||
|
{/* Category list */}
|
||||||
|
<div className="max-h-48 overflow-y-auto space-y-0.5">
|
||||||
|
{availableCategories.length === 0 ? (
|
||||||
|
<div className="text-xs text-muted-foreground px-2 py-2">
|
||||||
|
No categories available
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
availableCategories.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category}
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
||||||
|
onClick={() => handleCategoryToggle(category)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedCategories.includes(category)}
|
||||||
|
onCheckedChange={() => handleCategoryToggle(category)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm truncate">{category}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* Status Filter Dropdown */}
|
||||||
|
<Popover>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-8 px-2 gap-1.5',
|
||||||
|
selectedStatuses.length > 0 && 'bg-brand-500/20 text-brand-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CircleDot className="w-4 h-4" />
|
||||||
|
<span className="text-xs max-w-[120px] truncate">{statusButtonLabel}</span>
|
||||||
|
<ChevronDown className="w-3 h-3 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Filter by Status</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent align="start" className="w-56 p-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground px-2 py-1">Status</div>
|
||||||
|
|
||||||
|
{/* Select All option */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
||||||
|
onClick={handleSelectAllStatuses}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedStatuses.length === STATUS_FILTER_OPTIONS.length}
|
||||||
|
onCheckedChange={handleSelectAllStatuses}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{selectedStatuses.length === STATUS_FILTER_OPTIONS.length
|
||||||
|
? 'Deselect All'
|
||||||
|
: 'Select All'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-px bg-border" />
|
||||||
|
|
||||||
|
{/* Status list */}
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{STATUS_FILTER_OPTIONS.map((status) => {
|
||||||
|
const config = statusDisplayConfig[status];
|
||||||
|
const StatusIcon = config.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={status}
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
||||||
|
onClick={() => handleStatusToggle(status)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedStatuses.includes(status)}
|
||||||
|
onCheckedChange={() => handleStatusToggle(status)}
|
||||||
|
/>
|
||||||
|
<StatusIcon className={cn('w-3.5 h-3.5', config.colorClass)} />
|
||||||
|
<span className="text-sm">{config.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="h-6 w-px bg-border" />
|
||||||
|
|
||||||
|
{/* Positive/Negative Filter Toggle */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onNegativeFilterChange(!isNegativeFilter)}
|
||||||
|
aria-label={
|
||||||
|
isNegativeFilter
|
||||||
|
? 'Switch to show matching nodes'
|
||||||
|
: 'Switch to hide matching nodes'
|
||||||
|
}
|
||||||
|
aria-pressed={isNegativeFilter}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1.5 px-2 py-1 rounded text-xs transition-colors',
|
||||||
|
isNegativeFilter
|
||||||
|
? 'bg-orange-500/20 text-orange-500'
|
||||||
|
: 'hover:bg-accent text-muted-foreground hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isNegativeFilter ? (
|
||||||
|
<>
|
||||||
|
<EyeOff className="w-3.5 h-3.5" />
|
||||||
|
<span>Hide</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Eye className="w-3.5 h-3.5" />
|
||||||
|
<span>Show</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<Switch
|
||||||
|
checked={isNegativeFilter}
|
||||||
|
onCheckedChange={onNegativeFilterChange}
|
||||||
|
aria-label="Toggle between show and hide filter modes"
|
||||||
|
className="h-5 w-9 data-[state=checked]:bg-orange-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{isNegativeFilter
|
||||||
|
? 'Negative filter: Highlighting non-matching nodes'
|
||||||
|
: 'Positive filter: Highlighting matching nodes'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Clear Filters Button - only show when filters are active */}
|
||||||
|
{hasActiveFilter && (
|
||||||
|
<>
|
||||||
|
<div className="h-6 w-px bg-border" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={onClearFilters}
|
||||||
|
aria-label="Clear all filters"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Clear All Filters</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
type TaskNodeProps = NodeProps & {
|
type TaskNodeProps = NodeProps & {
|
||||||
data: TaskNodeData;
|
data: TaskNodeData;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { LogOut, User, Code2, RefreshCw } from 'lucide-react';
|
import { LogOut, User, Code2, RefreshCw } from 'lucide-react';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
|
|||||||
Reference in New Issue
Block a user