import { put } 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 { openSwitcher } from "./navbar.js"; import { findTask, focusedProject, focusTab, on, onEach, state, tabs } from "./state.js"; import { mountPreview } from "./preview.js"; import { mountProject } from "./project.js"; import { isReviewEvent, latestFindingCount, mountReviews } from "./reviews.js"; import { mountTranscript } from "./task.js"; import { mountTerminal } from "./terminal.js"; const views = { transcript: { label: "Transcript", mount: mountTranscript }, changes: { label: "Changes", mount: (root) => mountChanges(root) }, files: { label: "Files", mount: mountFiles }, terminal: { label: "Terminal", mount: mountTerminal }, preview: { label: "Preview", mount: mountPreview }, inbox: { label: "Inbox", mount: mountInbox }, reviews: { label: "Reviews", mount: mountReviews }, project: { label: "Project", mount: mountProject }, }; export function leadPane() { 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() { const strip = h("nav", { class: "pane-tabs" }); const body = h("div", { class: "pane-body" }); const picker = h("button", { class: "task-picker", title: "Switch task (⌘K)" }); picker.onclick = openSwitcher; const pane = h("section", { class: "pane task-pane", tabIndex: -1 }, strip, body); let shown; let dispose = () => { }; const renderPicker = () => { const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined; const project = focusedProject(); if (task) { pane.style.setProperty("--task", `var(--task-${task.color})`); } else { pane.style.removeProperty("--task"); } replace(picker, task ? `${project?.name ?? ""} ▸ ${task.title}` : (project?.name ?? "No task"), " ▾"); }; const more = h("button", { type: "button", class: "more", title: "More tabs" }); const menu = h("div", { class: "tab-menu", popover: "auto" }); more.onclick = () => { const rect = more.getBoundingClientRect(); menu.style.top = `${rect.bottom + 4}px`; menu.style.left = `${rect.left}px`; menu.togglePopover(); }; let buttons = []; let drawn = ""; let waiting = 0; let findings = 0; const count = (t) => (t === "inbox" ? waiting : t === "reviews" ? findings : 0); const badge = (t) => count(t) > 0 && h("span", { class: "badge", title: t === "inbox" ? "inbox items" : "findings in the latest review" }, count(t)); const layout = () => { for (const { button } of buttons) { button.hidden = false; } more.hidden = true; const style = getComputedStyle(strip); const gap = parseFloat(style.columnGap) || 0; const inner = strip.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); const pickerWidth = Math.min(picker.scrollWidth, inner * 0.4); const widths = new Map(buttons.map(({ tab, button }) => [tab, button.offsetWidth])); const total = (kept) => kept.reduce((sum, t) => sum + widths.get(t) + gap, 0); const all = buttons.map((b) => b.tab); let kept = all; if (total(all) + gap + pickerWidth > inner) { more.hidden = false; const budget = inner - pickerWidth - more.offsetWidth - 2 * gap; const tab = state.focus.tab; kept = []; for (const t of [tab, ...all.filter((t) => t !== tab)]) { if (kept.length > 0 && total([...kept, t]) > budget) { break; } kept.push(t); } } const hidden = all.filter((t) => !kept.includes(t)); for (const { tab, button } of buttons) { button.hidden = hidden.includes(tab); } const flagged = hidden.filter((t) => count(t) > 0); more.classList.toggle("attention", flagged.length > 0); replace(more, "More ▾", ...flagged.map(badge)); replace(menu, ...hidden.map((t) => { const b = h("button", { type: "button" }, views[t].label, badge(t)); b.onclick = () => { menu.hidePopover(); focusTab(t); }; return b; })); }; const available = (t) => { if (t === state.focus.tab) { return true; } switch (t) { case "inbox": return waiting > 0; case "preview": return state.transcript.some((ev) => ev.kind === "preview.exposed"); case "reviews": return state.transcript.some(isReviewEvent); default: return true; } }; const render = () => { const first = state.focus.tabs.filter((t, i, all) => tabs.includes(t) && all.indexOf(t) === i); const tab = state.focus.tab; waiting = state.inbox.filter((item) => item.task_id === state.selectedTaskId).length; findings = latestFindingCount(state.transcript); const order = [...first, ...tabs.filter((t) => !first.includes(t))].filter(available); const key = JSON.stringify([order, tab, waiting, findings]); if (key !== drawn) { drawn = key; buttons = order.map((t) => { const button = h("button", { class: [t === tab ? "active" : "", count(t) > 0 ? "attention" : ""].join(" ").trim() }, views[t].label, badge(t)); button.onclick = () => focusTab(t); return { tab: t, button }; }); replace(strip, ...buttons.map((b) => b.button), more, h("span", { class: "spacer" }), picker, menu); layout(); } if (tab === shown) { return; } dispose(); shown = tab; const root = h("div", { class: `view view-${tab}` }); replace(body, root); dispose = views[tab].mount(root); }; const seen = new Set(state.inbox.map((item) => item.id)); const openAsked = () => { const fresh = state.inbox.filter((item) => !seen.has(item.id)); for (const item of fresh) { seen.add(item.id); } if (state.focus.tab === "transcript" && fresh.some((item) => item.kind === "question" && item.task_id === state.selectedTaskId)) { focusTab("inbox"); } }; on("inbox", openAsked); onEach(["focus", "inbox", "selection", "transcript"], render); onEach(["projects", "tasks", "selection", "focus"], () => { renderPicker(); layout(); }); new ResizeObserver(layout).observe(strip); render(); renderPicker(); layout(); trackPaneFocus(pane); return pane; } function trackPaneFocus(pane) { let focused = false; let inside = null; let sending = Promise.resolve(); const send = () => { const body = { focused }; sending = sending.then(() => put("/api/me/pane-focus", { body })).catch((err) => 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 }); } }); } //# sourceMappingURL=panes.js.map