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:
Shirone
2026-01-25 12:59:58 +01:00
parent 906f471521
commit 605d9658d9
20 changed files with 1132 additions and 1200 deletions

View File

@@ -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 { cn } from '@/lib/utils';
@@ -18,24 +18,22 @@ export function BoardControls({ isMounted, onShowBoardBackground }: BoardControl
);
return (
<TooltipProvider>
<div className="flex items-center gap-2">
{/* Board Background Button */}
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onShowBoardBackground}
className={buttonClass}
data-testid="board-background-button"
>
<ImageIcon className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Board Background Settings</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
<div className="flex items-center gap-2">
{/* Board Background Button */}
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onShowBoardBackground}
className={buttonClass}
data-testid="board-background-button"
>
<ImageIcon className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Board Background Settings</p>
</TooltipContent>
</Tooltip>
</div>
);
}

View File

@@ -2,7 +2,7 @@
import { memo, useEffect, useMemo, useState } from 'react';
import { Feature, useAppStore } from '@/store/app-store';
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 { getBlockingDependencies } from '@automaker/dependency-resolver';
import { useShallow } from 'zustand/react/shallow';
@@ -28,24 +28,22 @@ export const CardBadges = memo(function CardBadges({ feature }: CardBadgesProps)
return (
<div className="flex flex-wrap items-center gap-1.5 px-3 pt-1.5 min-h-[24px]">
{/* Error badge */}
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
'bg-[var(--status-error-bg)] border-[var(--status-error)]/40 text-[var(--status-error)]'
)}
data-testid={`error-badge-${feature.id}`}
>
<AlertCircle className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
<p>{feature.error}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
'bg-[var(--status-error-bg)] border-[var(--status-error)]/40 text-[var(--status-error)]'
)}
data-testid={`error-badge-${feature.id}`}
>
<AlertCircle className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
<p>{feature.error}</p>
</TooltipContent>
</Tooltip>
</div>
);
});
@@ -138,147 +136,137 @@ export const PriorityBadges = memo(function PriorityBadges({
<div className="absolute top-2 left-2 flex items-center gap-1">
{/* Priority badge */}
{feature.priority && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
feature.priority === 1 &&
'bg-[var(--status-error-bg)] border-[var(--status-error)]/40 text-[var(--status-error)]',
feature.priority === 2 &&
'bg-[var(--status-warning-bg)] border-[var(--status-warning)]/40 text-[var(--status-warning)]',
feature.priority === 3 &&
'bg-[var(--status-info-bg)] border-[var(--status-info)]/40 text-[var(--status-info)]'
)}
data-testid={`priority-badge-${feature.id}`}
>
<span className="font-bold text-xs">
{feature.priority === 1 ? 'H' : feature.priority === 2 ? 'M' : 'L'}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
<p>
{feature.priority === 1
? 'High Priority'
: feature.priority === 2
? 'Medium Priority'
: 'Low Priority'}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
feature.priority === 1 &&
'bg-[var(--status-error-bg)] border-[var(--status-error)]/40 text-[var(--status-error)]',
feature.priority === 2 &&
'bg-[var(--status-warning-bg)] border-[var(--status-warning)]/40 text-[var(--status-warning)]',
feature.priority === 3 &&
'bg-[var(--status-info-bg)] border-[var(--status-info)]/40 text-[var(--status-info)]'
)}
data-testid={`priority-badge-${feature.id}`}
>
<span className="font-bold text-xs">
{feature.priority === 1 ? 'H' : feature.priority === 2 ? 'M' : 'L'}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
<p>
{feature.priority === 1
? 'High Priority'
: feature.priority === 2
? 'Medium Priority'
: 'Low Priority'}
</p>
</TooltipContent>
</Tooltip>
)}
{/* Manual verification badge */}
{showManualVerification && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
'bg-[var(--status-warning-bg)] border-[var(--status-warning)]/40 text-[var(--status-warning)]'
)}
data-testid={`skip-tests-badge-${feature.id}`}
>
<Hand className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
<p>Manual verification required</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
'bg-[var(--status-warning-bg)] border-[var(--status-warning)]/40 text-[var(--status-warning)]'
)}
data-testid={`skip-tests-badge-${feature.id}`}
>
<Hand className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
<p>Manual verification required</p>
</TooltipContent>
</Tooltip>
)}
{/* Blocked badge */}
{isBlocked && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
'bg-orange-500/20 border-orange-500/50 text-orange-500'
)}
data-testid={`blocked-badge-${feature.id}`}
>
<Lock className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
<p className="font-medium mb-1">
Blocked by {blockingDependencies.length} incomplete{' '}
{blockingDependencies.length === 1 ? 'dependency' : 'dependencies'}
</p>
<p className="text-muted-foreground">
{blockingDependencies
.map((depId) => {
const dep = features.find((f) => f.id === depId);
return dep?.description || depId;
})
.join(', ')}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
'bg-orange-500/20 border-orange-500/50 text-orange-500'
)}
data-testid={`blocked-badge-${feature.id}`}
>
<Lock className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
<p className="font-medium mb-1">
Blocked by {blockingDependencies.length} incomplete{' '}
{blockingDependencies.length === 1 ? 'dependency' : 'dependencies'}
</p>
<p className="text-muted-foreground">
{blockingDependencies
.map((depId) => {
const dep = features.find((f) => f.id === depId);
return dep?.description || depId;
})
.join(', ')}
</p>
</TooltipContent>
</Tooltip>
)}
{/* Just Finished badge */}
{isJustFinished && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
'bg-[var(--status-success-bg)] border-[var(--status-success)]/40 text-[var(--status-success)] animate-pulse'
)}
data-testid={`just-finished-badge-${feature.id}`}
>
<Sparkles className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
<p>Agent just finished working on this feature</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
'bg-[var(--status-success-bg)] border-[var(--status-success)]/40 text-[var(--status-success)] animate-pulse'
)}
data-testid={`just-finished-badge-${feature.id}`}
>
<Sparkles className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
<p>Agent just finished working on this feature</p>
</TooltipContent>
</Tooltip>
)}
{/* Pipeline exclusion badge */}
{hasPipelineExclusions && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
allPipelinesExcluded
? 'bg-violet-500/20 border-violet-500/50 text-violet-500'
: 'bg-violet-500/10 border-violet-500/30 text-violet-400'
)}
data-testid={`pipeline-exclusion-badge-${feature.id}`}
>
<SkipForward className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
<p className="font-medium mb-1">
{allPipelinesExcluded
? 'All pipelines skipped'
: `${excludedStepCount} of ${totalPipelineSteps} pipeline${totalPipelineSteps !== 1 ? 's' : ''} skipped`}
</p>
<p className="text-muted-foreground">
{allPipelinesExcluded
? 'This feature will skip all custom pipeline steps'
: 'Some custom pipeline steps will be skipped for this feature'}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
uniformBadgeClass,
allPipelinesExcluded
? 'bg-violet-500/20 border-violet-500/50 text-violet-500'
: 'bg-violet-500/10 border-violet-500/30 text-violet-400'
)}
data-testid={`pipeline-exclusion-badge-${feature.id}`}
>
<SkipForward className="w-3.5 h-3.5" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
<p className="font-medium mb-1">
{allPipelinesExcluded
? 'All pipelines skipped'
: `${excludedStepCount} of ${totalPipelineSteps} pipeline${totalPipelineSteps !== 1 ? 's' : ''} skipped`}
</p>
<p className="text-muted-foreground">
{allPipelinesExcluded
? 'This feature will skip all custom pipeline steps'
: 'Some custom pipeline steps will be skipped for this feature'}
</p>
</TooltipContent>
</Tooltip>
)}
</div>
);

View File

@@ -3,7 +3,7 @@
// @ts-nocheck
import { memo, useCallback, useState, useEffect } from 'react';
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 type { Feature } from '@/store/app-store';
import { RowActions, type RowActionHandlers } from './row-actions';
@@ -149,29 +149,27 @@ const IndicatorBadges = memo(function IndicatorBadges({
return (
<div className="flex items-center gap-1 ml-2">
<TooltipProvider delayDuration={200}>
{badges.map((badge) => (
<Tooltip key={badge.key}>
<TooltipTrigger asChild>
<div
className={cn(
'inline-flex items-center justify-center w-5 h-5 rounded border',
badge.colorClass,
badge.bgClass,
badge.borderClass,
badge.animate && 'animate-pulse'
)}
data-testid={`list-row-badge-${badge.key}`}
>
<badge.icon className="w-3 h-3" />
</div>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs max-w-[250px]">
<p>{badge.tooltip}</p>
</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
{badges.map((badge) => (
<Tooltip key={badge.key}>
<TooltipTrigger asChild>
<div
className={cn(
'inline-flex items-center justify-center w-5 h-5 rounded border',
badge.colorClass,
badge.bgClass,
badge.borderClass,
badge.animate && 'animate-pulse'
)}
data-testid={`list-row-badge-${badge.key}`}
>
<badge.icon className="w-3 h-3" />
</div>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs max-w-[250px]">
<p>{badge.tooltip}</p>
</TooltipContent>
</Tooltip>
))}
</div>
);
});

View File

@@ -50,7 +50,7 @@ import {
} from '../shared';
import type { WorkMode } from '../shared';
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 {
getAncestors,
formatAncestorContextForPrompt,
@@ -528,26 +528,24 @@ export function AddFeatureDialog({
<Cpu className="w-4 h-4 text-muted-foreground" />
<span>AI & Execution</span>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
onOpenChange(false);
navigate({ to: '/settings', search: { view: 'defaults' } });
}}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Settings2 className="w-3.5 h-3.5" />
<span>Edit Defaults</span>
</button>
</TooltipTrigger>
<TooltipContent>
<p>Change default model and planning settings for new features</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
onOpenChange(false);
navigate({ to: '/settings', search: { view: 'defaults' } });
}}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Settings2 className="w-3.5 h-3.5" />
<span>Edit Defaults</span>
</button>
</TooltipTrigger>
<TooltipContent>
<p>Change default model and planning settings for new features</p>
</TooltipContent>
</Tooltip>
</div>
<div className="space-y-1.5">
@@ -578,24 +576,22 @@ export function AddFeatureDialog({
compact
/>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="add-feature-planning"
compact
disabled
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="add-feature-planning"
compact
disabled
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
)}
</div>
<div className="space-y-1.5">

View File

@@ -41,7 +41,7 @@ import {
} from '../shared';
import type { WorkMode } from '../shared';
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 { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
@@ -420,26 +420,24 @@ export function EditFeatureDialog({
<Cpu className="w-4 h-4 text-muted-foreground" />
<span>AI & Execution</span>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
onClose();
navigate({ to: '/settings', search: { view: 'defaults' } });
}}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Settings2 className="w-3.5 h-3.5" />
<span>Edit Defaults</span>
</button>
</TooltipTrigger>
<TooltipContent>
<p>Change default model and planning settings for new features</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
onClose();
navigate({ to: '/settings', search: { view: 'defaults' } });
}}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Settings2 className="w-3.5 h-3.5" />
<span>Edit Defaults</span>
</button>
</TooltipTrigger>
<TooltipContent>
<p>Change default model and planning settings for new features</p>
</TooltipContent>
</Tooltip>
</div>
<div className="space-y-1.5">
@@ -470,24 +468,22 @@ export function EditFeatureDialog({
compact
/>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="edit-feature-planning"
compact
disabled
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="edit-feature-planning"
compact
disabled
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
)}
</div>
<div className="space-y-1.5">

View File

@@ -24,7 +24,7 @@ import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
import { cn } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
interface MassEditDialogProps {
open: boolean;
@@ -302,37 +302,35 @@ export function MassEditDialog({
/>
</FieldWrapper>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
'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 gap-2">
<Checkbox checked={false} disabled className="opacity-50" />
<Label className="text-sm font-medium text-muted-foreground">
Planning Mode
</Label>
</div>
</div>
<div className="opacity-50 pointer-events-none">
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="mass-edit-planning"
disabled
/>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
'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 gap-2">
<Checkbox checked={false} disabled className="opacity-50" />
<Label className="text-sm font-medium text-muted-foreground">
Planning Mode
</Label>
</div>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="opacity-50 pointer-events-none">
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="mass-edit-planning"
disabled
/>
</div>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
)}
{/* Priority */}

View File

@@ -13,7 +13,7 @@ import { Button } from '@/components/ui/button';
import { KanbanColumn, KanbanCard, EmptyStateCard } from './components';
import { Feature, useAppStore, formatShortcut } from '@/store/app-store';
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 { getColumnsWithPipeline, type ColumnId } from './constants';
import type { PipelineConfig } from '@automaker/types';
@@ -358,49 +358,47 @@ export function KanbanBoard({
contentClassName="perf-contain"
headerAction={
column.id === 'verified' ? (
<TooltipProvider>
<div className="flex items-center gap-1">
{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>
)}
<div className="flex items-center gap-1">
{columnFeatures.length > 0 && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 relative"
onClick={onShowCompletedModal}
data-testid="completed-features-button"
className="h-6 w-6 p-0"
onClick={onArchiveAllVerified}
data-testid="archive-all-verified-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>
)}
<CheckCircle2 className="w-3.5 h-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Completed Features ({completedCount})</p>
<p>Complete All</p>
</TooltipContent>
</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' ? (
<div className="flex items-center gap-1">
<Button

View File

@@ -1,5 +1,5 @@
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 {
/** The element to wrap with a tooltip */
@@ -29,16 +29,14 @@ export function TooltipWrapper({
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
{/* The div wrapper is necessary for tooltips to work on disabled elements */}
<div>{children}</div>
</TooltipTrigger>
<TooltipContent side={side}>
<p>{tooltipContent}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
{/* The div wrapper is necessary for tooltips to work on disabled elements */}
<div>{children}</div>
</TooltipTrigger>
<TooltipContent side={side}>
<p>{tooltipContent}</p>
</TooltipContent>
</Tooltip>
);
}

View File

@@ -1,5 +1,5 @@
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 { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
@@ -101,14 +101,12 @@ export function WorktreeDropdownItem({
{/* Branch name with optional tooltip */}
{isBranchNameTruncated ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{branchNameElement}</TooltipTrigger>
<TooltipContent>
<p className="font-mono text-xs">{worktree.branch}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{branchNameElement}</TooltipTrigger>
<TooltipContent>
<p className="font-mono text-xs">{worktree.branch}</p>
</TooltipContent>
</Tooltip>
) : (
branchNameElement
)}

View File

@@ -8,7 +8,7 @@ import {
DropdownMenuTrigger,
DropdownMenuGroup,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
GitBranch,
ChevronDown,
@@ -335,14 +335,12 @@ export function WorktreeDropdown({
const dropdownTrigger = <DropdownMenuTrigger asChild>{triggerButton}</DropdownMenuTrigger>;
const triggerWithTooltip = isBranchNameTruncated ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{dropdownTrigger}</TooltipTrigger>
<TooltipContent>
<p className="font-mono text-xs">{displayBranch}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{dropdownTrigger}</TooltipTrigger>
<TooltipContent>
<p className="font-mono text-xs">{displayBranch}</p>
</TooltipContent>
</Tooltip>
) : (
dropdownTrigger
);

View File

@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
import { Globe, CircleDot, GitPullRequest } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
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 type {
WorktreeInfo,
@@ -271,29 +271,27 @@ export function WorktreeTab({
</span>
)}
{hasChanges && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
isSelected
? 'bg-amber-500 text-amber-950 border-amber-400'
: '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" />
{changedFilesCount ?? '!'}
</span>
</TooltipTrigger>
<TooltipContent>
<p>
{changedFilesCount ?? 'Some'} uncommitted file
{changedFilesCount !== 1 ? 's' : ''}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
isSelected
? 'bg-amber-500 text-amber-950 border-amber-400'
: '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" />
{changedFilesCount ?? '!'}
</span>
</TooltipTrigger>
<TooltipContent>
<p>
{changedFilesCount ?? 'Some'} uncommitted file
{changedFilesCount !== 1 ? 's' : ''}
</p>
</TooltipContent>
</Tooltip>
)}
{prBadge}
</Button>
@@ -340,78 +338,72 @@ export function WorktreeTab({
</span>
)}
{hasChanges && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
isSelected
? 'bg-amber-500 text-amber-950 border-amber-400'
: '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" />
{changedFilesCount ?? '!'}
</span>
</TooltipTrigger>
<TooltipContent>
<p>
{changedFilesCount ?? 'Some'} uncommitted file
{changedFilesCount !== 1 ? 's' : ''}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
isSelected
? 'bg-amber-500 text-amber-950 border-amber-400'
: '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" />
{changedFilesCount ?? '!'}
</span>
</TooltipTrigger>
<TooltipContent>
<p>
{changedFilesCount ?? 'Some'} uncommitted file
{changedFilesCount !== 1 ? 's' : ''}
</p>
</TooltipContent>
</Tooltip>
)}
{prBadge}
</Button>
)}
{isDevServerRunning && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={isSelected ? 'default' : 'outline'}
size="sm"
className={cn(
'h-7 w-7 p-0 rounded-none border-r-0',
isSelected && 'bg-primary text-primary-foreground',
!isSelected && 'bg-secondary/50 hover:bg-secondary',
'text-green-500'
)}
onClick={() => onOpenDevServerUrl(worktree)}
aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
>
<Globe className="w-3 h-3" aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open dev server (:{devServerInfo?.port})</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={isSelected ? 'default' : 'outline'}
size="sm"
className={cn(
'h-7 w-7 p-0 rounded-none border-r-0',
isSelected && 'bg-primary text-primary-foreground',
!isSelected && 'bg-secondary/50 hover:bg-secondary',
'text-green-500'
)}
onClick={() => onOpenDevServerUrl(worktree)}
aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
>
<Globe className="w-3 h-3" aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open dev server (:{devServerInfo?.port})</p>
</TooltipContent>
</Tooltip>
)}
{isAutoModeRunning && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'flex items-center justify-center h-7 px-1.5 rounded-none border-r-0',
isSelected ? 'bg-primary text-primary-foreground' : 'bg-secondary/50'
)}
>
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Auto Mode Running</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'flex items-center justify-center h-7 px-1.5 rounded-none border-r-0',
isSelected ? 'bg-primary text-primary-foreground' : 'bg-secondary/50'
)}
>
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Auto Mode Running</p>
</TooltipContent>
</Tooltip>
)}
<WorktreeActionsDropdown

View File

@@ -1,6 +1,6 @@
import { useReactFlow, Panel } from '@xyflow/react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
ZoomIn,
ZoomOut,
@@ -30,109 +30,107 @@ export function GraphControls({
return (
<Panel position="bottom-left" className="flex flex-col gap-2">
<TooltipProvider delayDuration={200}>
<div
className="flex flex-col gap-1 p-1.5 rounded-lg backdrop-blur-sm border border-border shadow-lg text-popover-foreground"
style={{ backgroundColor: 'color-mix(in oklch, var(--popover) 90%, transparent)' }}
>
{/* Zoom controls */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => zoomIn({ duration: 200 })}
>
<ZoomIn className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Zoom In</TooltipContent>
</Tooltip>
<div
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)' }}
>
{/* Zoom controls */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => zoomIn({ duration: 200 })}
>
<ZoomIn className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Zoom In</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => zoomOut({ duration: 200 })}
>
<ZoomOut className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Zoom Out</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => zoomOut({ duration: 200 })}
>
<ZoomOut className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Zoom Out</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => fitView({ padding: 0.2, duration: 300 })}
>
<Maximize2 className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Fit View</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => fitView({ padding: 0.2, duration: 300 })}
>
<Maximize2 className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Fit View</TooltipContent>
</Tooltip>
<div className="h-px bg-border my-1" />
<div className="h-px bg-border my-1" />
{/* Layout controls */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn(
'h-8 w-8 p-0',
layoutDirection === 'LR' && 'bg-brand-500/20 text-brand-500'
)}
onClick={() => onRunLayout('LR')}
>
<ArrowRight className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Horizontal Layout</TooltipContent>
</Tooltip>
{/* Layout controls */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn(
'h-8 w-8 p-0',
layoutDirection === 'LR' && 'bg-brand-500/20 text-brand-500'
)}
onClick={() => onRunLayout('LR')}
>
<ArrowRight className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Horizontal Layout</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn(
'h-8 w-8 p-0',
layoutDirection === 'TB' && 'bg-brand-500/20 text-brand-500'
)}
onClick={() => onRunLayout('TB')}
>
<ArrowDown className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Vertical Layout</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn(
'h-8 w-8 p-0',
layoutDirection === 'TB' && 'bg-brand-500/20 text-brand-500'
)}
onClick={() => onRunLayout('TB')}
>
<ArrowDown className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Vertical Layout</TooltipContent>
</Tooltip>
<div className="h-px bg-border my-1" />
<div className="h-px bg-border my-1" />
{/* Lock toggle */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn('h-8 w-8 p-0', isLocked && 'bg-brand-500/20 text-brand-500')}
onClick={onToggleLock}
>
{isLocked ? <Lock className="w-4 h-4" /> : <Unlock className="w-4 h-4" />}
</Button>
</TooltipTrigger>
<TooltipContent side="right">{isLocked ? 'Unlock Nodes' : 'Lock Nodes'}</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
{/* Lock toggle */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn('h-8 w-8 p-0', isLocked && 'bg-brand-500/20 text-brand-500')}
onClick={onToggleLock}
>
{isLocked ? <Lock className="w-4 h-4" /> : <Unlock className="w-4 h-4" />}
</Button>
</TooltipTrigger>
<TooltipContent side="right">{isLocked ? 'Unlock Nodes' : 'Lock Nodes'}</TooltipContent>
</Tooltip>
</div>
</Panel>
);
}

View File

@@ -4,7 +4,7 @@ import { Checkbox } from '@/components/ui/checkbox';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
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 {
Filter,
X,
@@ -115,248 +115,244 @@ export function GraphFilterControls({
return (
<Panel position="top-left" className="flex items-center gap-2">
<TooltipProvider delayDuration={200}>
<div
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)' }}
>
{/* Search Input */}
<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" />
<Input
type="text"
placeholder="Search tasks..."
value={searchQuery}
onChange={(e) => onSearchQueryChange(e.target.value)}
className="h-8 w-48 pl-8 pr-8 text-sm bg-background/50"
/>
{searchQuery && (
<button
onClick={() => onSearchQueryChange('')}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label="Clear search"
>
<X className="w-3.5 h-3.5" />
</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
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)' }}
>
{/* Search Input */}
<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" />
<Input
type="text"
placeholder="Search tasks..."
value={searchQuery}
onChange={(e) => onSearchQueryChange(e.target.value)}
className="h-8 w-48 pl-8 pr-8 text-sm bg-background/50"
/>
{searchQuery && (
<button
onClick={() => onSearchQueryChange('')}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label="Clear search"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</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>
);
}

View File

@@ -26,7 +26,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} 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 & {
data: TaskNodeData;

View File

@@ -8,7 +8,7 @@ import {
SelectTrigger,
SelectValue,
} 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 { LogOut, User, Code2, RefreshCw } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';