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
111 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|