diff --git a/Wend.Api/wwwroot/css/app.css b/Wend.Api/wwwroot/css/app.css index 6100b62..d317326 100644 --- a/Wend.Api/wwwroot/css/app.css +++ b/Wend.Api/wwwroot/css/app.css @@ -30,7 +30,7 @@ gap: 0.5rem; } -.list-title { font-weight: 600; } +.list-title { font-weight: 600; margin: 0; font-size: inherit; } /* an

labelling its list region — neutralise heading defaults */ .list-actions { display: flex; @@ -290,7 +290,7 @@ /* Active card: a leading done checkbox beside the open button, then the move row. */ .card-row { display: flex; align-items: flex-start; gap: 0.4rem; } .card-row .card-chip { flex: 1; } -.card-done-toggle { width: 1.15rem; height: 1.15rem; margin-top: 0.55rem; flex: none; } +.card-done-toggle { width: 1.5rem; height: 1.5rem; margin-top: 0.35rem; flex: none; } /* Card-detail done toggle. */ .card-done { margin: 0 0 1rem; } @@ -380,6 +380,9 @@ align-items: center; min-height: 44px; /* touch target */ } +/* ≥24px checkbox targets (WCAG 2.5.8); the 44px row supplies the hit area. */ +.checklist-row input[type="checkbox"], +.done-item-row input[type="checkbox"] { width: 1.5rem; height: 1.5rem; } .checklist-row .rename-trigger { flex: 1; text-align: left; @@ -400,9 +403,8 @@ border-color: color-mix(in srgb, currentColor 15%, transparent); padding-top: 0.5rem; } -.done-item-row { opacity: 0.6; } -.done-item-text { text-decoration: line-through; } -.done-row-label { display: flex; gap: 0.5rem; align-items: center; flex: 1; } +.done-item-text { text-decoration: line-through; opacity: 0.7; } /* single 0.7 dim → ~5.36:1 (matches the board Done area); no stacked opacity */ +.checklist-done-label { display: flex; gap: 0.5rem; align-items: center; flex: 1; } /* Task-view header row + edit toggle. */ .card-view-top { diff --git a/Wend.Api/wwwroot/js/announce.js b/Wend.Api/wwwroot/js/announce.js index 3441cd0..524dad1 100644 --- a/Wend.Api/wwwroot/js/announce.js +++ b/Wend.Api/wwwroot/js/announce.js @@ -1,8 +1,10 @@ export function createAnnouncer(region) { return (message) => { region.textContent = ""; - requestAnimationFrame(() => { + // A short timeout (not rAF, which pauses in hidden/throttled tabs) lets the clear register + // so identical consecutive messages still re-announce. + setTimeout(() => { region.textContent = message; - }); + }, 120); }; } diff --git a/Wend.Api/wwwroot/js/board/controller.js b/Wend.Api/wwwroot/js/board/controller.js index 871fd0b..b587cb6 100644 --- a/Wend.Api/wwwroot/js/board/controller.js +++ b/Wend.Api/wwwroot/js/board/controller.js @@ -27,13 +27,11 @@ export function createBoardController(model, view, announce, { onBack, onOpenCar announce("Couldn't add the card — please try again."); } }, - rename: async (id) => { - const title = prompt("New list name?"); - if (!title || !title.trim()) return; + rename: async (id, text) => { try { - await model.rename(id, title.trim()); + await model.rename(id, text); announce("List renamed."); - view.focusNewListInput(); + view.focusListRenameTrigger(id); } catch { announce("Couldn't rename the list — please try again."); } diff --git a/Wend.Api/wwwroot/js/board/view.js b/Wend.Api/wwwroot/js/board/view.js index 7d2ae65..0f92e17 100644 --- a/Wend.Api/wwwroot/js/board/view.js +++ b/Wend.Api/wwwroot/js/board/view.js @@ -7,7 +7,7 @@ import { getPrefs } from "../prefs.js"; // "Done" is a render grouping on completedAt; the collapse state lives here (ui.doneOpen). export function createBoardView(root) { let lastBoard = null; - const ui = { doneOpen: false }; + const ui = { doneOpen: false, renamingId: null }; function render(board) { lastBoard = board; @@ -78,9 +78,14 @@ export function createBoardView(root) { }) .join(""); return ` -
  • - ${escapeHtml(l.title)} -
    +
  • +

    ${escapeHtml(l.title)}

    + ${ui.renamingId === l.id + ? `
    + + +
    ` + : `
    -
    + `}
    @@ -154,7 +159,9 @@ export function createBoardView(root) { root.querySelector(`.card-chip[data-card-id="${cardId}"]`)?.focus(); } function focusDoneToggle() { - root.querySelector(".done-toggle")?.focus(); + const t = root.querySelector(".done-toggle"); + if (t) t.focus(); + else focusHeading(); // fallback so a vanished toggle can't strand focus on } function focusListAction(id, preferred) { @@ -183,9 +190,33 @@ export function createBoardView(root) { item.querySelector(".card-chip")?.focus(); } + function focusListRenameTrigger(id) { + root.querySelector(`[data-action="rename"][data-id="${id}"]`)?.focus(); + } + // Enter / leave inline list rename (no server): flip ui + repaint + place focus. + function startListRename(id) { + ui.renamingId = id; + paint(); + root.querySelector(".rename-form input")?.select(); + } + function cancelListRename() { + const id = ui.renamingId; + ui.renamingId = null; + paint(); + if (id != null) focusListRenameTrigger(id); + } + function bindActions(handlers) { root.addEventListener("submit", async (e) => { const action = e.target.dataset.action; + if (action === "save-list-rename") { + e.preventDefault(); + const text = e.target.elements["text"].value.trim(); + if (!text) return; + ui.renamingId = null; + await handlers.rename(Number(e.target.dataset.id), text); + return; + } if (action !== "create" && action !== "create-card") return; e.preventDefault(); const title = e.target.title.value.trim(); @@ -202,13 +233,13 @@ export function createBoardView(root) { const btn = e.target.closest("button[data-action]"); if (!btn || btn.dataset.action === "create" || btn.dataset.action === "create-card") return; const action = btn.dataset.action; - if (action === "toggle-done-section") { ui.doneOpen = !ui.doneOpen; paint(); return; } + if (action === "toggle-done-section") { ui.doneOpen = !ui.doneOpen; paint(); focusDoneToggle(); return; } if (action === "back") return handlers.back(); if (action === "open-card") return handlers.openCard(Number(btn.dataset.cardId)); if (action === "card-up") return handlers.cardUp(Number(btn.dataset.cardId)); if (action === "card-down") return handlers.cardDown(Number(btn.dataset.cardId)); const id = Number(btn.dataset.id); - if (action === "rename") handlers.rename(id); + if (action === "rename") startListRename(id); else if (action === "delete") handlers.delete(id); else if (action === "move-left") handlers.moveLeft(id); else if (action === "move-right") handlers.moveRight(id); @@ -222,7 +253,10 @@ export function createBoardView(root) { if (!listId) return; handlers.moveCardTo(Number(sel.dataset.cardId), listId); }); + root.addEventListener("keydown", (e) => { + if (e.key === "Escape" && ui.renamingId != null) { e.stopPropagation(); cancelListRename(); } + }); } - return { render, focusHeading, focusNewListInput, focusNewCardInput, focusCard, focusDoneToggle, focusListAction, focusCardAction, bindActions }; + return { render, focusHeading, focusNewListInput, focusNewCardInput, focusCard, focusDoneToggle, focusListAction, focusCardAction, focusListRenameTrigger, bindActions }; } diff --git a/Wend.Api/wwwroot/js/boards/controller.js b/Wend.Api/wwwroot/js/boards/controller.js index 9c6b4ec..8dd1692 100644 --- a/Wend.Api/wwwroot/js/boards/controller.js +++ b/Wend.Api/wwwroot/js/boards/controller.js @@ -13,13 +13,11 @@ export function createBoardsController(model, view, announce, { onOpen } = {}) { announce("Couldn't add the board — please try again."); } }, - rename: async (id) => { - const title = prompt("New board name?"); - if (!title || !title.trim()) return; + rename: async (id, text) => { try { - await model.rename(id, title.trim()); + await model.rename(id, text); announce("Board renamed."); - view.focusNewBoardInput(); + view.focusRenameTrigger(id); } catch { announce("Couldn't rename the board — please try again."); } diff --git a/Wend.Api/wwwroot/js/boards/view.js b/Wend.Api/wwwroot/js/boards/view.js index 452216c..18776dc 100644 --- a/Wend.Api/wwwroot/js/boards/view.js +++ b/Wend.Api/wwwroot/js/boards/view.js @@ -1,11 +1,30 @@ // Renders boards (labelled controls) and forwards events via data-action. No fetch, no logic. +// Holds only transient rename UI state — the card/checklist inline-rename pattern (replaces prompt). import { escapeHtml } from "../escape.js"; + export function createBoardsView(root) { + let lastBoards = []; + const ui = { renamingId: null }; // board id being renamed, or null + let h = {}; + function render(boards) { - const items = boards.length - ? boards - .map( - (b) => ` + lastBoards = boards; + paint(); + } + + function paint() { + const items = lastBoards.length + ? lastBoards + .map((b) => + ui.renamingId === b.id + ? ` +
  • + + + +
  • +
  • ` + : `
  • @@ -35,28 +54,63 @@ export function createBoardsView(root) { root.querySelector(`[data-action="open"][data-id="${id}"]`)?.focus(); } + function focusRenameTrigger(id) { + root.querySelector(`[data-action="rename"][data-id="${id}"]`)?.focus(); + } + + // Enter / leave inline rename (no server): flip ui + repaint + place focus. + function startRename(id) { + ui.renamingId = id; + paint(); + root.querySelector(".rename-form input")?.select(); + } + function cancelRename() { + const id = ui.renamingId; + ui.renamingId = null; + paint(); + if (id != null) focusRenameTrigger(id); + else focusNewBoardInput(); + } + function bindActions(handlers) { + h = handlers; + root.addEventListener("submit", async (e) => { - if (e.target.dataset.action !== "create") return; + const action = e.target.dataset.action; e.preventDefault(); - const title = e.target.title.value.trim(); - const submit = e.target.querySelector("button[type=submit]"); - submit.disabled = true; - try { - await handlers.create(title); - } finally { - submit.disabled = false; + if (action === "create") { + const title = e.target.title.value.trim(); + const submit = e.target.querySelector("button[type=submit]"); + submit.disabled = true; + try { + await h.create(title); + } finally { + submit.disabled = false; + } + } else if (action === "save-rename") { + const text = e.target.elements["text"].value.trim(); // elements[] — f.text/f.name would hit form attrs + if (!text) return; + ui.renamingId = null; + await h.rename(Number(e.target.dataset.id), text); } }); + root.addEventListener("click", (e) => { const btn = e.target.closest("button[data-action]"); - if (!btn || btn.dataset.action === "create") return; + if (!btn || btn.dataset.action === "create" || btn.dataset.action === "save-rename") return; const id = Number(btn.dataset.id); - if (btn.dataset.action === "open") handlers.open(id); - else if (btn.dataset.action === "rename") handlers.rename(id); - else if (btn.dataset.action === "delete") handlers.delete(id); + if (btn.dataset.action === "open") h.open(id); + else if (btn.dataset.action === "rename") startRename(id); + else if (btn.dataset.action === "delete") h.delete(id); + }); + + root.addEventListener("keydown", (e) => { + if (e.key === "Escape" && ui.renamingId != null) { + e.stopPropagation(); + cancelRename(); + } }); } - return { render, focusNewBoardInput, focusOpen, bindActions }; -} \ No newline at end of file + return { render, focusNewBoardInput, focusOpen, focusRenameTrigger, bindActions }; +} diff --git a/Wend.Api/wwwroot/js/card/checklist.js b/Wend.Api/wwwroot/js/card/checklist.js index 9be1cf6..800f079 100644 --- a/Wend.Api/wwwroot/js/card/checklist.js +++ b/Wend.Api/wwwroot/js/card/checklist.js @@ -43,7 +43,7 @@ export function renderChecklist(card, ui) { ${ui.doneOpen ? `