From d87bf59b4c16dfe5a1d9847bf3b581562193e68e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 13:40:54 +0000 Subject: [PATCH 1/2] Add Gantt week views (1W/3W/6W) and timeline navigation arrows - Replace Day/Week zoom presets with three week-based views: 1, 3, and 6 weeks - Extend the scrollable timeline 42 days into the past so users can navigate backward - Add left/right arrow controls that pan one week at a time without changing the view range - Default to the 3-week view; preserve scroll position across re-renders Co-authored-by: Tanops --- index.html | 17 ++++++++++++++++ src/app/main.js | 46 ++++++++++++++++++++++++++++++++++--------- src/data/constants.js | 6 +++--- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index dca73ca..5c09f3d 100644 --- a/index.html +++ b/index.html @@ -249,6 +249,16 @@ .gflegend span{display:inline-flex;align-items:center;gap:7px;font-size:12.5px;font-weight:700;color:var(--ink);white-space:nowrap} .gflegend i{width:12px;height:12px;border-radius:4px;flex:none} .gzoom button{padding:7px 13px;font-size:12.5px;min-height:34px;display:flex;align-items:center;justify-content:center} + /* timeline week navigation — fixed over the chart edges, does not scroll with the grid */ + .gnav{position:absolute;left:0;right:0;bottom:10px;height:44px;z-index:22;pointer-events:none} + .gnavbtn{position:absolute;top:0;transform:none;pointer-events:auto; + width:30px;height:44px;border-radius:10px;background:rgba(255,255,255,.94); + border:1px solid var(--line);box-shadow:0 2px 10px rgba(20,24,35,.1); + font-size:22px;font-weight:400;line-height:1;color:var(--ink-2); + display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s} + .gnavbtn:hover{background:var(--surface);color:var(--ink)} + .gnavbtn:active{transform:scale(.96)} + .gnav-l{left:6px}.gnav-r{right:6px} /* owners shown as a compact segmented row of avatars */ .gpeople{padding:3px;gap:2px} .gpeople button{padding:2px;min-height:0;width:38px;height:34px;border-radius:8px;display:flex; @@ -605,6 +615,9 @@ .gpeople button{width:30px;height:28px} .gpeople .av{width:23px;height:23px;font-size:9px} .gzoom button{padding:6px 9px;font-size:11.5px;min-height:30px} + .gnav{bottom:6px;height:38px} + .gnavbtn{width:26px;height:38px;font-size:20px} + .gnav-l{left:4px}.gnav-r{right:4px} .gficon{width:31px;height:31px} .gficon svg{width:17px;height:17px} /* capture + team sheets use the full width on a phone */ @@ -739,6 +752,10 @@ +
+ + +
diff --git a/src/app/main.js b/src/app/main.js index 3efad79..f89dff7 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -1,7 +1,7 @@ import { PEOPLE, TODAY, HARDWARE_VOCAB, CLIENTS, SIZE_PTS, SIZE_NAMES, LEAD, ZOOMS, GBAR_H, - R0G, R1G, SPAN_G, TODAY_PX, + R0G, R1G, SPAN_G, GANTT_PAST, TODAY_PX, C_LATE, C_TODAY, C_RADAR, C_LATER, C_DONE, } from "../data/constants.js"; import { inferOwnerByDomain, canonHardware, findClient, buildRespMapText, buildVocabText, norm as _norm } from "../lib/domain.js"; @@ -231,7 +231,7 @@ function sizeScale(){ bars. Bar height tracks t-shirt size; each bar has a done-dot (left) and owner bubble (right). The window scrolls horizontally; zoom buttons set how many days fit on screen. */ /* chart starts at today - no dead space on the left */ -let ZOOM = 2, showDone = false; +let ZOOM = 1, showDone = false; // default: 3-week view const { dayN, dayIso, barSpan, workDays, barColor, barGeom, rollupSpan, spanFor, leafWeight, progWD, isUrgent, fmtD, @@ -290,7 +290,31 @@ function toggleFocus(){ focusToday=!focusToday; const b=$id("gfocusbtn"); if(b)b let GVIEW="proj"; // "proj" | "tasks" | "subs" function setGView(v){ GVIEW=v; defer(renderGantt); } function setZoom(i){ ZOOM=i; setTimeout(renderAll,0); } // drives the scale window and the gantt zoom +/* scroll the timeline by one week without changing the visible day count (1W / 3W / 6W) */ +function navTimeline(dir){ + const sc=document.querySelector(".gscroll"), inner=sc?.querySelector(".ginner"); + if(!sc||!inner) return; + const anchor=scrollAnchorDay(sc,inner); + const target=Math.max(R0G,Math.min(R1G-ZOOMS[ZOOM].v,anchor+dir*7)); + sc.scrollLeft=dayScrollPx(target,inner); + pinFlags(); placeFloat(); +} +/* pixel offset of a day index on the inner track (inverse of gx) */ +function dayScrollPx(d,inner){ + return gx(d)/100*inner.clientWidth; +} +/* day index nearest the left edge of the scrolled viewport */ +function scrollAnchorDay(sc,inner){ + const frac=sc.scrollLeft/Math.max(inner.clientWidth,1)*100; + let best=R0G, dist=Infinity; + for(let d=R0G;d<=R1G;d++){ + const g=gx(d); if(Math.abs(g-frac)=20; const months=[]; let lastM=-1; - for(let d=0;d<=R1G;d++){ const dt=new Date(dayIso(d)), m=dt.getFullYear()*12+dt.getMonth(); + for(let d=R0G;d<=R1G;d++){ const dt=new Date(dayIso(d)), m=dt.getFullYear()*12+dt.getMonth(); if(m!==lastM){ lastM=m; months.push({d,label:dt.toLocaleDateString("en-GB",{month:"short",year:"numeric"})}); } } const dticks=[]; - for(let d=0;d<=R1G;d++){ const dt=new Date(dayIso(d)), wknd=dt.getDay()%6===0; + for(let d=R0G;d<=R1G;d++){ const dt=new Date(dayIso(d)), wknd=dt.getDay()%6===0; if(showDaily||dt.getDay()===1||d===0) dticks.push({d,wd:"SMTWTFS"[dt.getDay()],num:dt.getDate(),wknd,today:d===0}); } const zoomEl=document.getElementById("gzoom"); if(zoomEl){ // build once; afterwards only toggle classes (keeps the clicked button alive) if(!zoomEl.childElementCount) - zoomEl.innerHTML=ZOOMS.map((z,i)=>``).join(""); + zoomEl.innerHTML=ZOOMS.map((z,i)=>``).join(""); [...zoomEl.children].forEach((b,i)=>b.classList.toggle("active",ZOOM===i)); } // keep the three toggle pictograms lit in line with their state @@ -324,13 +348,13 @@ function renderGantt(){ [...gv.children].forEach((b,i)=>b.classList.toggle("active",GVIEW===GVKEYS[i])); } // calendar grid: faint day lines, firmer Monday lines, alternate weeks washed - const wk=[0]; - for(let d=1;d<=R1G;d++) if(new Date(dayIso(d)).getDay()===1) wk.push(d); + const wk=[R0G]; + for(let d=R0G+1;d<=R1G;d++) if(new Date(dayIso(d)).getDay()===1) wk.push(d); wk.push(R1G); const deco=[]; for(let i=0;i`); - for(let d=1;d`); } months.forEach(m=>{ if(m.d>0) deco.push(`
`); }); const td=new Date(dayIso(0)); @@ -457,7 +481,10 @@ function renderGantt(){ (any?"":'
No scheduled tasks for this filter.
')+ ``; const sc=document.querySelector(".gscroll"); - sc.addEventListener("scroll",pinFlags,{passive:true}); + const inner=sc.querySelector(".ginner"); + if(savedScroll!==null) sc.scrollLeft=savedScroll; + else sc.scrollLeft=dayScrollPx(0,inner); + if(!sc._pinBound){ sc._pinBound=true; sc.addEventListener("scroll",pinFlags,{passive:true}); } const gpane=document.querySelector(".gantt"); if(gpane&&!gpane._floatBound){ gpane._floatBound=true; gpane.addEventListener("scroll",placeFloat,{passive:true}); } pinFlags(); placeFloat(); placeOverflowTitles(); @@ -1527,6 +1554,7 @@ const _globals = { toggleFlyout, toggleFocus, toggleShowDone, toggleSubs, closeCapture, toggleCapLang, minimizeCapture, sendTurn, restoreCapture, skipKey, saveKey, clearKey, closeTranscript, runTranscript, closeReview, closeTeam, closeSheet, setFilter, setScaleView, ding, toggleDone, openDetail, setZoom, setGView, + navTimeline, scrollToToday, toggleExp, updTask, refreshBarMenu, addChild, addProject, deleteTask, addCapTask, barDown, barContext, pickSearch, uploadPhoto, removePhoto, rvToggle, rvText, rvOwner, rvDue, rvSize, pushApproved, attachTranscript, doSearch, refreshCard, delCapTask, setTask, setTaskOwner, setTaskSize, setSub, setSubOwner, addSub, diff --git a/src/data/constants.js b/src/data/constants.js index 1e6cd9d..31d5545 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -39,14 +39,14 @@ export const SIZE_NAMES = { s: "S", m: "M", l: "L", xl: "XL" }; export const LEAD = { s: 1, m: 3, l: 7, xl: 14 }; export const ZOOMS = [ - { l: "Day", h: 0, v: 3 }, - { l: "Week", h: 7, v: 7 }, + { l: "1 week", h: 7, v: 7 }, { l: "3 weeks", h: 21, v: 21 }, { l: "6 weeks", h: 42, v: 42 }, ]; export const GBAR_H = { s: 26, m: 34, l: 44, xl: 56 }; -export const R0G = 0; +export const GANTT_PAST = 42; // days before today included in the scrollable timeline +export const R0G = -GANTT_PAST; export const R1G = 90; export const SPAN_G = R1G - R0G; export const TODAY_PX = 240; From 4fc3329283d041de923bc5bd44f48f068651668a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 13:41:19 +0000 Subject: [PATCH 2/2] Remove unused scrollToToday global export Co-authored-by: Tanops --- src/app/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/main.js b/src/app/main.js index f89dff7..5446fc0 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -1554,7 +1554,7 @@ const _globals = { toggleFlyout, toggleFocus, toggleShowDone, toggleSubs, closeCapture, toggleCapLang, minimizeCapture, sendTurn, restoreCapture, skipKey, saveKey, clearKey, closeTranscript, runTranscript, closeReview, closeTeam, closeSheet, setFilter, setScaleView, ding, toggleDone, openDetail, setZoom, setGView, - navTimeline, scrollToToday, + navTimeline, toggleExp, updTask, refreshBarMenu, addChild, addProject, deleteTask, addCapTask, barDown, barContext, pickSearch, uploadPhoto, removePhoto, rvToggle, rvText, rvOwner, rvDue, rvSize, pushApproved, attachTranscript, doSearch, refreshCard, delCapTask, setTask, setTaskOwner, setTaskSize, setSub, setSubOwner, addSub,