diff --git a/README.md b/README.md index d1c0562..0fa6be3 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,16 @@ A free, open-source, accessible, dark-mode-first kanban board — a calm alterna ## Status -**Slice 1 — local single-user board (in progress).** +**Slice 1 — local single-user board (complete).** Boards, lists, and cards work end to end — create, rename, delete, and reorder lists inside a board, add cards to a list, move a card within its list or to another list, label them, delete a card with a one-click undo, open a card into a focused task view with an Edit mode, keep a per-card checklist (add, rename, reorder, check off into a collapsible Done strip, delete with undo) with progress shown on the board's card chips, and tune it all in a small settings screen — saved to SQLite, accessible and dark-mode-first. -- **Done:** the board, list, card, label, and checklist backend (JSON APIs behind `IBoardRepository`, `IListRepository`, `ICardRepository`, `ILabelRepository`, and `IChecklistItemRepository` seams, EF Core + SQLite, 147 NUnit tests, localhost-only) and the vanilla-JS MVC frontend (board-view navigation, accessible list reordering, card chips with a focused task view, accessible card moving with up/down buttons and a move-to-list dropdown, an inline label picker with soft-tint chips, a per-card checklist with a Done strip and chip progress bars, an undo-first delete for cards and checklist items with a transient "Deleted · Undo" toast, a task-view Edit mode, a localStorage settings screen gating the card Done checkboxes and the Delete card button, screen-reader announcements, keyboard focus management). -- **Next:** mobile + accessibility polish (per-list Done strips and a single-list phone view). +- **Done:** the board, list, card, label, and checklist backend (JSON APIs behind `IBoardRepository`, `IListRepository`, `ICardRepository`, `ILabelRepository`, and `IChecklistItemRepository` seams, EF Core + SQLite, 147 NUnit tests, localhost-only) and the vanilla-JS MVC frontend (board-view navigation, accessible list reordering, card chips with a focused task view, accessible card moving with up/down buttons and a move-to-list dropdown, an inline label picker with soft-tint chips, a per-card checklist with a Done strip and chip progress bars, an undo-first delete for cards and checklist items with a transient "Deleted · Undo" toast, a task-view Edit mode, a localStorage settings screen gating the card Done checkboxes and the Delete card button, screen-reader announcements, keyboard focus management, per-list Done strips, and a mobile single-list switcher). +- **Next:** Slice 2 — sharing and multi-user accounts. -Design specs: [`docs/2026-06-15-wend-slice1-design.md`](docs/2026-06-15-wend-slice1-design.md), [`docs/2026-06-19-wend-lists-design.md`](docs/2026-06-19-wend-lists-design.md), [`docs/2026-06-22-wend-cards-design.md`](docs/2026-06-22-wend-cards-design.md), [`docs/2026-06-23-wend-labels-design.md`](docs/2026-06-23-wend-labels-design.md), [`docs/2026-06-24-wend-card-moving-design.md`](docs/2026-06-24-wend-card-moving-design.md), [`docs/2026-06-25-wend-done-design.md`](docs/2026-06-25-wend-done-design.md), [`docs/2026-07-07-wend-delete-undo-design.md`](docs/2026-07-07-wend-delete-undo-design.md), [`docs/2026-07-07-wend-checklist-design.md`](docs/2026-07-07-wend-checklist-design.md) +Design specs: [`docs/2026-06-15-wend-slice1-design.md`](docs/2026-06-15-wend-slice1-design.md), [`docs/2026-06-19-wend-lists-design.md`](docs/2026-06-19-wend-lists-design.md), [`docs/2026-06-22-wend-cards-design.md`](docs/2026-06-22-wend-cards-design.md), [`docs/2026-06-23-wend-labels-design.md`](docs/2026-06-23-wend-labels-design.md), [`docs/2026-06-24-wend-card-moving-design.md`](docs/2026-06-24-wend-card-moving-design.md), [`docs/2026-06-25-wend-done-design.md`](docs/2026-06-25-wend-done-design.md), [`docs/2026-07-07-wend-delete-undo-design.md`](docs/2026-07-07-wend-delete-undo-design.md), [`docs/2026-07-07-wend-checklist-design.md`](docs/2026-07-07-wend-checklist-design.md), [`docs/2026-07-08-wend-mobile-a11y-polish-design.md`](docs/2026-07-08-wend-mobile-a11y-polish-design.md) -Build plans: [`docs/plans/2026-06-16-slice1-foundation-boards.md`](docs/plans/2026-06-16-slice1-foundation-boards.md), [`docs/plans/2026-06-19-slice1-lists.md`](docs/plans/2026-06-19-slice1-lists.md), [`docs/plans/2026-06-22-slice1-cards.md`](docs/plans/2026-06-22-slice1-cards.md), [`docs/plans/2026-06-23-slice1-labels.md`](docs/plans/2026-06-23-slice1-labels.md), [`docs/plans/2026-06-24-slice1-card-moving.md`](docs/plans/2026-06-24-slice1-card-moving.md), [`docs/plans/2026-06-25-slice1-done.md`](docs/plans/2026-06-25-slice1-done.md), [`docs/plans/2026-07-07-slice1-delete-undo.md`](docs/plans/2026-07-07-slice1-delete-undo.md), [`docs/plans/2026-07-07-slice1-checklist.md`](docs/plans/2026-07-07-slice1-checklist.md) +Build plans: [`docs/plans/2026-06-16-slice1-foundation-boards.md`](docs/plans/2026-06-16-slice1-foundation-boards.md), [`docs/plans/2026-06-19-slice1-lists.md`](docs/plans/2026-06-19-slice1-lists.md), [`docs/plans/2026-06-22-slice1-cards.md`](docs/plans/2026-06-22-slice1-cards.md), [`docs/plans/2026-06-23-slice1-labels.md`](docs/plans/2026-06-23-slice1-labels.md), [`docs/plans/2026-06-24-slice1-card-moving.md`](docs/plans/2026-06-24-slice1-card-moving.md), [`docs/plans/2026-06-25-slice1-done.md`](docs/plans/2026-06-25-slice1-done.md), [`docs/plans/2026-07-07-slice1-delete-undo.md`](docs/plans/2026-07-07-slice1-delete-undo.md), [`docs/plans/2026-07-07-slice1-checklist.md`](docs/plans/2026-07-07-slice1-checklist.md), [`docs/plans/2026-07-08-slice1-mobile-a11y-polish.md`](docs/plans/2026-07-08-slice1-mobile-a11y-polish.md) ## Stack diff --git a/Wend.Api/wwwroot/css/app.css b/Wend.Api/wwwroot/css/app.css index d317326..15bb40f 100644 --- a/Wend.Api/wwwroot/css/app.css +++ b/Wend.Api/wwwroot/css/app.css @@ -38,6 +38,13 @@ flex-wrap: wrap; } +/* Mobile single-list switcher: view one list at a time; all columns show at ≥768px. */ +.list-switcher-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; } +.list-switcher-label { font-weight: 600; } +.list-switcher { flex: 1; min-height: 44px; max-width: 100%; } +.list-columns > .list-card { display: none; } +.list-columns > .list-card.is-current { display: flex; } + @media (min-width: 768px) { .list-columns { flex-direction: row; @@ -45,6 +52,8 @@ overflow-x: auto; } .list-card { flex: 0 0 16rem; } + .list-switcher-row { display: none; } + .list-columns > .list-card { display: flex; } /* every column visible on desktop */ } .app-header { @@ -296,8 +305,7 @@ .card-done { margin: 0 0 1rem; } .card-done-label { display: inline-flex; align-items: center; gap: 0.5rem; min-height: 44px; cursor: pointer; } -/* Global Done area below the lists. */ -.done-area { margin-top: 1.5rem; } +/* Shared strip disclosure — used by the per-list board Done strips and the checklist Done strip. */ .done-toggle { width: 100%; text-align: left; @@ -308,19 +316,6 @@ border-color: color-mix(in srgb, currentColor 18%, transparent); border-radius: 0.5rem; } -.done-body { margin-top: 0.6rem; display: flex; flex-direction: column; gap: 0.75rem; } -.done-group-title { margin: 0 0 0.3rem; font-size: 0.85rem; opacity: 0.75; } -.done-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; } -.done-row-label { - display: inline-flex; - align-items: center; - gap: 0.5rem; - min-height: 44px; - cursor: pointer; - opacity: 0.7; -} -.done-row-title { text-decoration: line-through; } -.done-row .card-done-toggle { margin-top: 0; } /* ---- Undo toast (uses the design-system .toast-region / .toast component) ---- */ .toast { align-items: center; } diff --git a/Wend.Api/wwwroot/js/board/controller.js b/Wend.Api/wwwroot/js/board/controller.js index b587cb6..2cd8180 100644 --- a/Wend.Api/wwwroot/js/board/controller.js +++ b/Wend.Api/wwwroot/js/board/controller.js @@ -1,12 +1,22 @@ +import { setSelectedListId } from "../prefs.js"; + // Wires the board view to the model: announces results, manages focus, confirms list deletes, // turns move-left/right into a target position, and forwards card actions (reorder + move-to-list). // onBack() returns to the overview; onOpenCard(cardId) opens a card's task view. export function createBoardController(model, view, announce, { onBack, onOpenCard } = {}) { let lists = []; + let boardId = null; view.bindActions({ back: () => onBack?.(), openCard: (cardId) => onOpenCard?.(cardId), + selectList: (listId) => { + const list = lists.find((l) => l.id === listId); + if (!list) return; + setSelectedListId(boardId, listId); + const count = (list.cards ?? []).filter((c) => !c.completedAt).length; + announce(`Showing ${list.title}, ${count} ${count === 1 ? "card" : "cards"}.`); + }, create: async (title) => { if (!title) return; try { @@ -63,13 +73,14 @@ export function createBoardController(model, view, announce, { onBack, onOpenCar } }, toggleDone: async (cardId, completed) => { - const card = lists.flatMap((l) => l.cards ?? []).find((c) => c.id === cardId); + const list = lists.find((l) => (l.cards ?? []).some((c) => c.id === cardId)); + const card = list?.cards?.find((c) => c.id === cardId); const title = card ? card.title : "the card"; try { await model.setCardDone(cardId, completed); if (completed) { announce(`Marked done: ${title}.`); - view.focusDoneToggle(); + if (list) view.focusListDoneToggle(list.id); else view.focusHeading(); } else { announce(`Restored: ${title}.`); view.focusCard(cardId); @@ -112,6 +123,7 @@ export function createBoardController(model, view, announce, { onBack, onOpenCar model.subscribe((board) => { lists = board.lists; + boardId = board.id; view.render(board); }); } diff --git a/Wend.Api/wwwroot/js/board/view.js b/Wend.Api/wwwroot/js/board/view.js index 0f92e17..83c29b7 100644 --- a/Wend.Api/wwwroot/js/board/view.js +++ b/Wend.Api/wwwroot/js/board/view.js @@ -1,13 +1,13 @@ import { escapeHtml } from "../escape.js"; -import { getPrefs } from "../prefs.js"; +import { getPrefs, getSelectedListId } from "../prefs.js"; // Renders one board's view: back link, title, add-list form, each list with its move/rename/delete -// controls, its ACTIVE cards (a leading done checkbox + label chips + move controls) and an add-card -// form, then a collapsible global Done area (done cards grouped by list, each with an un-check). -// "Done" is a render grouping on completedAt; the collapse state lives here (ui.doneOpen). +// controls, its ACTIVE cards (a leading done checkbox + label chips + move controls), a collapsible +// per-list Done strip (that list's completed cards, each with an un-check), and an add-card form. +// "Done" is a render grouping on completedAt; per-list collapse state lives here (ui.doneOpenLists). export function createBoardView(root) { let lastBoard = null; - const ui = { doneOpen: false, renamingId: null }; + const ui = { doneOpenLists: new Set(), renamingId: null, selectedListId: null }; function render(board) { lastBoard = board; @@ -17,6 +17,13 @@ export function createBoardView(root) { function paint() { const board = lastBoard; const lists = board.lists; + // Resolve the mobile switcher's current list: keep a valid selection, else the remembered + // one (validated), else the first list. Self-heals when the selected list is deleted. + const validIds = new Set(lists.map((l) => l.id)); + if (!validIds.has(ui.selectedListId)) { + const remembered = getSelectedListId(board.id); + ui.selectedListId = validIds.has(remembered) ? remembered : (lists[0]?.id ?? null); + } const labelsById = new Map((board.labels ?? []).map((l) => [l.id, l])); const prefs = getPrefs(); @@ -77,8 +84,25 @@ export function createBoardView(root) { `; }) .join(""); + const doneCards = (l.cards ?? []).filter((c) => c.completedAt); + const doneOpen = ui.doneOpenLists.has(l.id); + const doneStrip = doneCards.length ? ` +