Kanban card user icon should be in title (vibe-kanban) (#1303)

* Done! The kanban card layout is now restructured with **Title + Avatar + Actions** in the header row, and the description spans the full card width below.

* Done! The avatar is now inline at the start of the title text with:
- Sized down to `h-5 w-5` (from default `h-6 w-6`) to align better with the `text-sm` title
- Inline-block with vertical alignment
- Title text gets full remaining width without squishing
- Actions stay on the right

* Done! Changed to `inline-flex` so the avatar initials are properly centered while staying inline with the title text.

* Done! Created [TaskCardHeader](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/665f-kanban-card-user/frontend/src/components/tasks/TaskCardHeader.tsx) and updated both cards:

**Changes:**
- **TaskCardHeader**: Shared component with inline avatar + optional actions slot
- **TaskCard**: Now uses TaskCardHeader with status indicators passed via `right` prop
- **SharedTaskCard**: Normalized to use TaskCardHeader with TaskCard styles (no more muted text, consistent spacing/sizing)

Both cards now share the same header pattern with inline avatars.

* invert header avatar colour
This commit is contained in:
Louis Knight-Webb
2025-11-17 15:57:54 +00:00
committed by GitHub
parent d578b6f586
commit 124ebbf32c
4 changed files with 89 additions and 50 deletions

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef } from 'react';
import { KanbanCard } from '@/components/ui/shadcn-io/kanban';
import type { SharedTaskRecord } from '@/hooks/useProjectTasks';
import { UserAvatar } from './UserAvatar';
import { TaskCardHeader } from './TaskCardHeader';
interface SharedTaskCardProps {
task: SharedTaskRecord;
@@ -48,23 +48,22 @@ export function SharedTaskCard({
dragDisabled
className="relative overflow-hidden pl-5 before:absolute before:left-0 before:top-0 before:bottom-0 before:w-[3px] before:bg-muted-foreground before:content-['']"
>
<div className="flex items-center gap-3">
<UserAvatar
firstName={task.assignee_first_name ?? undefined}
lastName={task.assignee_last_name ?? undefined}
username={task.assignee_username ?? undefined}
// TODO: Add imageUrl={task.assignee_avatar_url} when backend provides it
<div className="flex flex-col gap-2">
<TaskCardHeader
title={task.title}
avatar={{
firstName: task.assignee_first_name ?? undefined,
lastName: task.assignee_last_name ?? undefined,
username: task.assignee_username ?? undefined,
}}
/>
<div className="flex min-w-0 flex-1 flex-col gap-1 font-light">
<h4 className="text-sm text-muted-foreground line-clamp-2">
{task.title}
</h4>
{task.description && (
<p className="text-xs text-muted-foreground line-clamp-2">
{task.description}
</p>
)}
</div>
{task.description && (
<p className="text-sm text-secondary-foreground break-words">
{task.description.length > 130
? `${task.description.substring(0, 130)}...`
: task.description}
</p>
)}
</div>
</KanbanCard>
);

View File

@@ -8,7 +8,7 @@ import { useNavigateWithSearch } from '@/hooks';
import { paths } from '@/lib/paths';
import { attemptsApi } from '@/lib/api';
import type { SharedTaskRecord } from '@/hooks/useProjectTasks';
import { UserAvatar } from './UserAvatar';
import { TaskCardHeader } from './TaskCardHeader';
import { useTranslation } from 'react-i18next';
type Task = TaskWithAttemptStatus;
@@ -93,35 +93,29 @@ export function TaskCard({
: undefined
}
>
<div className="flex gap-3">
{sharedTask ? (
<UserAvatar
firstName={sharedTask.assignee_first_name ?? undefined}
lastName={sharedTask.assignee_last_name ?? undefined}
username={sharedTask.assignee_username ?? undefined}
// TODO: Add imageUrl={sharedTask.assignee_avatar_url} when backend provides it
className="self-center"
/>
) : null}
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex flex-1 min-w-0 items-center gap-2">
<h4 className="flex-1 min-w-0 line-clamp-2 font-light text-sm">
{task.title}
</h4>
<div className="flex items-center gap-1">
{/* In Progress Spinner */}
<div className="flex flex-col gap-2">
<TaskCardHeader
title={task.title}
avatar={
sharedTask
? {
firstName: sharedTask.assignee_first_name ?? undefined,
lastName: sharedTask.assignee_last_name ?? undefined,
username: sharedTask.assignee_username ?? undefined,
}
: undefined
}
right={
<>
{task.has_in_progress_attempt && (
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
)}
{/* Merged Indicator */}
{task.has_merged_attempt && (
<CheckCircle className="h-4 w-4 text-green-500" />
)}
{/* Failed Indicator */}
{task.last_attempt_failed && !task.has_merged_attempt && (
<XCircle className="h-4 w-4 text-destructive" />
)}
{/* Parent Task Indicator */}
{task.parent_task_attempt && (
<Button
variant="icon"
@@ -134,18 +128,17 @@ export function TaskCard({
<Link className="h-4 w-4" />
</Button>
)}
{/* Actions Menu */}
<ActionsDropdown task={task} sharedTask={sharedTask} />
</div>
</div>
{task.description && (
<p className="text-sm text-secondary-foreground break-words">
{task.description.length > 130
? `${task.description.substring(0, 130)}...`
: task.description}
</p>
)}
</div>
</>
}
/>
{task.description && (
<p className="text-sm text-secondary-foreground break-words">
{task.description.length > 130
? `${task.description.substring(0, 130)}...`
: task.description}
</p>
)}
</div>
</KanbanCard>
);

View File

@@ -0,0 +1,47 @@
import type { ReactNode } from 'react';
import { UserAvatar } from './UserAvatar';
interface HeaderAvatar {
firstName?: string;
lastName?: string;
username?: string;
imageUrl?: string;
}
interface TaskCardHeaderProps {
title: ReactNode;
avatar?: HeaderAvatar;
right?: ReactNode;
className?: string;
titleClassName?: string;
}
export function TaskCardHeader({
title,
avatar,
right,
className,
titleClassName,
}: TaskCardHeaderProps) {
return (
<div className={`flex items-start gap-3 min-w-0 ${className ?? ''}`}>
<h4
className={`flex-1 min-w-0 line-clamp-2 font-light text-sm ${titleClassName ?? ''}`}
>
{avatar ? (
<UserAvatar
firstName={avatar.firstName}
lastName={avatar.lastName}
username={avatar.username}
imageUrl={avatar.imageUrl}
className="mr-2 inline-flex align-middle h-5 w-5"
/>
) : null}
<span className="align-middle">{title}</span>
</h4>
{right ? (
<div className="flex items-center gap-1 shrink-0">{right}</div>
) : null}
</div>
);
}

View File

@@ -99,7 +99,7 @@ export const UserAvatar = ({
return (
<div
className={cn(
'flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full border border-border bg-muted text-xs font-medium text-muted-foreground',
'flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full border border-border bg-muted-foreground text-xs font-medium text-muted',
className
)}
title={label}