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

@@ -0,0 +1,82 @@
import { format, formatDistanceToNow, isPast, isToday } from "date-fns";
import { Button } from "@/components/ui/button";
import { useBoardStore } from "@/stores/board-store";
interface DueDatePickerProps {
cardId: string;
dueDate: string | null;
}
export function DueDatePicker({ cardId, dueDate }: DueDatePickerProps) {
const updateCard = useBoardStore((s) => s.updateCard);
const dateObj = dueDate ? new Date(dueDate) : null;
const overdue = dateObj != null && isPast(dateObj) && !isToday(dateObj);
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const val = e.target.value;
updateCard(cardId, { dueDate: val || null });
}
function handleClear() {
updateCard(cardId, { dueDate: null });
}
// Format the date value for the HTML date input (YYYY-MM-DD)
const inputValue = dateObj
? format(dateObj, "yyyy-MM-dd")
: "";
return (
<div className="flex flex-col gap-2">
{/* Header */}
<h4 className="font-mono text-xs uppercase tracking-widest text-pylon-text-secondary">
Due Date
</h4>
{/* Current date display */}
{dateObj && (
<div className="flex items-center gap-2">
<span
className={`text-sm font-medium ${
overdue ? "text-pylon-danger" : "text-pylon-text"
}`}
>
{format(dateObj, "MMM d, yyyy")}
</span>
<span
className={`text-xs ${
overdue ? "text-pylon-danger" : "text-pylon-text-secondary"
}`}
>
{overdue
? `overdue by ${formatDistanceToNow(dateObj)}`
: isToday(dateObj)
? "today"
: `in ${formatDistanceToNow(dateObj)}`}
</span>
</div>
)}
{/* Date input + clear */}
<div className="flex items-center gap-2">
<input
type="date"
value={inputValue}
onChange={handleChange}
className="h-7 rounded-md border border-pylon-text-secondary/20 bg-pylon-column px-2 text-xs text-pylon-text outline-none focus:border-pylon-accent focus:ring-1 focus:ring-pylon-accent"
/>
{dueDate && (
<Button
variant="ghost"
size="xs"
onClick={handleClear}
className="text-pylon-text-secondary hover:text-pylon-danger"
>
Clear
</Button>
)}
</div>
</div>
);
}