Design a new landing page layout for the upcoming product launch campaign.
Feb 24
18
7
cards
A draggable-ready kanban task card. Shows one or more tag chips at the top-left, a three-dot options button (keyboard-accessible with focus-visible ring), a task description paragraph, and a compact stats row with due-date, comment count, and attachment count icons. A row of overlapping circular viewer avatars with a '+N' overflow count closes the card. On hover the box-shadow deepens and a dashed border in --border fades in — all pure CSS transitions, no JS required. Server-renderable with no hooks or state.
Ported from Yaya12085 (MIT)
Design a new landing page layout for the upcoming product launch campaign.
1. Register the namespace (once per project):
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
npx shadcn add @bottega/kanban-task-card<KanbanTaskCard />| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | Design a new landing page layout for the upcoming product launch campaign. | Card task title / description text. |
| tags | KanbanTaskCardTag[] | [{ label: "Design" }, { label: "UI/UX" }] | Tag chips shown at the top-left of the card. |
| dueDate | string | Feb 24 | Due date label displayed in the stats row (e.g. "Feb 24"). |
| commentsCount | number | 18 | Number of comments shown in the stats row. |
| attachmentsCount | number | 7 | Number of file attachments shown in the stats row. |
| viewerAvatarCount | number | 3 | Number of viewer avatar circles to render (decorative). |
| viewerCount | number | 20 | Extra viewer count shown after the avatar circles (e.g. 20 → "+20"). |
import React from "react";
import styles from "./kanban-task-card.module.css";
export interface KanbanTaskCardTag {
label: string;
}
export interface KanbanTaskCardProps {
/** Card task title / description text. */
title?: string;
/** Tag chips shown at the top-left of the card. */
tags?: KanbanTaskCardTag[];
/** Due date label displayed in the stats row (e.g. "Feb 24"). */
dueDate?: string;
/** Number of comments shown in the stats row. */
commentsCount?: number;
/** Number of file attachments shown in the stats row. */
attachmentsCount?: number;
/** Number of viewer avatar circles to render (decorative). */
viewerAvatarCount?: number;
/** Extra viewer count shown after the avatar circles (e.g. 20 → "+20"). */
viewerCount?: number;
}
/** Three-dot horizontal ellipsis SVG. */
function ThreeDotIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 41.915 41.916"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M11.214,20.956c0,3.091-2.509,5.589-5.607,5.589C2.51,26.544,0,24.046,0,20.956c0-3.082,2.511-5.585,5.607-5.585C8.705,15.371,11.214,17.874,11.214,20.956z" />
<path d="M26.564,20.956c0,3.091-2.509,5.589-5.606,5.589c-3.097,0-5.607-2.498-5.607-5.589c0-3.082,2.511-5.585,5.607-5.585C24.056,15.371,26.564,17.874,26.564,20.956z" />
<path d="M41.915,20.956c0,3.091-2.509,5.589-5.607,5.589c-3.097,0-5.606-2.498-5.606-5.589c0-3.082,2.511-5.585,5.606-5.585C39.406,15.371,41.915,17.874,41.915,20.956z" />
</svg>
);
}
/** Clock / deadline icon. */
function ClockIcon({ className }: { className?: string }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path strokeLinecap="round" strokeWidth="2" stroke="currentColor" d="M12 8V12L15 15" />
<circle strokeWidth="2" stroke="currentColor" r="9" cy="12" cx="12" />
</svg>
);
}
/** Chat bubble / comments icon. */
function CommentIcon({ className }: { className?: string }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
strokeLinejoin="round"
strokeLinecap="round"
strokeWidth="1.5"
stroke="currentColor"
d="M16 10H16.01M12 10H12.01M8 10H8.01M3 10C3 4.64706 5.11765 3 12 3C18.8824 3 21 4.64706 21 10C21 15.3529 18.8824 17 12 17C11.6592 17 11.3301 16.996 11.0124 16.9876L7 21V16.4939C4.0328 15.6692 3 13.7383 3 10Z"
/>
</svg>
);
}
/** Attachment / pin icon. */
function AttachmentIcon({ className }: { className?: string }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="-2.5 0 32 32"
fill="currentColor"
aria-hidden="true"
focusable="false"
>
<path d="M0 10.284l0.505 0.36c0.089 0.064 0.92 0.621 2.604 0.621 0.27 0 0.55-0.015 0.836-0.044 3.752 4.346 6.411 7.472 7.060 8.299-1.227 2.735-1.42 5.808-0.537 8.686l0.256 0.834 7.63-7.631 8.309 8.309 0.742-0.742-8.309-8.309 7.631-7.631-0.834-0.255c-2.829-0.868-5.986-0.672-8.686 0.537-0.825-0.648-3.942-3.3-8.28-7.044 0.11-0.669 0.23-2.183-0.575-3.441l-0.352-0.549-8.001 8.001zM1.729 10.039l6.032-6.033c0.385 1.122 0.090 2.319 0.086 2.334l-0.080 0.314 0.245 0.214c7.409 6.398 8.631 7.39 8.992 7.546l-0.002 0.006 0.195 0.058 0.185-0.087c2.257-1.079 4.903-1.378 7.343-0.836l-13.482 13.481c-0.55-2.47-0.262-5.045 0.837-7.342l0.104-0.218-0.098-0.221-0.031 0.013c-0.322-0.632-1.831-2.38-7.498-8.944l-0.185-0.215-0.282 0.038c-0.338 0.045-0.668 0.069-0.981 0.069-0.595 0-1.053-0.083-1.38-0.176z" />
</svg>
);
}
/** Single-user avatar SVG. */
function UserIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
strokeWidth="2"
stroke="currentColor"
d="M17 8C17 10.7614 14.7614 13 12 13C9.23858 13 7 10.7614 7 8C7 5.23858 9.23858 3 12 3C14.7614 3 17 5.23858 17 8Z"
/>
<path
strokeLinecap="round"
strokeWidth="2"
stroke="currentColor"
d="M3 21C3.95728 17.9237 6.41998 17 12 17C17.58 17 20.0427 17.9237 21 21"
/>
</svg>
);
}
/**
* Kanban Task Card
*
* A draggable-ready task surface card. Shows tag chips, a three-dot options
* menu, a task description, a stats row (due date / comments / attachments),
* and a row of overlapping viewer-avatar circles. On hover the card shadow
* deepens and a dashed border appears — all via CSS, no JS required.
*
* Port of https://uiverse.io/Yaya12085/breezy-eagle-39 (MIT).
*/
export function KanbanTaskCard({
title = "Design a new landing page layout for the upcoming product launch campaign.",
tags = [{ label: "Design" }, { label: "UI/UX" }],
dueDate = "Feb 24",
commentsCount = 18,
attachmentsCount = 7,
viewerAvatarCount = 3,
viewerCount = 20,
}: KanbanTaskCardProps) {
return (
// ponytail: draggable is structural affordance only — no drag JS needed to render
<div className={styles.task} draggable={true} role="article" aria-label={title}>
{/* Header: tag chips + options button */}
<div className={styles.tags}>
<div className={styles.tagList} role="group" aria-label="Tags">
{tags.map((tag) => (
<span key={tag.label} className={styles.tag}>
{tag.label}
</span>
))}
</div>
<button className={styles.options} aria-label="Task options" type="button">
<ThreeDotIcon className={styles.optionsSvg} />
</button>
</div>
{/* Task description */}
<p className={styles.description}>{title}</p>
{/* Stats row */}
<div className={styles.stats} aria-label="Task stats">
<div className={styles.statGroup}>
<div className={styles.statItem}>
<ClockIcon className={styles.statIcon} />
<span>{dueDate}</span>
</div>
<div className={styles.statItem}>
<CommentIcon className={styles.statIcon} />
<span>{commentsCount}</span>
</div>
<div className={styles.statItem}>
<AttachmentIcon className={styles.statIcon} />
<span>{attachmentsCount}</span>
</div>
</div>
{/* Viewer avatars */}
<div className={styles.viewer} aria-label={`${viewerAvatarCount + viewerCount} viewers`}>
{Array.from({ length: viewerAvatarCount }).map((_, i) => (
<span key={i} className={styles.viewerAvatar} aria-hidden="true">
<UserIcon />
</span>
))}
<span className={styles.viewerAvatar} aria-hidden="true">
+{viewerCount}
</span>
</div>
</div>
</div>
);
}
export default KanbanTaskCard;