Send pick-any checks with Other text; keep inbox focus across updates

This commit is contained in:
Ian Gulliver
2026-10-14 23:54:13 -07:00
parent 2b4d92e7d5
commit 75c76eff0a
11 changed files with 184 additions and 134 deletions
+47 -23
View File
@@ -10,6 +10,8 @@ export function mountInbox(root) {
const tabs = h("nav", { class: "scope-tabs" });
const list = h("div", { class: "inbox-list" });
const cards = new Map();
const groups = new Map();
const empty = h("div", { class: "empty" }, "Nothing waiting on you.");
const visible = () => {
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
if (scope === "task" && task) {
@@ -61,12 +63,28 @@ export function mountInbox(root) {
cards.set(item.id, { sig, el });
return el;
};
const els = groupByTask(items).map((g) => {
const task = findTask(g.taskId);
const project = state.projects.find((p) => p.id === g.projectId);
return h("section", { class: `task-group ${g.blocked ? "blocked" : ""}` }, h("h3", {}, h("span", { class: "project-name" }, project?.name ?? g.projectId, " › "), h("a", { href: `#/task/${g.taskId}` }, task?.title ?? g.taskId)), ...g.items.map(cardFor));
const shown = groupByTask(items);
const keepGroups = new Set(shown.map((g) => g.taskId));
for (const id of groups.keys()) {
if (!keepGroups.has(id)) {
groups.delete(id);
}
}
const els = shown.map((g) => {
const projectName = state.projects.find((p) => p.id === g.projectId)?.name ?? g.projectId;
const title = findTask(g.taskId)?.title ?? g.taskId;
const head = JSON.stringify([projectName, title]);
let group = groups.get(g.taskId);
if (!group || group.head !== head) {
const h3 = h("h3", {}, h("span", { class: "project-name" }, projectName, " › "), h("a", { href: `#/task/${g.taskId}` }, title));
group = { head, h3, el: group?.el ?? h("section", {}) };
groups.set(g.taskId, group);
}
group.el.className = `task-group ${g.blocked ? "blocked" : ""}`;
reconcile(group.el, [group.h3, ...g.items.map(cardFor)]);
return group.el;
});
replace(list, ...(els.length > 0 ? els : [h("div", { class: "empty" }, "Nothing waiting on you.")]));
reconcile(list, els.length > 0 ? els : [empty]);
};
const off = onEach(["inbox", "selection", "tasks"], render);
const changes = h("section", { class: "inbox-changes" });
@@ -78,6 +96,17 @@ export function mountInbox(root) {
offChanges();
};
}
function reconcile(parent, nodes) {
nodes.forEach((node, i) => {
const at = parent.childNodes[i];
if (at !== node) {
parent.insertBefore(node, at ?? null);
}
});
while (parent.childNodes.length > nodes.length) {
parent.lastChild.remove();
}
}
function groupByTask(items) {
const groups = new Map();
for (const item of [...items].sort((a, b) => a.created_at.localeCompare(b.created_at))) {
@@ -156,15 +185,12 @@ function card(item) {
const now = (checked.get(item.id) ?? []).filter((o) => o !== text);
if (input.checked) {
now.push(text);
texts.delete(item.id);
}
checked.set(item.id, options.filter((o) => now.includes(o)));
renderActions();
};
boxes.push(input);
return h("label", { class: "option" }, input, " ", inlineMarkdown(text), tag);
};
const boxes = [];
const textEntry = (label, placeholder, shared) => {
const open = texts.has(item.id);
const text = h("textarea", { rows: 3, placeholder, value: texts.get(item.id) ?? "" });
@@ -175,9 +201,12 @@ function card(item) {
block.hidden = true;
editor.hidden = false;
text.focus();
shared?.onOpen();
});
const go = () => {
if (shared) {
shared.onSubmit();
return;
}
if (text.value.trim()) {
void submit({ item_id: item.id, type: "other", text: text.value });
}
@@ -223,23 +252,18 @@ function card(item) {
const refresh = () => {
send.disabled = !typed() && (checked.get(item.id) ?? []).length === 0;
};
const other = textEntry("Other…", placeholder, {
onOpen: () => {
checked.delete(item.id);
for (const box of boxes) {
box.checked = false;
}
refresh();
},
onInput: refresh,
});
send.onclick = () => {
if (typed()) {
void submit({ item_id: item.id, type: "other", text: texts.get(item.id) });
const go = () => {
const options = checked.get(item.id) ?? [];
if (options.length > 0) {
void submit({ item_id: item.id, type: "option", options, text: typed() ? texts.get(item.id) : undefined });
return;
}
void submit({ item_id: item.id, type: "option", options: checked.get(item.id) ?? [] });
if (typed()) {
void submit({ item_id: item.id, type: "other", text: texts.get(item.id) });
}
};
const other = textEntry("Other…", placeholder, { onInput: refresh, onSubmit: go });
send.onclick = go;
refresh();
replace(actions, other, h("div", { class: "submit-row" }, send));
return;