feat: Phase 2 card interactions - priority picker, context menu, WIP limits, column collapse, checklist reorder
- PriorityPicker component with 5 colored chips in card detail modal - Card context menu: Move to column, Set priority, Duplicate, Delete - duplicateCard store action (clones card, inserts after original) - Column WIP limits with amber/red indicators when at/over limit - Column collapse/expand to 40px vertical strip - Checklist item drag reordering with grip handle - Comment store actions (addComment, deleteComment) for Phase 3
This commit is contained in:
@@ -5,11 +5,22 @@ import { motion, useReducedMotion, AnimatePresence } from "framer-motion";
|
||||
import { fadeSlideUp, springs } from "@/lib/motion";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import type { Card, Label } from "@/types/board";
|
||||
import type { Card, Label, Priority } from "@/types/board";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
import { LabelDots } from "@/components/board/LabelDots";
|
||||
import { ChecklistBar } from "@/components/board/ChecklistBar";
|
||||
import { Paperclip, AlignLeft } from "lucide-react";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
import { useBoardStore } from "@/stores/board-store";
|
||||
|
||||
/* ---------- Due date status ---------- */
|
||||
|
||||
@@ -97,83 +108,152 @@ export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: Card
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
padding: `calc(0.75rem * var(--density-factor))`,
|
||||
opacity: getAgingOpacity(card.updatedAt),
|
||||
}}
|
||||
onClick={handleClick}
|
||||
className="w-full rounded-lg bg-pylon-surface shadow-sm text-left"
|
||||
layoutId={`card-${card.id}`}
|
||||
variants={fadeSlideUp}
|
||||
initial={prefersReducedMotion ? false : "hidden"}
|
||||
animate="visible"
|
||||
whileHover={{ scale: 1.02, y: -2, boxShadow: "0 4px 12px oklch(0% 0 0 / 10%)" }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={springs.bouncy}
|
||||
layout
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
role="article"
|
||||
aria-label={card.title}
|
||||
>
|
||||
{/* Cover color bar */}
|
||||
{card.coverColor && (
|
||||
<div
|
||||
className="mb-2 h-1 rounded-t-lg"
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<motion.button
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
backgroundColor: `oklch(55% 0.12 ${card.coverColor})`,
|
||||
margin: `calc(-0.75rem * var(--density-factor)) calc(-0.75rem * var(--density-factor)) 0.5rem`,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
padding: `calc(0.75rem * var(--density-factor))`,
|
||||
opacity: getAgingOpacity(card.updatedAt),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Label dots */}
|
||||
{card.labels.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<LabelDots labelIds={card.labels} boardLabels={boardLabels} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Card title */}
|
||||
<p className="text-sm font-medium text-pylon-text">{card.title}</p>
|
||||
|
||||
{/* Footer row: priority + due date + checklist + icons */}
|
||||
{(hasDueDate || card.checklist.length > 0 || card.attachments.length > 0 || card.description || card.priority !== "none") && (
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
{card.priority !== "none" && (
|
||||
<span
|
||||
className={`size-2.5 shrink-0 rounded-full ${card.priority === "urgent" ? "animate-pulse" : ""}`}
|
||||
style={{ backgroundColor: PRIORITY_COLORS[card.priority] }}
|
||||
title={`Priority: ${card.priority}`}
|
||||
onClick={handleClick}
|
||||
className="w-full rounded-lg bg-pylon-surface shadow-sm text-left"
|
||||
layoutId={`card-${card.id}`}
|
||||
variants={fadeSlideUp}
|
||||
initial={prefersReducedMotion ? false : "hidden"}
|
||||
animate="visible"
|
||||
whileHover={{ scale: 1.02, y: -2, boxShadow: "0 4px 12px oklch(0% 0 0 / 10%)" }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={springs.bouncy}
|
||||
layout
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
role="article"
|
||||
aria-label={card.title}
|
||||
>
|
||||
{/* Cover color bar */}
|
||||
{card.coverColor && (
|
||||
<div
|
||||
className="mb-2 h-1 rounded-t-lg"
|
||||
style={{
|
||||
backgroundColor: `oklch(55% 0.12 ${card.coverColor})`,
|
||||
margin: `calc(-0.75rem * var(--density-factor)) calc(-0.75rem * var(--density-factor)) 0.5rem`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{dueDateStatus && card.dueDate && (
|
||||
<span
|
||||
className={`font-mono text-xs rounded px-1 py-0.5 ${dueDateStatus.color} ${dueDateStatus.bgColor}`}
|
||||
title={dueDateStatus.label}
|
||||
>
|
||||
{format(new Date(card.dueDate), "MMM d")}
|
||||
</span>
|
||||
|
||||
{/* Label dots */}
|
||||
{card.labels.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<LabelDots labelIds={card.labels} boardLabels={boardLabels} />
|
||||
</div>
|
||||
)}
|
||||
{card.checklist.length > 0 && (
|
||||
<ChecklistBar checklist={card.checklist} />
|
||||
|
||||
{/* Card title */}
|
||||
<p className="text-sm font-medium text-pylon-text">{card.title}</p>
|
||||
|
||||
{/* Footer row: priority + due date + checklist + icons */}
|
||||
{(hasDueDate || card.checklist.length > 0 || card.attachments.length > 0 || card.description || card.priority !== "none") && (
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
{card.priority !== "none" && (
|
||||
<span
|
||||
className={`size-2.5 shrink-0 rounded-full ${card.priority === "urgent" ? "animate-pulse" : ""}`}
|
||||
style={{ backgroundColor: PRIORITY_COLORS[card.priority] }}
|
||||
title={`Priority: ${card.priority}`}
|
||||
/>
|
||||
)}
|
||||
{dueDateStatus && card.dueDate && (
|
||||
<span
|
||||
className={`font-mono text-xs rounded px-1 py-0.5 ${dueDateStatus.color} ${dueDateStatus.bgColor}`}
|
||||
title={dueDateStatus.label}
|
||||
>
|
||||
{format(new Date(card.dueDate), "MMM d")}
|
||||
</span>
|
||||
)}
|
||||
{card.checklist.length > 0 && (
|
||||
<ChecklistBar checklist={card.checklist} />
|
||||
)}
|
||||
{card.description && (
|
||||
<DescriptionPreview description={card.description} />
|
||||
)}
|
||||
{card.attachments.length > 0 && (
|
||||
<span className="flex items-center gap-0.5 text-pylon-text-secondary">
|
||||
<Paperclip className="size-3" />
|
||||
<span className="font-mono text-xs">{card.attachments.length}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{card.description && (
|
||||
<DescriptionPreview description={card.description} />
|
||||
)}
|
||||
{card.attachments.length > 0 && (
|
||||
<span className="flex items-center gap-0.5 text-pylon-text-secondary">
|
||||
<Paperclip className="size-3" />
|
||||
<span className="font-mono text-xs">{card.attachments.length}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.button>
|
||||
</ContextMenuTrigger>
|
||||
<CardContextMenuContent cardId={card.id} columnId={columnId} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Card context menu ---------- */
|
||||
|
||||
function CardContextMenuContent({ cardId, columnId }: { cardId: string; columnId: string }) {
|
||||
const board = useBoardStore((s) => s.board);
|
||||
const moveCard = useBoardStore((s) => s.moveCard);
|
||||
const updateCard = useBoardStore((s) => s.updateCard);
|
||||
const duplicateCard = useBoardStore((s) => s.duplicateCard);
|
||||
const deleteCard = useBoardStore((s) => s.deleteCard);
|
||||
|
||||
if (!board) return null;
|
||||
|
||||
const otherColumns = board.columns.filter((c) => c.id !== columnId);
|
||||
const priorities: { value: Priority; label: string }[] = [
|
||||
{ value: "none", label: "None" },
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
{ value: "urgent", label: "Urgent" },
|
||||
];
|
||||
|
||||
return (
|
||||
<ContextMenuContent>
|
||||
{otherColumns.length > 0 && (
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
{otherColumns.map((col) => (
|
||||
<ContextMenuItem
|
||||
key={col.id}
|
||||
onClick={() => moveCard(cardId, columnId, col.id, col.cardIds.length)}
|
||||
>
|
||||
{col.title}
|
||||
</ContextMenuItem>
|
||||
))}
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
)}
|
||||
</motion.button>
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger>Set priority</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
{priorities.map(({ value, label }) => (
|
||||
<ContextMenuItem
|
||||
key={value}
|
||||
onClick={() => updateCard(cardId, { priority: value })}
|
||||
>
|
||||
{label}
|
||||
</ContextMenuItem>
|
||||
))}
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuItem onClick={() => duplicateCard(cardId)}>
|
||||
Duplicate
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => deleteCard(cardId)}
|
||||
>
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,11 @@ import { MoreHorizontal } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
@@ -17,7 +20,6 @@ import type { Column, ColumnWidth } from "@/types/board";
|
||||
interface ColumnHeaderProps {
|
||||
column: Column;
|
||||
cardCount: number;
|
||||
boardColor?: string;
|
||||
}
|
||||
|
||||
const COLOR_PRESETS = [
|
||||
@@ -33,7 +35,7 @@ const COLOR_PRESETS = [
|
||||
{ hue: "0", label: "Slate" },
|
||||
];
|
||||
|
||||
export function ColumnHeader({ column, cardCount, boardColor }: ColumnHeaderProps) {
|
||||
export function ColumnHeader({ column, cardCount }: ColumnHeaderProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(column.title);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -42,6 +44,8 @@ export function ColumnHeader({ column, cardCount, boardColor }: ColumnHeaderProp
|
||||
const deleteColumn = useBoardStore((s) => s.deleteColumn);
|
||||
const setColumnWidth = useBoardStore((s) => s.setColumnWidth);
|
||||
const setColumnColor = useBoardStore((s) => s.setColumnColor);
|
||||
const setColumnWipLimit = useBoardStore((s) => s.setColumnWipLimit);
|
||||
const toggleColumnCollapse = useBoardStore((s) => s.toggleColumnCollapse);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing && inputRef.current) {
|
||||
@@ -74,13 +78,7 @@ export function ColumnHeader({ column, cardCount, boardColor }: ColumnHeaderProp
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between border-b border-border px-3 pb-2 pt-3" style={{
|
||||
borderTop: column.color
|
||||
? `3px solid oklch(55% 0.12 ${column.color})`
|
||||
: boardColor
|
||||
? `3px solid ${boardColor}30`
|
||||
: undefined
|
||||
}}>
|
||||
<div className="flex items-center justify-between border-b border-border px-3 pb-2 pt-3">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
{editing ? (
|
||||
<input
|
||||
@@ -102,8 +100,14 @@ export function ColumnHeader({ column, cardCount, boardColor }: ColumnHeaderProp
|
||||
{column.title}
|
||||
</span>
|
||||
)}
|
||||
<span className="shrink-0 font-mono text-xs text-pylon-text-secondary">
|
||||
{cardCount}
|
||||
<span className={`shrink-0 font-mono text-xs ${
|
||||
column.wipLimit != null && cardCount > column.wipLimit
|
||||
? "text-pylon-danger font-bold"
|
||||
: column.wipLimit != null && cardCount === column.wipLimit
|
||||
? "text-[oklch(65%_0.15_70)]"
|
||||
: "text-pylon-text-secondary"
|
||||
}`}>
|
||||
{cardCount}{column.wipLimit != null ? `/${column.wipLimit}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -126,38 +130,28 @@ export function ColumnHeader({ column, cardCount, boardColor }: ColumnHeaderProp
|
||||
>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toggleColumnCollapse(column.id)}>
|
||||
Collapse
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Width</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={() => handleWidthChange("narrow")}>
|
||||
Narrow
|
||||
{column.width === "narrow" && (
|
||||
<span className="ml-auto text-pylon-accent">*</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleWidthChange("standard")}>
|
||||
Standard
|
||||
{column.width === "standard" && (
|
||||
<span className="ml-auto text-pylon-accent">*</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleWidthChange("wide")}>
|
||||
Wide
|
||||
{column.width === "wide" && (
|
||||
<span className="ml-auto text-pylon-accent">*</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuRadioGroup value={column.width} onValueChange={(v) => handleWidthChange(v as ColumnWidth)}>
|
||||
<DropdownMenuRadioItem value="narrow">Narrow</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="standard">Standard</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="wide">Wide</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Color</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={() => setColumnColor(column.id, null)}>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={column.color == null}
|
||||
onSelect={() => setColumnColor(column.id, null)}
|
||||
>
|
||||
None
|
||||
{column.color == null && (
|
||||
<span className="ml-auto text-pylon-accent">*</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex flex-wrap gap-1.5 px-2 py-1.5">
|
||||
{COLOR_PRESETS.map(({ hue, label }) => (
|
||||
@@ -176,6 +170,21 @@ export function ColumnHeader({ column, cardCount, boardColor }: ColumnHeaderProp
|
||||
</div>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>WIP Limit</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={column.wipLimit?.toString() ?? "none"}
|
||||
onValueChange={(v) => setColumnWipLimit(column.id, v === "none" ? null : parseInt(v))}
|
||||
>
|
||||
<DropdownMenuRadioItem value="none">None</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="3">3</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="5">5</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="7">7</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="10">10</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Plus, ChevronRight } from "lucide-react";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { fadeSlideUp, springs, staggerContainer } from "@/lib/motion";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ColumnHeader } from "@/components/board/ColumnHeader";
|
||||
import { AddCardInput } from "@/components/board/AddCardInput";
|
||||
import { CardThumbnail } from "@/components/board/CardThumbnail";
|
||||
@@ -26,11 +26,13 @@ const WIDTH_MAP = {
|
||||
interface KanbanColumnProps {
|
||||
column: Column;
|
||||
onCardClick?: (cardId: string) => void;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
|
||||
export function KanbanColumn({ column, onCardClick, isNew }: KanbanColumnProps) {
|
||||
const [showAddCard, setShowAddCard] = useState(false);
|
||||
const board = useBoardStore((s) => s.board);
|
||||
const toggleColumnCollapse = useBoardStore((s) => s.toggleColumnCollapse);
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
|
||||
const width = WIDTH_MAP[column.width];
|
||||
@@ -54,31 +56,68 @@ export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
|
||||
data: { type: "column", columnId: column.id },
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : undefined,
|
||||
width,
|
||||
};
|
||||
const borderTop = column.color
|
||||
? `3px solid oklch(55% 0.12 ${column.color})`
|
||||
: board?.color
|
||||
? `3px solid ${board.color}30`
|
||||
: undefined;
|
||||
|
||||
const cardCount = column.cardIds.length;
|
||||
|
||||
const wipTint = column.wipLimit != null
|
||||
? cardCount > column.wipLimit
|
||||
? "oklch(70% 0.08 25 / 15%)"
|
||||
: cardCount === column.wipLimit
|
||||
? "oklch(75% 0.08 70 / 15%)"
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
<motion.div
|
||||
ref={setSortableNodeRef}
|
||||
style={style}
|
||||
className="group/column flex shrink-0 flex-col rounded-lg bg-pylon-column"
|
||||
aria-label={`${column.title} column, ${cardCount} card${cardCount !== 1 ? "s" : ""}`}
|
||||
variants={fadeSlideUp}
|
||||
initial={prefersReducedMotion ? false : "hidden"}
|
||||
animate="visible"
|
||||
style={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : undefined,
|
||||
}}
|
||||
animate={{ width: column.collapsed ? 40 : width, opacity: 1 }}
|
||||
initial={isNew ? { width: 0, opacity: 0 } : false}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={springs.bouncy}
|
||||
layout
|
||||
className="shrink-0 overflow-hidden"
|
||||
{...attributes}
|
||||
>
|
||||
{column.collapsed ? (
|
||||
<button
|
||||
onClick={() => toggleColumnCollapse(column.id)}
|
||||
className="flex h-full w-full flex-col items-center gap-2 rounded-lg bg-pylon-column py-3 hover:bg-pylon-column/80 transition-colors"
|
||||
style={{ borderTop }}
|
||||
{...listeners}
|
||||
>
|
||||
<ChevronRight className="size-3.5 text-pylon-text-secondary" />
|
||||
<span
|
||||
className="font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary"
|
||||
style={{ writingMode: "vertical-rl", transform: "rotate(180deg)" }}
|
||||
>
|
||||
{column.title}
|
||||
</span>
|
||||
<span className="mt-auto font-mono text-xs text-pylon-text-secondary">
|
||||
{cardCount}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<motion.section
|
||||
className="group/column flex h-full flex-col overflow-hidden rounded-lg bg-pylon-column"
|
||||
aria-label={`${column.title} column, ${cardCount} card${cardCount !== 1 ? "s" : ""}`}
|
||||
style={{ borderTop, backgroundColor: wipTint }}
|
||||
variants={fadeSlideUp}
|
||||
initial={isNew || prefersReducedMotion ? false : undefined}
|
||||
animate={isNew ? "visible" : undefined}
|
||||
transition={springs.bouncy}
|
||||
>
|
||||
{/* The column header is the drag handle for column reordering */}
|
||||
<div {...listeners}>
|
||||
<ColumnHeader column={column} cardCount={column.cardIds.length} boardColor={board?.color} />
|
||||
<ColumnHeader column={column} cardCount={column.cardIds.length} />
|
||||
</div>
|
||||
|
||||
{/* Card list - wrapped in SortableContext for within-column sorting */}
|
||||
@@ -86,7 +125,11 @@ export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
|
||||
items={column.cardIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<ScrollArea className="flex-1 overflow-y-auto">
|
||||
<OverlayScrollbarsComponent
|
||||
className="flex-1"
|
||||
options={{ scrollbars: { theme: "os-theme-pylon", autoHide: "scroll", autoHideDelay: 600, clickScroll: true }, overflow: { x: "hidden" } }}
|
||||
defer
|
||||
>
|
||||
<motion.ul
|
||||
ref={setDroppableNodeRef}
|
||||
className="flex min-h-[40px] list-none flex-col"
|
||||
@@ -115,7 +158,7 @@ export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
|
||||
</li>
|
||||
)}
|
||||
</motion.ul>
|
||||
</ScrollArea>
|
||||
</OverlayScrollbarsComponent>
|
||||
</SortableContext>
|
||||
|
||||
{/* Add card section */}
|
||||
@@ -138,5 +181,7 @@ export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
|
||||
</div>
|
||||
)}
|
||||
</motion.section>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user