Add concurrency caps, the two-pane web UI, AGENTS.md, and plan status
This commit is contained in:
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
import { h, replace } from "./dom.js";
|
||||
import { activeTasks, findTask, onEach, state } from "./state.js";
|
||||
export function mountNavbar(root, showInbox) {
|
||||
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
||||
crumb.onclick = openSwitcher;
|
||||
const inbox = h("button", { class: "inbox-count" });
|
||||
inbox.onclick = showInbox;
|
||||
const conn = h("span", { class: "connection down", title: "connecting" });
|
||||
const me = h("span", { class: "me" });
|
||||
const render = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
replace(crumb, task ? `${project?.name ?? ""} ▸ ${task.title}` : "No task", " ▾");
|
||||
const blocked = state.inbox.filter((i) => i.priority === "blocked").length;
|
||||
inbox.className = blocked > 0 ? "inbox-count urgent" : "inbox-count";
|
||||
replace(inbox, `Inbox ${state.inbox.length}`, blocked > 0 && h("span", { class: "blocked-count" }, ` · ${blocked} blocked`));
|
||||
conn.className = state.connected ? "connection up" : "connection down";
|
||||
conn.title = state.connected ? "live" : "reconnecting";
|
||||
replace(me, state.me?.display_name ?? "");
|
||||
};
|
||||
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, conn, me);
|
||||
render();
|
||||
addEventListener("keydown", (e) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
openSwitcher();
|
||||
}
|
||||
});
|
||||
}
|
||||
function openSwitcher() {
|
||||
if (document.querySelector(".switcher")) {
|
||||
return;
|
||||
}
|
||||
const input = h("input", { placeholder: "Go to task…", autocomplete: "off" });
|
||||
const list = h("ul", { class: "switcher-list" });
|
||||
const backdrop = h("div", { class: "switcher-backdrop" });
|
||||
const box = h("div", { class: "switcher" }, input, list);
|
||||
let selected = 0;
|
||||
let matches = [];
|
||||
const close = () => {
|
||||
backdrop.remove();
|
||||
box.remove();
|
||||
};
|
||||
const go = (taskId) => {
|
||||
close();
|
||||
location.hash = `#/task/${taskId}`;
|
||||
};
|
||||
const render = () => {
|
||||
const q = input.value.trim().toLowerCase();
|
||||
matches = activeTasks().filter(({ project, task }) => `${project.name} ${task.title}`.toLowerCase().includes(q));
|
||||
selected = Math.min(selected, Math.max(matches.length - 1, 0));
|
||||
replace(list, ...matches.map(({ project, task }, i) => {
|
||||
const li = h("li", { class: i === selected ? "selected" : "" }, h("span", { class: "project-name" }, project.name), " ", task.title);
|
||||
li.onmousedown = (e) => {
|
||||
e.preventDefault();
|
||||
go(task.id);
|
||||
};
|
||||
return li;
|
||||
}), matches.length === 0 && h("li", { class: "empty-row" }, "No matching tasks."));
|
||||
};
|
||||
input.oninput = () => {
|
||||
selected = 0;
|
||||
render();
|
||||
};
|
||||
input.onkeydown = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
close();
|
||||
}
|
||||
else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
selected = Math.min(selected + 1, matches.length - 1);
|
||||
render();
|
||||
}
|
||||
else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
selected = Math.max(selected - 1, 0);
|
||||
render();
|
||||
}
|
||||
else if (e.key === "Enter") {
|
||||
const match = matches[selected];
|
||||
if (match) {
|
||||
go(match.task.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
backdrop.onclick = close;
|
||||
document.body.append(backdrop, box);
|
||||
render();
|
||||
input.focus();
|
||||
}
|
||||
//# sourceMappingURL=navbar.js.map
|
||||
Reference in New Issue
Block a user