100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
import { put, type TaskTab } from "./api.js";
|
|
import { mountChanges } from "./changes.js";
|
|
import { h, replace } from "./dom.js";
|
|
import { mountFiles } from "./files.js";
|
|
import { mountInbox } from "./inbox.js";
|
|
import { mountLead } from "./lead.js";
|
|
import { focusTab, on, state, tabs } from "./state.js";
|
|
import { mountProject } from "./project.js";
|
|
import { mountTranscript } from "./task.js";
|
|
|
|
const views: Record<TaskTab, { label: string; mount: (root: HTMLElement) => () => void }> = {
|
|
transcript: { label: "Transcript", mount: mountTranscript },
|
|
changes: { label: "Changes", mount: (root) => mountChanges(root) },
|
|
files: { label: "Files", mount: mountFiles },
|
|
inbox: { label: "Inbox", mount: mountInbox },
|
|
project: { label: "Project", mount: mountProject },
|
|
};
|
|
|
|
export function leadPane(): HTMLElement {
|
|
const root = h("div", { class: "view view-lead" });
|
|
mountLead(root);
|
|
return h("section", { class: "pane" }, h("nav", { class: "pane-tabs" }, h("button", { class: "active" }, "Lead")), h("div", { class: "pane-body" }, root));
|
|
}
|
|
|
|
export function taskPane(): HTMLElement {
|
|
const strip = h("nav", { class: "pane-tabs" });
|
|
const body = h("div", { class: "pane-body" });
|
|
let shown: TaskTab | undefined;
|
|
let dispose: () => void = () => {};
|
|
|
|
const render = () => {
|
|
const first = state.focus.tabs.filter((t, i, all) => tabs.includes(t) && all.indexOf(t) === i);
|
|
const order = [...first, ...tabs.filter((t) => !first.includes(t))];
|
|
const tab = state.focus.tab;
|
|
replace(
|
|
strip,
|
|
...order.map((t) => {
|
|
const b = h("button", { class: t === tab ? "active" : "" }, views[t].label);
|
|
b.onclick = () => focusTab(t);
|
|
return b;
|
|
}),
|
|
);
|
|
if (tab === shown) {
|
|
return;
|
|
}
|
|
dispose();
|
|
shown = tab;
|
|
const root = h("div", { class: `view view-${tab}` });
|
|
replace(body, root);
|
|
dispose = views[tab].mount(root);
|
|
};
|
|
|
|
on("focus", render);
|
|
render();
|
|
const pane = h("section", { class: "pane", tabIndex: -1 }, strip, body);
|
|
trackPaneFocus(pane);
|
|
return pane;
|
|
}
|
|
|
|
function trackPaneFocus(pane: HTMLElement): void {
|
|
let focused = false;
|
|
let inside: Element | null = null;
|
|
let sending: Promise<unknown> = Promise.resolve();
|
|
const send = () => {
|
|
const body = { focused };
|
|
sending = sending.then(() => put("/api/me/pane-focus", { body })).catch((err: unknown) => console.error("set pane focus", err));
|
|
};
|
|
const update = () => {
|
|
setTimeout(() => {
|
|
if (focused && inside && !inside.isConnected && document.activeElement === document.body) {
|
|
// A re-render removed the focused element (a tab button, a list row); that isn't the human leaving.
|
|
pane.focus({ preventScroll: true });
|
|
}
|
|
const active = document.activeElement;
|
|
const now = document.hasFocus() && pane.contains(active);
|
|
inside = now ? active : null;
|
|
if (now === focused) {
|
|
return;
|
|
}
|
|
focused = now;
|
|
send();
|
|
});
|
|
};
|
|
on("connection", () => {
|
|
if (state.connected && focused) {
|
|
send();
|
|
}
|
|
});
|
|
document.addEventListener("focusin", update);
|
|
document.addEventListener("focusout", update);
|
|
addEventListener("focus", update);
|
|
addEventListener("blur", update);
|
|
addEventListener("pagehide", () => {
|
|
if (focused) {
|
|
focused = false;
|
|
void put("/api/me/pane-focus", { body: { focused: false }, keepalive: true });
|
|
}
|
|
});
|
|
}
|