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 { useCallback, useEffect, useRef } from 'react';
|
||||||
import { KanbanCard } from '@/components/ui/shadcn-io/kanban';
|
import { KanbanCard } from '@/components/ui/shadcn-io/kanban';
|
||||||
import type { SharedTaskRecord } from '@/hooks/useProjectTasks';
|
import type { SharedTaskRecord } from '@/hooks/useProjectTasks';
|
||||||
import { UserAvatar } from './UserAvatar';
|
import { TaskCardHeader } from './TaskCardHeader';
|
||||||
|
|
||||||
interface SharedTaskCardProps {
|
interface SharedTaskCardProps {
|
||||||
task: SharedTaskRecord;
|
task: SharedTaskRecord;
|
||||||
@@ -48,24 +48,23 @@ export function SharedTaskCard({
|
|||||||
dragDisabled
|
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-['']"
|
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">
|
<div className="flex flex-col gap-2">
|
||||||
<UserAvatar
|
<TaskCardHeader
|
||||||
firstName={task.assignee_first_name ?? undefined}
|
title={task.title}
|
||||||
lastName={task.assignee_last_name ?? undefined}
|
avatar={{
|
||||||
username={task.assignee_username ?? undefined}
|
firstName: task.assignee_first_name ?? undefined,
|
||||||
// TODO: Add imageUrl={task.assignee_avatar_url} when backend provides it
|
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 && (
|
{task.description && (
|
||||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
<p className="text-sm text-secondary-foreground break-words">
|
||||||
{task.description}
|
{task.description.length > 130
|
||||||
|
? `${task.description.substring(0, 130)}...`
|
||||||
|
: task.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</KanbanCard>
|
</KanbanCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useNavigateWithSearch } from '@/hooks';
|
|||||||
import { paths } from '@/lib/paths';
|
import { paths } from '@/lib/paths';
|
||||||
import { attemptsApi } from '@/lib/api';
|
import { attemptsApi } from '@/lib/api';
|
||||||
import type { SharedTaskRecord } from '@/hooks/useProjectTasks';
|
import type { SharedTaskRecord } from '@/hooks/useProjectTasks';
|
||||||
import { UserAvatar } from './UserAvatar';
|
import { TaskCardHeader } from './TaskCardHeader';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type Task = TaskWithAttemptStatus;
|
type Task = TaskWithAttemptStatus;
|
||||||
@@ -93,35 +93,29 @@ export function TaskCard({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex gap-3">
|
<div className="flex flex-col gap-2">
|
||||||
{sharedTask ? (
|
<TaskCardHeader
|
||||||
<UserAvatar
|
title={task.title}
|
||||||
firstName={sharedTask.assignee_first_name ?? undefined}
|
avatar={
|
||||||
lastName={sharedTask.assignee_last_name ?? undefined}
|
sharedTask
|
||||||
username={sharedTask.assignee_username ?? undefined}
|
? {
|
||||||
// TODO: Add imageUrl={sharedTask.assignee_avatar_url} when backend provides it
|
firstName: sharedTask.assignee_first_name ?? undefined,
|
||||||
className="self-center"
|
lastName: sharedTask.assignee_last_name ?? undefined,
|
||||||
/>
|
username: sharedTask.assignee_username ?? undefined,
|
||||||
) : null}
|
}
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
: undefined
|
||||||
<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">
|
right={
|
||||||
{task.title}
|
<>
|
||||||
</h4>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{/* In Progress Spinner */}
|
|
||||||
{task.has_in_progress_attempt && (
|
{task.has_in_progress_attempt && (
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
|
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
|
||||||
)}
|
)}
|
||||||
{/* Merged Indicator */}
|
|
||||||
{task.has_merged_attempt && (
|
{task.has_merged_attempt && (
|
||||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||||
)}
|
)}
|
||||||
{/* Failed Indicator */}
|
|
||||||
{task.last_attempt_failed && !task.has_merged_attempt && (
|
{task.last_attempt_failed && !task.has_merged_attempt && (
|
||||||
<XCircle className="h-4 w-4 text-destructive" />
|
<XCircle className="h-4 w-4 text-destructive" />
|
||||||
)}
|
)}
|
||||||
{/* Parent Task Indicator */}
|
|
||||||
{task.parent_task_attempt && (
|
{task.parent_task_attempt && (
|
||||||
<Button
|
<Button
|
||||||
variant="icon"
|
variant="icon"
|
||||||
@@ -134,10 +128,10 @@ export function TaskCard({
|
|||||||
<Link className="h-4 w-4" />
|
<Link className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* Actions Menu */}
|
|
||||||
<ActionsDropdown task={task} sharedTask={sharedTask} />
|
<ActionsDropdown task={task} sharedTask={sharedTask} />
|
||||||
</div>
|
</>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
{task.description && (
|
{task.description && (
|
||||||
<p className="text-sm text-secondary-foreground break-words">
|
<p className="text-sm text-secondary-foreground break-words">
|
||||||
{task.description.length > 130
|
{task.description.length > 130
|
||||||
@@ -146,7 +140,6 @@ export function TaskCard({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</KanbanCard>
|
</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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
title={label}
|
title={label}
|
||||||
|
|||||||
Reference in New Issue
Block a user