mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(panel): task-detail tab state in URL, nav placement, kanban overflow, sidebar divider, tooltip sweep
- Task detail: active tab lives in ?tab= (survives reload, back/forward, and prev/next task jumps); prev/next arrows move into the header row next to Actions instead of their own row above the title - Constraints section always starts collapsed (project boilerplate) - Kanban: native overflow scroll replaces Radix ScrollArea (display:table viewport let cards grow past the column and clip); columns share width (flex-1, 18rem floor, 24rem cap); dark column colors normalized to /40 tints - Sidebar footer: drop the Separator doubled with the wrapper's border-t - Tooltips: self-providing Tooltip root (300ms) + hover hints across sidebar, header, task detail, kanban, and every icon-only button that had none
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Plus, X, GripVertical } from "lucide-react";
|
||||
@@ -70,15 +75,20 @@ export function AcceptanceCriteriaEditor({
|
||||
onChange={(e) => handleUpdate(index, e.target.value)}
|
||||
className="flex-1 h-8"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => handleRemove(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => handleRemove(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Remove this criterion</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,11 @@ import { useState } from "react";
|
||||
import { useTasks } from "@/hooks/use-tasks";
|
||||
import { TaskStatus } from "@/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -80,15 +85,20 @@ export function DependencySelector({
|
||||
{task.id.slice(0, 8)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 shrink-0"
|
||||
onClick={() => removeTask(task.id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 shrink-0"
|
||||
onClick={() => removeTask(task.id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Remove dependency</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -37,21 +37,22 @@ describe("TaskDescription", () => {
|
||||
render(<TaskDescription task={task} />);
|
||||
|
||||
expect(screen.getByText("Implement the thing.")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Route handlers must stay thin"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Two independent toggles: one for Description, one for Constraints.
|
||||
// Constraints is project boilerplate, so it ALWAYS starts collapsed.
|
||||
const constraintsToggle = screen.getByRole("button", {
|
||||
name: "Constraints",
|
||||
});
|
||||
expect(constraintsToggle).toHaveAttribute("aria-expanded", "true");
|
||||
fireEvent.click(constraintsToggle);
|
||||
expect(constraintsToggle).toHaveAttribute("aria-expanded", "false");
|
||||
expect(
|
||||
screen.queryByText("Route handlers must stay thin"),
|
||||
).not.toBeInTheDocument();
|
||||
// Collapsing Constraints doesn't affect the Description section.
|
||||
fireEvent.click(constraintsToggle);
|
||||
expect(constraintsToggle).toHaveAttribute("aria-expanded", "true");
|
||||
expect(
|
||||
screen.getByText("Route handlers must stay thin"),
|
||||
).toBeInTheDocument();
|
||||
// Expanding Constraints doesn't affect the Description section.
|
||||
expect(screen.getByText("Implement the thing.")).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useScrollRestorationStore } from "@/lib/stores";
|
||||
const mockPush = vi.fn();
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: mockPush, back: vi.fn() }),
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
}));
|
||||
|
||||
import { TaskListNav } from "../task-list-nav";
|
||||
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -78,24 +83,31 @@ export function CollapsibleSection({
|
||||
<Collapsible open={open} onOpenChange={setOpen} className="contents">
|
||||
<CardHeader className={cn("pb-3", headerClassName)}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200",
|
||||
!open && "-rotate-90",
|
||||
)}
|
||||
/>
|
||||
<CardTitle className="flex min-w-0 items-center gap-2 text-lg">
|
||||
{title}
|
||||
</CardTitle>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200",
|
||||
!open && "-rotate-90",
|
||||
)}
|
||||
/>
|
||||
<CardTitle className="flex min-w-0 items-center gap-2 text-lg">
|
||||
{title}
|
||||
</CardTitle>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{open ? "Collapse section" : "Expand section"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{actions && (
|
||||
<div className="flex shrink-0 items-center gap-2">{actions}</div>
|
||||
)}
|
||||
|
||||
@@ -180,6 +180,9 @@ export function TaskDescription({ task }: TaskDescriptionProps) {
|
||||
</CollapsibleSection>
|
||||
{task.constraints ? (
|
||||
<CollapsibleSection
|
||||
// Boilerplate identical on every task in the project — always
|
||||
// starts collapsed.
|
||||
defaultOpen={false}
|
||||
title={
|
||||
<>
|
||||
<ShieldAlert className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
|
||||
@@ -53,6 +53,11 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { TaskTypeBadge } from "../task-type-badge";
|
||||
import { CopyButton } from "@/components/ui/copy-button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
// Status badge colors
|
||||
const statusColors: Record<TaskStatus, string> = {
|
||||
@@ -109,9 +114,11 @@ const statusLabels: Record<TaskStatus, string> = {
|
||||
interface TaskHeaderProps {
|
||||
task: Task;
|
||||
onAction?: (action: string) => void;
|
||||
/** Right-aligned slot rendered before the Actions menu (prev/next nav). */
|
||||
nav?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function TaskHeader({ task, onAction }: TaskHeaderProps) {
|
||||
export function TaskHeader({ task, onAction, nav }: TaskHeaderProps) {
|
||||
const router = useRouter();
|
||||
const deleteTask = useDeleteTask();
|
||||
const updateTask = useUpdateTask();
|
||||
@@ -513,12 +520,20 @@ export function TaskHeader({ task, onAction }: TaskHeaderProps) {
|
||||
value={task.status}
|
||||
onValueChange={(v) => handleStatusChange(v as TaskStatus)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`w-40 shrink-0 h-7 text-xs font-medium border-0 ${statusColors[task.status]}`}
|
||||
disabled={isTransitionsLoading}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectTrigger
|
||||
className={`w-40 shrink-0 h-7 text-xs font-medium border-0 ${statusColors[task.status]}`}
|
||||
disabled={isTransitionsLoading}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Change status — valid transitions first, the rest as forced
|
||||
admin overrides
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<SelectContent>
|
||||
{/* Always render the current status first so the trigger value is always present */}
|
||||
<SelectItem key={task.status} value={task.status}>
|
||||
@@ -563,9 +578,14 @@ export function TaskHeader({ task, onAction }: TaskHeaderProps) {
|
||||
value={task.team}
|
||||
onValueChange={(v) => handleTeamChange(v as Team)}
|
||||
>
|
||||
<SelectTrigger className="w-36 shrink-0 h-7 text-sm text-muted-foreground border-0 bg-transparent hover:bg-muted/50 px-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectTrigger className="w-36 shrink-0 h-7 text-sm text-muted-foreground border-0 bg-transparent hover:bg-muted/50 px-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reassign this task to a team</TooltipContent>
|
||||
</Tooltip>
|
||||
<SelectContent>
|
||||
{Object.values(Team).map((team) => (
|
||||
<SelectItem key={team} value={team}>
|
||||
@@ -588,17 +608,25 @@ export function TaskHeader({ task, onAction }: TaskHeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions — pinned top-right; never moves regardless of title length
|
||||
or a dropdown's selected-label width. */}
|
||||
<div className="shrink-0">
|
||||
{/* Nav + Actions — pinned top-right; never moves regardless of title
|
||||
length or a dropdown's selected-label width. */}
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{nav}
|
||||
{actions.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
Actions
|
||||
<MoreVertical className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
Actions
|
||||
<MoreVertical className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Lifecycle actions available from this status
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end">
|
||||
{/* Lifecycle actions (non-cancel) */}
|
||||
{actions
|
||||
|
||||
@@ -7,12 +7,11 @@ import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
interface TaskListNavProps {
|
||||
task: Task;
|
||||
@@ -40,6 +39,7 @@ function isEditableTarget(target: EventTarget | null): boolean {
|
||||
// explaining why — there is no list order to fall back to, so we don't guess.
|
||||
export function TaskListNav({ task }: TaskListNavProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const context = useScrollRestorationStore((state) => state.taskListNav);
|
||||
|
||||
const items = context?.items ?? [];
|
||||
@@ -49,7 +49,14 @@ export function TaskListNav({ task }: TaskListNavProps) {
|
||||
const nextItem =
|
||||
hasContext && index < items.length - 1 ? items[index + 1] : null;
|
||||
|
||||
const query = context?.queryString ? `?${context.queryString}` : "";
|
||||
// Carry the active detail tab (?tab=) into the adjacent task's URL so
|
||||
// prev/next keeps the user on the same tab.
|
||||
const params = new URLSearchParams(context?.queryString ?? "");
|
||||
const activeTab = searchParams.get("tab");
|
||||
if (activeTab) params.set("tab", activeTab);
|
||||
else params.delete("tab");
|
||||
const qs = params.toString();
|
||||
const query = qs ? `?${qs}` : "";
|
||||
|
||||
// Alt+ArrowLeft/Right mirrors the prev/next buttons above.
|
||||
useEffect(() => {
|
||||
@@ -68,22 +75,20 @@ export function TaskListNav({ task }: TaskListNavProps) {
|
||||
}, [prevItem, nextItem, query, router]);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<NavButton
|
||||
direction="prev"
|
||||
item={prevItem}
|
||||
query={query}
|
||||
disabledReason={!hasContext ? NO_CONTEXT_TOOLTIP : undefined}
|
||||
/>
|
||||
<NavButton
|
||||
direction="next"
|
||||
item={nextItem}
|
||||
query={query}
|
||||
disabledReason={!hasContext ? NO_CONTEXT_TOOLTIP : undefined}
|
||||
/>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<NavButton
|
||||
direction="prev"
|
||||
item={prevItem}
|
||||
query={query}
|
||||
disabledReason={!hasContext ? NO_CONTEXT_TOOLTIP : undefined}
|
||||
/>
|
||||
<NavButton
|
||||
direction="next"
|
||||
item={nextItem}
|
||||
query={query}
|
||||
disabledReason={!hasContext ? NO_CONTEXT_TOOLTIP : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Task } from "@/types";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { TabOverview } from "./tab-overview";
|
||||
import { TabPlan } from "./tab-plan";
|
||||
import { TabProgress } from "./tab-progress";
|
||||
@@ -16,13 +22,28 @@ import {
|
||||
GitCommit,
|
||||
StickyNote,
|
||||
Link2,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
interface TaskTabsProps {
|
||||
task: Task;
|
||||
}
|
||||
|
||||
interface TabDef {
|
||||
value: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
hint: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_TAB = "overview";
|
||||
|
||||
export function TaskTabs({ task }: TaskTabsProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Calculate badge counts
|
||||
const progressCount = task.progress_updates.length + task.checkpoints.length;
|
||||
const commitCount = task.commits.length;
|
||||
@@ -33,58 +54,85 @@ export function TaskTabs({ task }: TaskTabsProps) {
|
||||
(task.quick_context ? 1 : 0);
|
||||
const depsCount = task.dependency_ids.length + task.blocker_ids.length;
|
||||
|
||||
const tabs: TabDef[] = [
|
||||
{
|
||||
value: "overview",
|
||||
label: "Overview",
|
||||
icon: FileText,
|
||||
hint: "Description, acceptance criteria, and metadata",
|
||||
},
|
||||
{
|
||||
value: "plan",
|
||||
label: "Plan",
|
||||
icon: Layout,
|
||||
hint: "The delegated sub-task plan",
|
||||
count: task.plan ? task.plan.sub_tasks.length : undefined,
|
||||
},
|
||||
{
|
||||
value: "progress",
|
||||
label: "Progress",
|
||||
icon: Clock,
|
||||
hint: "Progress updates and checkpoints",
|
||||
count: progressCount > 0 ? progressCount : undefined,
|
||||
},
|
||||
{
|
||||
value: "commits",
|
||||
label: "Commits",
|
||||
icon: GitCommit,
|
||||
hint: "Commits linked to this task",
|
||||
count: commitCount > 0 ? commitCount : undefined,
|
||||
},
|
||||
{
|
||||
value: "notes",
|
||||
label: "Notes",
|
||||
icon: StickyNote,
|
||||
hint: "Dev, QA, and auditor notes",
|
||||
count: notesCount > 0 ? notesCount : undefined,
|
||||
},
|
||||
{
|
||||
value: "dependencies",
|
||||
label: "Deps",
|
||||
icon: Link2,
|
||||
hint: "Dependencies and blockers",
|
||||
count: depsCount > 0 ? depsCount : undefined,
|
||||
},
|
||||
];
|
||||
|
||||
// The active tab lives in the URL (?tab=) so it survives reloads,
|
||||
// back/forward, and prev/next task navigation. Unknown values fall back to
|
||||
// the default rather than rendering an empty pane.
|
||||
const tabParam = searchParams.get("tab");
|
||||
const activeTab = tabs.some((t) => t.value === tabParam)
|
||||
? (tabParam as string)
|
||||
: DEFAULT_TAB;
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
if (value === DEFAULT_TAB) params.delete("tab");
|
||||
else params.set("tab", value);
|
||||
const qs = params.toString();
|
||||
router.replace(`${pathname}${qs ? `?${qs}` : ""}`, { scroll: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="overview" className="mt-6">
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="mt-6">
|
||||
<TabsList className="grid w-full grid-cols-6 lg:w-auto lg:inline-grid">
|
||||
<TabsTrigger value="overview" className="gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Overview</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="plan" className="gap-2">
|
||||
<Layout className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Plan</span>
|
||||
{task.plan && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{task.plan.sub_tasks.length}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="progress" className="gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Progress</span>
|
||||
{progressCount > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{progressCount}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="commits" className="gap-2">
|
||||
<GitCommit className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Commits</span>
|
||||
{commitCount > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{commitCount}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="notes" className="gap-2">
|
||||
<StickyNote className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Notes</span>
|
||||
{notesCount > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{notesCount}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="dependencies" className="gap-2">
|
||||
<Link2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Deps</span>
|
||||
{depsCount > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{depsCount}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
{tabs.map((tab) => (
|
||||
<Tooltip key={tab.value}>
|
||||
<TooltipTrigger asChild>
|
||||
<TabsTrigger value={tab.value} className="gap-2">
|
||||
<tab.icon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{tab.label}</span>
|
||||
{tab.count !== undefined && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{tab.count}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tab.hint}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<div className="mt-4">
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { TaskType } from "@/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Code,
|
||||
FileText,
|
||||
@@ -64,11 +71,16 @@ export function TaskTypeBadge({
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`${config.color} ${className}`}>
|
||||
<span className="flex items-center gap-1">
|
||||
{config.icon}
|
||||
{showLabel && <span>{config.label}</span>}
|
||||
</span>
|
||||
</Badge>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="outline" className={`${config.color} ${className}`}>
|
||||
<span className="flex items-center gap-1">
|
||||
{config.icon}
|
||||
{showLabel && <span>{config.label}</span>}
|
||||
</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Task type: {config.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user