// Timeline page: fold Milestone / Epic rows, remember what is folded, resize // the label column, and scroll to "now". Rendering and positions come from the // server (bar positions are % of the chart width). (() => { const KEY = "tracker-timeline-collapsed"; const WIDTH_KEY = "tracker-timeline-label-width"; const MIN_LABEL = 200; const load = () => { try { return new Set(JSON.parse(localStorage.getItem(KEY) || "[]")); } catch { return new Set(); } }; const save = (set) => { try { localStorage.setItem(KEY, JSON.stringify([...set])); } catch {} }; // Hide every row below a folded row (children, grandchildren, …). function apply() { const tl = document.getElementById("timeline"); if (!tl) return; let w = null; try { w = parseFloat(localStorage.getItem(WIDTH_KEY)); } catch {} if (w > 0) setLabelWidth(tl, w, false); const collapsed = load(); const rows = [...tl.querySelectorAll(".tl-row")]; const hiddenParents = new Set(); rows.forEach((row) => { const parent = row.dataset.parent; const hide = parent && (collapsed.has(parent) || hiddenParents.has(parent)); row.hidden = !!hide; if (hide || collapsed.has(row.dataset.id)) hiddenParents.add(row.dataset.id); const btn = row.querySelector("[data-tl-toggle]"); if (btn) btn.setAttribute("aria-expanded", String(!collapsed.has(row.dataset.id))); }); } // Width of the label column; never so wide that the chart disappears. function setLabelWidth(tl, px, remember) { const max = Math.max(MIN_LABEL, tl.clientWidth - 160); const w = Math.round(Math.min(max, Math.max(MIN_LABEL, px))); tl.style.setProperty("--tl-label", w + "px"); const sep = tl.querySelector("[data-tl-split]"); if (sep) { sep.setAttribute("aria-valuenow", String(w)); sep.setAttribute("aria-valuemin", String(MIN_LABEL)); sep.setAttribute("aria-valuemax", String(Math.round(max))); } if (remember) { try { localStorage.setItem(WIDTH_KEY, String(w)); } catch {} } } function labelWidth(tl) { const c = tl.querySelector(".tl-corner"); return c ? c.offsetWidth : 0; } // Drag the border between labels and chart (header handle or any row's border). document.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; const cell = e.target.closest("[data-tl-split], .tl-label, .tl-corner"); const tl = cell && cell.closest("#timeline"); if (!tl) return; const edge = cell.closest(".tl-label, .tl-corner").getBoundingClientRect().right; if (!e.target.hasAttribute("data-tl-split") && Math.abs(e.clientX - edge) > 5) return; e.preventDefault(); const startX = e.clientX, startW = labelWidth(tl); tl.classList.add("is-resizing"); const move = (ev) => setLabelWidth(tl, startW + ev.clientX - startX, false); const up = (ev) => { setLabelWidth(tl, startW + ev.clientX - startX, true); tl.classList.remove("is-resizing"); document.removeEventListener("pointermove", move); document.removeEventListener("pointerup", up); document.removeEventListener("pointercancel", up); }; document.addEventListener("pointermove", move); document.addEventListener("pointerup", up); document.addEventListener("pointercancel", up); }); // Keyboard: arrows move the border, Home goes back to the default width. document.addEventListener("keydown", (e) => { const sep = e.target.closest && e.target.closest("[data-tl-split]"); if (!sep) return; const tl = sep.closest("#timeline"); const step = e.shiftKey ? 80 : 20; if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault(); setLabelWidth(tl, labelWidth(tl) + (e.key === "ArrowRight" ? step : -step), true); } else if (e.key === "Home") { e.preventDefault(); tl.style.removeProperty("--tl-label"); try { localStorage.removeItem(WIDTH_KEY); } catch {} } }); function scrollToNow(smooth) { const tl = document.getElementById("timeline"); if (!tl) return; const nowPct = parseFloat(tl.dataset.now); const axis = tl.querySelector(".tl-axis"); if (!(nowPct >= 0) || !axis) return; const now = (nowPct / 100) * axis.offsetWidth; const labelW = labelWidth(tl); const left = Math.max(0, now - (tl.clientWidth - labelW) / 2); tl.scrollTo({ left, behavior: smooth ? "smooth" : "auto" }); } document.addEventListener("click", (e) => { const t = e.target.closest("[data-tl-toggle], [data-tl-today], [data-tl-expand], [data-tl-collapse]"); if (!t) return; if (t.dataset.tlToggle) { const set = load(); set.has(t.dataset.tlToggle) ? set.delete(t.dataset.tlToggle) : set.add(t.dataset.tlToggle); save(set); apply(); } else if (t.hasAttribute("data-tl-today")) { scrollToNow(true); } else if (t.hasAttribute("data-tl-expand")) { save(new Set()); apply(); } else if (t.hasAttribute("data-tl-collapse")) { const ids = [...document.querySelectorAll("#timeline .tl-epic")].map((r) => r.dataset.id); save(new Set(ids)); apply(); } }); document.addEventListener("tracker:updated", apply); apply(); scrollToNow(false); })();