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:
186
src/components/card-detail/LabelPicker.tsx
Normal file
186
src/components/card-detail/LabelPicker.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { useState } from "react";
|
||||
import { Plus, Check } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { useBoardStore } from "@/stores/board-store";
|
||||
import type { Label } from "@/types/board";
|
||||
|
||||
interface LabelPickerProps {
|
||||
cardId: string;
|
||||
cardLabelIds: string[];
|
||||
boardLabels: Label[];
|
||||
}
|
||||
|
||||
const COLOR_SWATCHES = [
|
||||
"#ef4444", // red
|
||||
"#f97316", // orange
|
||||
"#eab308", // yellow
|
||||
"#22c55e", // green
|
||||
"#06b6d4", // cyan
|
||||
"#3b82f6", // blue
|
||||
"#8b5cf6", // violet
|
||||
"#ec4899", // pink
|
||||
];
|
||||
|
||||
export function LabelPicker({
|
||||
cardId,
|
||||
cardLabelIds,
|
||||
boardLabels,
|
||||
}: LabelPickerProps) {
|
||||
const toggleCardLabel = useBoardStore((s) => s.toggleCardLabel);
|
||||
const addLabel = useBoardStore((s) => s.addLabel);
|
||||
|
||||
const [newLabelName, setNewLabelName] = useState("");
|
||||
const [newLabelColor, setNewLabelColor] = useState(COLOR_SWATCHES[0]);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
|
||||
const currentLabels = boardLabels.filter((l) => cardLabelIds.includes(l.id));
|
||||
|
||||
function handleCreateLabel() {
|
||||
const trimmed = newLabelName.trim();
|
||||
if (trimmed) {
|
||||
addLabel(trimmed, newLabelColor);
|
||||
setNewLabelName("");
|
||||
setShowCreate(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleCreateLabel();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-mono text-xs uppercase tracking-widest text-pylon-text-secondary">
|
||||
Labels
|
||||
</h4>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-pylon-text-secondary hover:text-pylon-text"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-64 bg-pylon-surface p-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-mono text-xs uppercase tracking-widest text-pylon-text-secondary">
|
||||
Toggle labels
|
||||
</p>
|
||||
|
||||
{/* Existing labels */}
|
||||
{boardLabels.length > 0 && (
|
||||
<div className="flex max-h-40 flex-col gap-1 overflow-y-auto">
|
||||
{boardLabels.map((label) => {
|
||||
const isSelected = cardLabelIds.includes(label.id);
|
||||
return (
|
||||
<button
|
||||
key={label.id}
|
||||
onClick={() => toggleCardLabel(cardId, label.id)}
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors hover:bg-pylon-column"
|
||||
>
|
||||
<span
|
||||
className="size-3 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: label.color }}
|
||||
/>
|
||||
<span className="flex-1 truncate text-pylon-text">
|
||||
{label.name}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<Check className="size-3.5 shrink-0 text-pylon-accent" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create new label */}
|
||||
{showCreate ? (
|
||||
<div className="flex flex-col gap-2 border-t border-pylon-text-secondary/20 pt-2">
|
||||
<input
|
||||
autoFocus
|
||||
value={newLabelName}
|
||||
onChange={(e) => setNewLabelName(e.target.value)}
|
||||
onKeyDown={handleCreateKeyDown}
|
||||
placeholder="Label name..."
|
||||
className="h-7 rounded-md bg-pylon-column px-2 text-sm text-pylon-text outline-none placeholder:text-pylon-text-secondary/60 focus:ring-1 focus:ring-pylon-accent"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{COLOR_SWATCHES.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => setNewLabelColor(color)}
|
||||
className="size-5 rounded-full transition-transform hover:scale-110"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
outline:
|
||||
newLabelColor === color
|
||||
? "2px solid var(--pylon-accent)"
|
||||
: "none",
|
||||
outlineOffset: "1px",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button size="xs" onClick={handleCreateLabel}>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setShowCreate(false);
|
||||
setNewLabelName("");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="border-t border-pylon-text-secondary/20 pt-2 text-left text-xs text-pylon-text-secondary transition-colors hover:text-pylon-text"
|
||||
>
|
||||
+ Create label
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Current labels display */}
|
||||
{currentLabels.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{currentLabels.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium text-white"
|
||||
style={{ backgroundColor: label.color }}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs italic text-pylon-text-secondary/60">
|
||||
No labels
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user