Skip to content
12 changes: 7 additions & 5 deletions Wend.Api/wwwroot/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
gap: 0.5rem;
}

.list-title { font-weight: 600; }
.list-title { font-weight: 600; margin: 0; font-size: inherit; } /* an <h3> labelling its list region — neutralise heading defaults */

.list-actions {
display: flex;
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions Wend.Api/wwwroot/js/announce.js
Original file line number Diff line number Diff line change
@@ -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);
};
}
8 changes: 3 additions & 5 deletions Wend.Api/wwwroot/js/board/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down
52 changes: 43 additions & 9 deletions Wend.Api/wwwroot/js/board/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,9 +78,14 @@ export function createBoardView(root) {
})
.join("");
return `
<li class="list-card" data-list-id="${l.id}">
<span class="list-title">${escapeHtml(l.title)}</span>
<div class="list-actions">
<li class="list-card" data-list-id="${l.id}" role="group" aria-labelledby="list-${l.id}-title">
<h3 id="list-${l.id}-title" class="list-title">${escapeHtml(l.title)}</h3>
${ui.renamingId === l.id
? `<form class="rename-form" data-action="save-list-rename" data-id="${l.id}">
<input name="text" value="${escapeHtml(l.title)}" aria-label="List name" maxlength="200" required />
<button type="submit">Save</button>
</form>`
: `<div class="list-actions">
<button data-action="move-left" data-id="${l.id}" ${first ? "disabled" : ""}
aria-label="Move list left: ${escapeHtml(l.title)}">◀</button>
<button data-action="move-right" data-id="${l.id}" ${last ? "disabled" : ""}
Expand All @@ -89,7 +94,7 @@ export function createBoardView(root) {
aria-label="Rename list: ${escapeHtml(l.title)}">Rename</button>
<button data-action="delete" data-id="${l.id}"
aria-label="Delete list: ${escapeHtml(l.title)}">Delete</button>
</div>
</div>`}
<ul class="card-list">${cards}</ul>
<form class="card-form" data-action="create-card" data-list-id="${l.id}">
<input name="title" aria-label="Add a card to ${escapeHtml(l.title)}" placeholder="Add a card…" required />
Expand Down Expand Up @@ -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 <body>
}

function focusListAction(id, preferred) {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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 };
}
8 changes: 3 additions & 5 deletions Wend.Api/wwwroot/js/boards/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down
90 changes: 72 additions & 18 deletions Wend.Api/wwwroot/js/boards/view.js
Original file line number Diff line number Diff line change
@@ -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
? `
<li>
<form class="rename-form" data-action="save-rename" data-id="${b.id}">
<input name="text" value="${escapeHtml(b.title)}" aria-label="Board name" maxlength="200" required />
<button type="submit">Save</button>
</form>
</li>`
: `
<li>
<button class="board-open" data-action="open" data-id="${b.id}"
aria-label="Open board: ${escapeHtml(b.title)}">${escapeHtml(b.title)}</button>
Expand Down Expand Up @@ -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 };
}
return { render, focusNewBoardInput, focusOpen, focusRenameTrigger, bindActions };
}
2 changes: 1 addition & 1 deletion Wend.Api/wwwroot/js/card/checklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function renderChecklist(card, ui) {
${ui.doneOpen ? `<ul class="done-items">${done
.map((i) => `
<li class="done-item-row" data-item-id="${i.id}">
<label class="done-row-label">
<label class="checklist-done-label">
<input type="checkbox" data-action="toggle-item" data-item-id="${i.id}" checked
aria-label="Mark not done: ${escapeHtml(i.text)}" />
<span class="done-item-text">${escapeHtml(i.text)}</span>
Expand Down
3 changes: 1 addition & 2 deletions Wend.Api/wwwroot/js/card/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,7 @@ export function createCardController(model, view, announce, {onBack, onDeleted,
}
},
deleteLabel: async (id, name) => {
if (!confirm(`Delete
'${name}'? It will be removed from every card that uses it.`)) return;
if (!confirm(`Delete '${name}'? It will be removed from every card that uses it.`)) return;
try {
await model.deleteLabel(id);
announce("Label deleted.");
Expand Down
2 changes: 1 addition & 1 deletion Wend.Api/wwwroot/js/card/labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function renderLabels(card, palette, ui) {
<section class="labels-section" aria-label="Labels">
<div class="labels-attached">${attachedChips}</div>
<button type="button" class="labels-toggle" data-action="toggle-picker"
aria-haspopup="true" aria-expanded="${ui.pickerOpen ? "true" : "false"}">+ Labels</button>
aria-expanded="${ui.pickerOpen ? "true" : "false"}">+ Labels</button>
${ui.pickerOpen ? `<div class="label-picker" role="group" aria-label="Choose labels">${body}</div>` : ""}
</section>`;
}
Expand Down
11 changes: 7 additions & 4 deletions Wend.Api/wwwroot/js/card/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ export function createCardView(root) {
</div>
<h2 class="card-heading" tabindex="-1">${
ui.renamingId === "title"
? `<form class="rename-form" data-action="save-title">
<input name="text" value="${escapeHtml(card.title)}" aria-label="Card title" maxlength="200" required />
<button type="submit">Save</button>
</form>`
? escapeHtml(card.title)
: `<button type="button" class="rename-trigger" data-action="rename-title"
aria-label="Rename: ${escapeHtml(card.title)}">${escapeHtml(card.title)}</button>`
}</h2>
${ui.renamingId === "title"
? `<form class="rename-form" data-action="save-title">
<input name="text" value="${escapeHtml(card.title)}" aria-label="Card title" maxlength="200" required />
<button type="submit">Save</button>
</form>`
: ""}
<p class="card-list-name">In list: <strong>${escapeHtml(card.listTitle)}</strong></p>
${prefs.showCardDone ? `<div class="card-done">
<label class="card-done-label">
Expand Down
3 changes: 2 additions & 1 deletion Wend.Api/wwwroot/js/settings/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function createSettingsController(model, view, announce, { onBack } = {})
view.bindActions({
back: () => onBack?.(),
toggle: (key, value) => {
model.set(key, value);
model.set(key, value); // notify → view.render rebuilds the checkbox
view.focusPref(key); // return focus to the flipped checkbox
announce(`${NAMES[key] ?? "Setting"} ${value ? "on" : "off"}.`);
},
});
Expand Down
Loading