From 24d4c752239dbda78f144ca0c071b3b0d9a6930b Mon Sep 17 00:00:00 2001 From: harrydawson Date: Fri, 5 Jun 2026 12:49:22 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20Todo=20Panel=20polish=20=E2=80=94=20emp?= =?UTF-8?q?ty=20state,=20color=20bleed,=20edge=20cases=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - First-time empty state shows centered prompt when no categories exist - Items get lighter variant of category color as subtle background - Empty/whitespace-only item titles rejected at store level (createItem guard) - Enter on empty title keeps input open instead of silently closing - Category name truncation and collapse state preservation verified - 3 new edge case tests --- apps/web/src/components/TodoPanel.tsx | 18 ++++++++++++----- packages/shared/src/todoStore.test.ts | 28 +++++++++++++++++++++++++++ packages/shared/src/todoStore.ts | 2 ++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/TodoPanel.tsx b/apps/web/src/components/TodoPanel.tsx index ac4bf988b857..930736d2dea7 100644 --- a/apps/web/src/components/TodoPanel.tsx +++ b/apps/web/src/components/TodoPanel.tsx @@ -233,7 +233,13 @@ export function TodoPanel() { {loading &&
Loading...
} {error &&
Failed to load todos
} {!loading && !error && filteredCategories.length === 0 && doneItems.length === 0 && ( -
No todos yet
+
+

+ No categories yet. +
+ Click + to create one. +

+
)} {filteredCategories.map((category) => ( @@ -378,12 +384,14 @@ function SortableCategoryHeader({ function SortableTodoItem({ item, + categoryColor, categoryJiraLink, onCycleItem, onSelectItem, mutate, }: { item: TodoItem; + categoryColor: string; categoryJiraLink: string | undefined; onCycleItem: (itemId: string) => void; onSelectItem: (itemId: string) => void; @@ -453,7 +461,7 @@ function SortableTodoItem({ return (
@@ -540,9 +548,8 @@ function TodoCategoryRow({ const commitAdd = () => { const trimmed = addValue.trim(); - if (trimmed) { - mutate([{ type: "createItem", categoryId: category.id, title: trimmed }]); - } + if (!trimmed) return; + mutate([{ type: "createItem", categoryId: category.id, title: trimmed }]); setAdding(false); setAddValue(""); }; @@ -682,6 +689,7 @@ function TodoCategoryRow({ { const created = next.items.find((i) => i.categoryId === "cat-1"); expect(created!.sortOrder).toBe(0); }); + + it("rejects empty title and returns unchanged state", () => { + const state = loadTodos(sampleCategories, sampleItems); + const next = createItem(state, "cat-1", ""); + + expect(next).toBe(state); + expect(next.items).toHaveLength(2); + }); + + it("rejects whitespace-only title and returns unchanged state", () => { + const state = loadTodos(sampleCategories, sampleItems); + const next = createItem(state, "cat-1", " "); + + expect(next).toBe(state); + expect(next.items).toHaveLength(2); + }); + + it("rejects whitespace-only title through applyMutation", () => { + const state = loadTodos(sampleCategories, sampleItems); + const next = applyMutation(state, { + type: "createItem", + categoryId: "cat-1", + title: " ", + }); + + expect(next).toBe(state); + expect(next.items).toHaveLength(2); + }); }); describe("updateDescription", () => { diff --git a/packages/shared/src/todoStore.ts b/packages/shared/src/todoStore.ts index 91cb9d9faf35..6abbecc74903 100644 --- a/packages/shared/src/todoStore.ts +++ b/packages/shared/src/todoStore.ts @@ -114,6 +114,8 @@ export function deleteCategory( } export function createItem(state: TodoState, categoryId: string, title: string): TodoState { + if (!title.trim()) return state; + // @effect-diagnostics-next-line globalDate:off const now = new Date().toISOString(); const categoryItems = state.items.filter((item) => item.categoryId === categoryId);