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:
Your Name
2026-02-16 14:46:20 +02:00
parent 8ca3b81e92
commit e535177914
7 changed files with 526 additions and 142 deletions

View File

@@ -28,10 +28,13 @@ interface BoardActions {
moveColumn: (fromIndex: number, toIndex: number) => void;
setColumnWidth: (columnId: string, width: ColumnWidth) => void;
setColumnColor: (columnId: string, color: string | null) => void;
setColumnWipLimit: (columnId: string, limit: number | null) => void;
toggleColumnCollapse: (columnId: string) => void;
addCard: (columnId: string, title: string) => string;
updateCard: (cardId: string, updates: Partial<Card>) => void;
deleteCard: (cardId: string) => void;
duplicateCard: (cardId: string) => string | null;
moveCard: (
cardId: string,
fromColumnId: string,
@@ -48,10 +51,14 @@ interface BoardActions {
toggleChecklistItem: (cardId: string, itemId: string) => void;
updateChecklistItem: (cardId: string, itemId: string, text: string) => void;
deleteChecklistItem: (cardId: string, itemId: string) => void;
reorderChecklistItems: (cardId: string, fromIndex: number, toIndex: number) => void;
addAttachment: (cardId: string, attachment: Omit<Attachment, "id">) => void;
removeAttachment: (cardId: string, attachmentId: string) => void;
addComment: (cardId: string, text: string) => void;
deleteComment: (cardId: string, commentId: string) => void;
updateBoardTitle: (title: string) => void;
updateBoardColor: (color: string) => void;
updateBoardSettings: (settings: Board["settings"]) => void;
@@ -195,6 +202,26 @@ export const useBoardStore = create<BoardState & BoardActions>()(
}));
},
setColumnWipLimit: (columnId, limit) => {
mutate(get, set, (b) => ({
...b,
updatedAt: now(),
columns: b.columns.map((c) =>
c.id === columnId ? { ...c, wipLimit: limit } : c
),
}));
},
toggleColumnCollapse: (columnId) => {
mutate(get, set, (b) => ({
...b,
updatedAt: now(),
columns: b.columns.map((c) =>
c.id === columnId ? { ...c, collapsed: !c.collapsed } : c
),
}));
},
// -- Card actions --
addCard: (columnId, title) => {
@@ -260,6 +287,48 @@ export const useBoardStore = create<BoardState & BoardActions>()(
});
},
duplicateCard: (cardId) => {
const { board } = get();
if (!board) return null;
const original = board.cards[cardId];
if (!original) return null;
const column = board.columns.find((c) => c.cardIds.includes(cardId));
if (!column) return null;
const newId = ulid();
const ts = now();
const clone: Card = {
...original,
id: newId,
title: `${original.title} (copy)`,
comments: [],
createdAt: ts,
updatedAt: ts,
};
const insertIndex = column.cardIds.indexOf(cardId) + 1;
mutate(get, set, (b) => ({
...b,
updatedAt: ts,
cards: { ...b.cards, [newId]: clone },
columns: b.columns.map((c) =>
c.id === column.id
? {
...c,
cardIds: [
...c.cardIds.slice(0, insertIndex),
newId,
...c.cardIds.slice(insertIndex),
],
}
: c
),
}));
return newId;
},
moveCard: (cardId, fromColumnId, toColumnId, toIndex) => {
mutate(get, set, (b) => ({
...b,
@@ -428,6 +497,24 @@ export const useBoardStore = create<BoardState & BoardActions>()(
});
},
reorderChecklistItems: (cardId, fromIndex, toIndex) => {
mutate(get, set, (b) => {
const card = b.cards[cardId];
if (!card) return b;
const items = [...card.checklist];
const [moved] = items.splice(fromIndex, 1);
items.splice(toIndex, 0, moved);
return {
...b,
updatedAt: now(),
cards: {
...b.cards,
[cardId]: { ...card, checklist: items, updatedAt: now() },
},
};
});
},
// -- Attachment actions --
addAttachment: (cardId, attachment) => {
@@ -473,6 +560,47 @@ export const useBoardStore = create<BoardState & BoardActions>()(
});
},
// -- Comment actions --
addComment: (cardId, text) => {
mutate(get, set, (b) => {
const card = b.cards[cardId];
if (!card) return b;
const comment = { id: ulid(), text, createdAt: now() };
return {
...b,
updatedAt: now(),
cards: {
...b.cards,
[cardId]: {
...card,
comments: [comment, ...card.comments],
updatedAt: now(),
},
},
};
});
},
deleteComment: (cardId, commentId) => {
mutate(get, set, (b) => {
const card = b.cards[cardId];
if (!card) return b;
return {
...b,
updatedAt: now(),
cards: {
...b.cards,
[cardId]: {
...card,
comments: card.comments.filter((c) => c.id !== commentId),
updatedAt: now(),
},
},
};
});
},
// -- Board metadata --
updateBoardTitle: (title) => {