feat: add two-panel card detail modal with markdown, checklist, labels, dates, attachments

- CardDetailModal: two-panel layout (60/40) with inline title editing
- MarkdownEditor: edit/preview toggle with react-markdown + remark-gfm
- ChecklistSection: add/toggle/edit/delete items with progress counter
- LabelPicker: toggle labels + create new labels with color swatches
- DueDatePicker: date input with relative time and overdue styling
- AttachmentSection: list with remove, placeholder add button
- Wired into BoardView via selectedCardId state
This commit is contained in:
Your Name
2026-02-15 19:05:02 +02:00
parent 86de747bc4
commit b527d441e3
9 changed files with 796 additions and 5 deletions

View File

@@ -23,6 +23,7 @@ import {
CardOverlay,
ColumnOverlay,
} from "@/components/board/DragOverlayContent";
import { CardDetailModal } from "@/components/card-detail/CardDetailModal";
import type { Board } from "@/types/board";
function findColumnByCardId(board: Board, cardId: string) {
@@ -35,6 +36,7 @@ export function BoardView() {
const moveCard = useBoardStore((s) => s.moveCard);
const moveColumn = useBoardStore((s) => s.moveColumn);
const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
const [addingColumn, setAddingColumn] = useState(false);
const [newColumnTitle, setNewColumnTitle] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
@@ -233,6 +235,7 @@ export function BoardView() {
const columnIds = board.columns.map((c) => c.id);
return (
<>
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
@@ -246,7 +249,11 @@ export function BoardView() {
>
<div className="flex h-full gap-6 overflow-x-auto p-6">
{board.columns.map((column) => (
<KanbanColumn key={column.id} column={column} />
<KanbanColumn
key={column.id}
column={column}
onCardClick={setSelectedCardId}
/>
))}
{/* Add column button / inline input */}
@@ -307,5 +314,11 @@ export function BoardView() {
) : null}
</DragOverlay>
</DndContext>
<CardDetailModal
cardId={selectedCardId}
onClose={() => setSelectedCardId(null)}
/>
</>
);
}

View File

@@ -9,9 +9,10 @@ interface CardThumbnailProps {
card: Card;
boardLabels: Label[];
columnId: string;
onCardClick?: (cardId: string) => void;
}
export function CardThumbnail({ card, boardLabels, columnId }: CardThumbnailProps) {
export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: CardThumbnailProps) {
const {
attributes,
listeners,
@@ -35,8 +36,7 @@ export function CardThumbnail({ card, boardLabels, columnId }: CardThumbnailProp
const overdue = dueDate != null && isPast(dueDate) && !isToday(dueDate);
function handleClick() {
// Card detail modal will be wired in Task 11
console.log("Card clicked:", card.id);
onCardClick?.(card.id);
}
return (

View File

@@ -23,9 +23,10 @@ const WIDTH_MAP = {
interface KanbanColumnProps {
column: Column;
onCardClick?: (cardId: string) => void;
}
export function KanbanColumn({ column }: KanbanColumnProps) {
export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
const [showAddCard, setShowAddCard] = useState(false);
const board = useBoardStore((s) => s.board);
@@ -85,6 +86,7 @@ export function KanbanColumn({ column }: KanbanColumnProps) {
card={card}
boardLabels={board?.labels ?? []}
columnId={column.id}
onCardClick={onCardClick}
/>
);
})}