Files
Codewalkers/apps/web/src/components/DependencyIndicator.tsx
Lukas May 6a9d9e3452 feat: Redesign task and phase dependency display in plans tab
Replace plain text dependency indicators with visual, status-aware components:
- New DependencyChip/PhaseNumberBadge components with status-colored styling
- Sidebar shows compact numbered circles for phase deps instead of text
- Detail panel uses bordered cards with phase badges and status indicators
- Task dependency callout bars with resolved/total counters
- Collapse mechanism for tasks with 3+ dependencies (+N more button)
- Full dark mode support via semantic status tokens
2026-03-04 05:28:11 +01:00

111 lines
3.4 KiB
TypeScript

import { useState } from "react";
import { ArrowUp, ChevronDown, ChevronRight } from "lucide-react";
import { StatusDot, mapEntityStatus } from "@/components/StatusDot";
import { cn } from "@/lib/utils";
interface DependencyItem {
name: string;
status: string;
}
interface DependencyIndicatorProps {
blockedBy: DependencyItem[];
type: "task" | "phase";
className?: string;
}
const MAX_VISIBLE = 3;
export function DependencyIndicator({
blockedBy,
type: _type,
className,
}: DependencyIndicatorProps) {
const [expanded, setExpanded] = useState(false);
if (blockedBy.length === 0) return null;
const allResolved = blockedBy.every(
(item) => mapEntityStatus(item.status) === "success",
);
const resolvedCount = blockedBy.filter(
(item) => mapEntityStatus(item.status) === "success",
).length;
const shouldCollapse = blockedBy.length > MAX_VISIBLE;
const visibleItems = shouldCollapse && !expanded
? blockedBy.slice(0, MAX_VISIBLE)
: blockedBy;
const hiddenCount = blockedBy.length - MAX_VISIBLE;
return (
<div
className={cn(
"rounded-md border-l-2 py-1 pl-2.5 pr-2",
allResolved
? "border-l-status-success-border/70 bg-status-success-bg/30"
: "border-l-status-warning-border/70 bg-status-warning-bg/30",
className,
)}
>
<div className="flex items-center gap-1.5">
<ArrowUp
className={cn(
"h-3 w-3 shrink-0",
allResolved ? "text-status-success-fg/50" : "text-status-warning-fg/50",
)}
/>
<span
className={cn(
"text-[10px] font-medium uppercase tracking-wide",
allResolved ? "text-status-success-fg/60" : "text-status-warning-fg/60",
)}
>
blocked by {resolvedCount}/{blockedBy.length}
</span>
</div>
<div className="mt-0.5 flex flex-wrap items-center gap-1 pl-[18px]">
{visibleItems.map((item, idx) => (
<span
key={`${item.name}-${idx}`}
className="inline-flex items-center gap-1 rounded-full border border-border/40 bg-background/60 px-1.5 py-px text-[11px]"
>
<StatusDot status={item.status} size="sm" />
<span className="max-w-[160px] truncate">{item.name}</span>
</span>
))}
{shouldCollapse && !expanded && (
<button
className={cn(
"inline-flex items-center gap-0.5 rounded-full px-1.5 py-px text-[10px] font-medium transition-colors hover:bg-accent",
allResolved ? "text-status-success-fg/70" : "text-status-warning-fg/70",
)}
onClick={(e) => {
e.stopPropagation();
setExpanded(true);
}}
>
+{hiddenCount} more
<ChevronRight className="h-2.5 w-2.5" />
</button>
)}
{shouldCollapse && expanded && (
<button
className={cn(
"inline-flex items-center gap-0.5 rounded-full px-1.5 py-px text-[10px] font-medium transition-colors hover:bg-accent",
allResolved ? "text-status-success-fg/70" : "text-status-warning-fg/70",
)}
onClick={(e) => {
e.stopPropagation();
setExpanded(false);
}}
>
show less
<ChevronDown className="h-2.5 w-2.5" />
</button>
)}
</div>
</div>
);
}