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:
committed by
GitHub
parent
d578b6f586
commit
124ebbf32c
@@ -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,24 +48,23 @@ 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 className="text-sm text-secondary-foreground break-words">
|
||||
{task.description.length > 130
|
||||
? `${task.description.substring(0, 130)}...`
|
||||
: task.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</KanbanCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,10 +128,10 @@ 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
|
||||
@@ -146,7 +140,6 @@ export function TaskCard({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</KanbanCard>
|
||||
);
|
||||
}
|
||||
|
||||
47
frontend/src/components/tasks/TaskCardHeader.tsx
Normal file
47
frontend/src/components/tasks/TaskCardHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user