From cc1719ff74ae078fe7fada6b7b22a68a782f18ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 16:17:07 +0000 Subject: [PATCH 1/5] Pin task bar avatar and title when bar extends off-screen Wrap owner avatar and title in a gpin cluster that slides within each bar on horizontal scroll, keeping both visible in the viewport at any zoom level. Updates on scroll, resize, render, and bar drag. Co-authored-by: Tanops --- index.html | 35 ++++++++++++++++++++--------------- src/app/main.js | 29 +++++++++++++++++++++++------ tests/smoke.test.js | 6 ++++++ 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/index.html b/index.html index dca73ca..4eeaa68 100644 --- a/index.html +++ b/index.html @@ -294,11 +294,19 @@ .gbar.gdropover{box-shadow:0 0 0 3px var(--accent)} .pgroup.gdropover{background:var(--accent-soft);border-radius:12px} .gbar{position:absolute;top:50%;transform:translateY(-50%);height:20px;border-radius:999px;min-width:16px; - display:flex;align-items:center;gap:6px;padding:0 12px 0 4px;overflow:hidden; box-shadow:0 2px 7px rgba(20,24,35,.16); - cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none} + cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none; + overflow:hidden} .gbar:hover{box-shadow:0 3px 11px rgba(20,24,35,.24)} .gbar:active{cursor:grabbing} + /* avatar + title cluster — slides within the bar to stay in the scrolled viewport */ + .gpin{position:absolute;left:0;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:6px; + z-index:2;pointer-events:none;padding-left:4px;max-width:calc(100% - 24px)} + .gpin .ttl{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden; + text-overflow:ellipsis;flex:1;min-width:0;text-align:left;padding-left:1px} + .gbar.gsz-s .gpin .ttl{font-size:10.5px}.gbar.gsz-l .gpin .ttl{font-size:13px} + .gbar.gsz-xl .gpin .ttl{font-size:14px} + .gbar.gsub .gpin .ttl{color:inherit;font-size:11px} /* owner avatar fills the round left cap with an even ~4px margin on every side, so the space around it reads the same top, bottom and left regardless of bar height */ .gava{flex:none;position:relative;z-index:2;line-height:0} @@ -308,20 +316,18 @@ .gbar.gsz-l .gava .av{width:36px;height:36px;font-size:14px} .gbar.gsz-xl .gava .av{width:48px;height:48px;font-size:18px} .gbar.gsz-s{height:26px}.gbar.gsz-m{height:34px}.gbar.gsz-l{height:44px}.gbar.gsz-xl{height:56px} - .gbar .ttl{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden; - text-overflow:ellipsis;pointer-events:none;flex:1;min-width:0;text-align:left;padding-left:1px} - .gbar.gsz-s .ttl{font-size:10.5px}.gbar.gsz-l .ttl{font-size:13px}.gbar.gsz-xl .ttl{font-size:14px} - /* title rendered to the right of a too-narrow bar (plain black text, no background) */ - .gttlout{position:absolute;top:50%;transform:translateY(-50%);font-size:12px;font-weight:700; - color:var(--ink);white-space:nowrap;pointer-events:none;z-index:3} - .gdot{flex:none;position:relative;z-index:2;width:16px;height:16px;border-radius:50%;padding:0; + .gdot{position:absolute;right:4px;top:50%;transform:translateY(-50%); + flex:none;z-index:2;width:16px;height:16px;border-radius:50%;padding:0; border:2px solid rgba(255,255,255,.9);color:#fff;font-size:9px; display:flex;align-items:center;justify-content:center; - opacity:0;width:0;margin:0;border-width:0;transition:opacity .12s} - .gbar:hover .gdot{opacity:1;width:16px;border-width:2px;margin-right:0} - .gbar.gsz-s:hover .gdot{width:13px} - .gdot.on{opacity:1;width:16px;border-width:2px;background:#fff;border-color:#fff;color:var(--green)} + opacity:0;border-width:0;transition:opacity .12s} + .gbar:hover .gdot{opacity:1;border-width:2px} + .gbar.gsz-s:hover .gdot{width:13px;height:13px} + .gdot.on{opacity:1;border-width:2px;background:#fff;border-color:#fff;color:var(--green)} .gbar.gsz-s .gdot{width:13px;height:13px} + /* title rendered to the right of a too-narrow bar (plain black text, no background) */ + .gttlout{position:absolute;top:50%;transform:translateY(-50%);font-size:12px;font-weight:700; + color:var(--ink);white-space:nowrap;pointer-events:none;z-index:3} .gavb{flex:none;position:relative;z-index:2;margin-left:auto;width:18px;height:18px;border-radius:50%; background:#fff;font-size:8.5px;font-weight:800;pointer-events:none; display:flex;align-items:center;justify-content:center} @@ -333,7 +339,6 @@ .gexp:hover{color:var(--accent)} /* subtasks are OUTLINED (light fill, coloured border + text) vs solid filled tasks */ .gbar.gsub{box-shadow:0 1px 4px rgba(20,24,35,.12)} - .gbar.gsub .ttl{color:inherit;font-size:11px} .gbar.gsub .gava .av{width:15px;height:15px;font-size:7.5px;box-shadow:0 0 0 2px #fff} /* subtask bars have a WHITE fill, so the white done-dot was invisible on them — give the dot the bar's own colour on hover, and a solid green tick once checked */ @@ -341,7 +346,7 @@ .gbar.gsub .gdot.on{background:var(--green);border-color:var(--green);color:#fff} .pflat{padding:12px 0 6px} .gbar.gdone{opacity:.4} - .gbar.gdone .ttl{text-decoration:line-through} + .gbar.gdone .gpin .ttl{text-decoration:line-through} .ear{position:absolute;top:0;bottom:0;width:13px;cursor:ew-resize;touch-action:none; opacity:0;transition:opacity .15s} .gbar:hover .ear,.gbar.dragging .ear{opacity:1} diff --git a/src/app/main.js b/src/app/main.js index 3efad79..c1c19f6 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -372,8 +372,7 @@ function renderGantt(){ ?`background:#fff;border:2px solid ${col};color:${col}` :fillBg}"> - ${av(n.owner,"xs")} - ${n.title} +
${av(n.owner,"xs")}${n.title}
@@ -457,10 +456,10 @@ function renderGantt(){ (any?"":'
No scheduled tasks for this filter.
')+ ``; const sc=document.querySelector(".gscroll"); - sc.addEventListener("scroll",pinFlags,{passive:true}); + if(!sc._pinBound){ sc._pinBound=true; sc.addEventListener("scroll",pinGanttScroll,{passive:true}); } const gpane=document.querySelector(".gantt"); if(gpane&&!gpane._floatBound){ gpane._floatBound=true; gpane.addEventListener("scroll",placeFloat,{passive:true}); } - pinFlags(); placeFloat(); placeOverflowTitles(); + pinGanttScroll(); placeFloat(); placeOverflowTitles(); // unstick Chromium's hover hit-testing after the DOM swap (otherwise tooltips/hover // stay dead until you move the mouse or switch tabs) if(typeof requestAnimationFrame!=="undefined") @@ -487,7 +486,7 @@ function placeOverflowTitles(){ document.querySelectorAll("#gantt .gttlout").forEach(x=>x.remove()); document.querySelectorAll("#gantt .gtrack").forEach(track=>{ track.querySelectorAll(":scope > .gbar").forEach(bar=>{ - const ttl=bar.querySelector(".ttl"); if(!ttl) return; + const ttl=bar.querySelector(".gpin .ttl")||bar.querySelector(".ttl"); if(!ttl) return; const full=ttl.scrollWidth, vis=ttl.clientWidth; if(full>4 && vis{ + const pin=bar.querySelector(".gpin"); if(!pin) return; + pin.style.left="0px"; + const bl=bar.offsetLeft, br=bl+bar.offsetWidth; + if(br<=v0+pad||bl>=v1-pad) return; + const pinW=pin.offsetWidth; if(pinW<2) return; + const want=Math.max(bl,v0+pad); + const maxL=Math.max(bl,br-pinW-pad); + const abs=Math.min(want,maxL); + if(abs>bl+1) pin.style.left=(abs-bl)+"px"; + }); +} function pinFlags(){ const sc=document.querySelector(".gscroll"); if(!sc) return; const v0=sc.scrollLeft; @@ -515,7 +531,7 @@ function pinFlags(){ lbl.style.left=left+"px"; }); } -window.addEventListener("resize",()=>{ sizeScale(); applyTilt(lastTilt); defer(renderGantt); }); +window.addEventListener("resize",()=>{ sizeScale(); applyTilt(lastTilt); defer(renderGantt); pinBars(); }); /* --- floating "gripped pill" ghost, shared by bar drags and pop-up task drags --- */ function makeGhost(text,color){ const g=document.createElement("div"); @@ -601,6 +617,7 @@ function barMove(ev){ const [cs,ce]=barGeom(G.s,G.e,G.n.done); G.el.style.left=gx(cs)+"%"; G.el.style.width=(gx(ce)-gx(cs))+"%"; + pinBars(); if(G.ghost){ G.ghost.style.display=G.moved?"":"none"; G.ghost.textContent=G.n.title+" · due "+fmtD(dayIso(G.e)); placeGhost(G.ghost,ev); } diff --git a/tests/smoke.test.js b/tests/smoke.test.js index a3f9aa0..e1a20e0 100644 --- a/tests/smoke.test.js +++ b/tests/smoke.test.js @@ -9,6 +9,12 @@ describe("project layout", () => { expect(html).toContain('onclick="addProject()"'); }); + it("pins task bar avatar and title during horizontal scroll", () => { + const main = readFileSync("src/app/main.js", "utf8"); + expect(main).toContain("function pinBars()"); + expect(main).toContain('class="gpin"'); + }); + it("exports core lib modules", async () => { const domain = await import("../src/lib/domain.js"); const tree = await import("../src/lib/tree.js"); From 28054877c246fa5580673baa8f611c58e32869c0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 16:25:45 +0000 Subject: [PATCH 2/5] Add project name speech bubbles matching task bar pattern Show each project name in a rounded bubble with the owner avatar and title cluster (gpin), pin the bubble while scrolling wide project spans, and spill truncated names into overflow labels like task bars. Co-authored-by: Tanops --- index.html | 12 +++++++++--- src/app/main.js | 47 +++++++++++++++++++++++++++++---------------- tests/smoke.test.js | 6 ++++++ 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/index.html b/index.html index 4eeaa68..a049389 100644 --- a/index.html +++ b/index.html @@ -280,10 +280,16 @@ (auto-computed from task completion weighted by size) */ .gsumline{position:absolute;top:18px;height:9px;border-radius:999px;background:#ccd4e2;pointer-events:none;z-index:0} .gsumfill{position:absolute;top:18px;height:9px;border-radius:999px;background:#3b6ef6;pointer-events:none;z-index:1;min-width:0} - .gsumlbl{position:absolute;top:0;z-index:2;font-size:12.5px;font-weight:700;color:var(--ink-2); - background:none;padding:0 2px 0 1px;white-space:nowrap;cursor:grab; + .gsumlbl{position:absolute;top:0;z-index:2;display:inline-flex;align-items:center; + background:var(--surface);border:1px solid var(--line);border-radius:999px; + padding:3px 10px 3px 3px;box-shadow:0 2px 8px rgba(20,24,35,.1); + font:inherit;white-space:nowrap;cursor:grab; touch-action:none;-webkit-user-select:none;user-select:none} - .gsumlbl:hover{color:var(--ink)} + .gsumlbl:hover{box-shadow:0 3px 11px rgba(20,24,35,.16)} + .gsumlbl .gpin{position:relative;left:0;top:auto;transform:none;padding-left:0; + max-width:min(240px,42vw);pointer-events:none;gap:7px} + .gsumlbl .gpin .ttl{color:var(--ink);font-size:12.5px;font-weight:700} + .gsumlbl .gava .av{width:22px;height:22px;font-size:9px;box-shadow:0 0 0 2px var(--surface)} .gsumpct{position:absolute;top:16px;transform:translateX(7px);font-size:11px;font-weight:700; color:var(--ink-3);white-space:nowrap;pointer-events:none} .pgroup.gdropover .gsumline{background:var(--accent)} diff --git a/src/app/main.js b/src/app/main.js index c1c19f6..4cecb66 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -415,7 +415,9 @@ function renderGantt(){ rows.push(`
+ data-full="${p.title} — ${ppc}% done · ${pPts} pts · ${open} open · due ${p.due?fmtD(p.due):"no date"} — click to manage, drag to reorder"> +
${av(p.owner,"xs")}${p.title}
+
${ppc}% @@ -484,23 +486,35 @@ function kickHover(){ full title as plain text just to the right of the bar (no background) */ function placeOverflowTitles(){ document.querySelectorAll("#gantt .gttlout").forEach(x=>x.remove()); + const overflow=(track,bar,ttl,rightPct,off)=>{ + const full=ttl.scrollWidth, vis=ttl.clientWidth; + if(full>4 && vis{ track.querySelectorAll(":scope > .gbar").forEach(bar=>{ const ttl=bar.querySelector(".gpin .ttl")||bar.querySelector(".ttl"); if(!ttl) return; - const full=ttl.scrollWidth, vis=ttl.clientWidth; - if(full>4 && vis .gsumlbl"); + if(lbl){ + const ttl=lbl.querySelector(".gpin .ttl")||lbl.querySelector(".ttl"); if(!ttl) return; + const line=track.querySelector(".gsumline"); + const right=line + ?((line.offsetLeft+line.offsetWidth)/Math.max(track.clientWidth,1)*100) + :(parseFloat(lbl.style.left||0)+lbl.offsetWidth/Math.max(track.clientWidth,1)*100); + overflow(track,lbl,ttl,right,8); + } }); } -function pinGanttScroll(){ pinFlags(); pinBars(); } +function pinGanttScroll(){ pinFlags(); pinBars(); placeOverflowTitles(); } /* keep avatar + title visible when a bar extends beyond the horizontal viewport */ function pinBars(){ const sc=document.querySelector(".gscroll"); if(!sc) return; @@ -519,15 +533,14 @@ function pinBars(){ } function pinFlags(){ const sc=document.querySelector(".gscroll"); if(!sc) return; - const v0=sc.scrollLeft; + const v0=sc.scrollLeft, pad=6; sc.querySelectorAll(".gsumrow").forEach(row=>{ const lbl=row.querySelector(".gsumlbl"), line=row.querySelector(".gsumline"); if(!lbl||!line) return; - // keep the project name visible: slide the label to the viewport's left edge as its - // range line scrolls past, but never beyond the line's right end const ll=line.offsetLeft, lr=ll+line.offsetWidth; - let left=Math.max(ll,v0+4); - left=Math.min(left,Math.max(lr-46,ll)); + const pinW=lbl.offsetWidth||46; + let left=Math.max(ll,v0+pad); + left=Math.min(left,Math.max(lr-pinW-pad,ll)); lbl.style.left=left+"px"; }); } diff --git a/tests/smoke.test.js b/tests/smoke.test.js index e1a20e0..1d408cd 100644 --- a/tests/smoke.test.js +++ b/tests/smoke.test.js @@ -15,6 +15,12 @@ describe("project layout", () => { expect(main).toContain('class="gpin"'); }); + it("renders project name speech bubbles with owner avatar", () => { + const main = readFileSync("src/app/main.js", "utf8"); + expect(main).toContain('class="gsumlbl"'); + expect(main).toMatch(/gsumlbl[\s\S]*class="gpin"/); + }); + it("exports core lib modules", async () => { const domain = await import("../src/lib/domain.js"); const tree = await import("../src/lib/tree.js"); From b4f66c33852e83ae1f5816ccd293fbcdd083ac13 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 16:43:20 +0000 Subject: [PATCH 3/5] Integrate project title bubble on progress bar track Wrap summary line, fill, label, and percent in a gsumtrack container. Center the title bubble on the bar's left cap so the gray track and blue fill remain visible across the full project span. Co-authored-by: Tanops --- index.html | 29 ++++++++++++++++------------- src/app/main.js | 38 ++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/index.html b/index.html index a049389..d0eeff9 100644 --- a/index.html +++ b/index.html @@ -275,23 +275,26 @@ /* project header: name on its own line, the progress bar on the line below it */ /* top-align so the row's extra height (the gap before the first task) stays BELOW the bar, instead of being centered and pushing the project name down away from the ribbon */ - .gsumrow{min-height:30px;align-items:flex-start} - /* project bar: a base track = the whole project duration, with a progress fill over it - (auto-computed from task completion weighted by size) */ - .gsumline{position:absolute;top:18px;height:9px;border-radius:999px;background:#ccd4e2;pointer-events:none;z-index:0} - .gsumfill{position:absolute;top:18px;height:9px;border-radius:999px;background:#3b6ef6;pointer-events:none;z-index:1;min-width:0} - .gsumlbl{position:absolute;top:0;z-index:2;display:inline-flex;align-items:center; + .gsumrow{min-height:30px;align-items:flex-start;padding-top:4px} + /* project track: progress bar spans the full width; title bubble sits on the left cap */ + .gsumtrack{position:absolute;top:8px;min-height:28px} + .gsumline{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%); + height:9px;border-radius:999px;background:#ccd4e2;pointer-events:none;z-index:0} + .gsumfill{position:absolute;left:0;top:50%;transform:translateY(-50%); + height:9px;border-radius:999px;background:#3b6ef6;pointer-events:none;z-index:1;min-width:0} + .gsumlbl{position:absolute;left:0;top:50%;transform:translateY(-50%);z-index:3; + display:inline-flex;align-items:center; background:var(--surface);border:1px solid var(--line);border-radius:999px; - padding:3px 10px 3px 3px;box-shadow:0 2px 8px rgba(20,24,35,.1); + padding:2px 9px 2px 2px;box-shadow:0 2px 8px rgba(20,24,35,.12); font:inherit;white-space:nowrap;cursor:grab; touch-action:none;-webkit-user-select:none;user-select:none} - .gsumlbl:hover{box-shadow:0 3px 11px rgba(20,24,35,.16)} + .gsumlbl:hover{box-shadow:0 3px 11px rgba(20,24,35,.18)} .gsumlbl .gpin{position:relative;left:0;top:auto;transform:none;padding-left:0; - max-width:min(240px,42vw);pointer-events:none;gap:7px} - .gsumlbl .gpin .ttl{color:var(--ink);font-size:12.5px;font-weight:700} - .gsumlbl .gava .av{width:22px;height:22px;font-size:9px;box-shadow:0 0 0 2px var(--surface)} - .gsumpct{position:absolute;top:16px;transform:translateX(7px);font-size:11px;font-weight:700; - color:var(--ink-3);white-space:nowrap;pointer-events:none} + max-width:min(220px,38vw);pointer-events:none;gap:6px} + .gsumlbl .gpin .ttl{color:var(--ink);font-size:12px;font-weight:700} + .gsumlbl .gava .av{width:20px;height:20px;font-size:8.5px;box-shadow:0 0 0 2px var(--surface)} + .gsumpct{position:absolute;right:0;top:50%;transform:translate(100%,-50%);margin-left:7px; + font-size:11px;font-weight:700;color:var(--ink-3);white-space:nowrap;pointer-events:none;z-index:2} .pgroup.gdropover .gsumline{background:var(--accent)} .pgroup.insb{box-shadow:0 -3px 0 0 var(--accent)} .pgroup.insa{box-shadow:0 3px 0 0 var(--accent)} diff --git a/src/app/main.js b/src/app/main.js index 4cecb66..20e4e31 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -413,14 +413,16 @@ function renderGantt(){ let pPts=0; flat([p],x=>{ if(x.children.length) return; pPts+=SIZE_PTS[x.size||"m"]; }); const ph=Math.max(7,Math.min(20,Math.round(5+Math.sqrt(pPts)*2.2))); rows.push(`
-
- -
-
- ${ppc}% +
+
+
+
+ + ${ppc}% +
${taskRows}
`); }); @@ -506,9 +508,9 @@ function placeOverflowTitles(){ const lbl=track.querySelector(":scope > .gsumlbl"); if(lbl){ const ttl=lbl.querySelector(".gpin .ttl")||lbl.querySelector(".ttl"); if(!ttl) return; - const line=track.querySelector(".gsumline"); - const right=line - ?((line.offsetLeft+line.offsetWidth)/Math.max(track.clientWidth,1)*100) + const sum=track.querySelector(":scope > .gsumtrack"); + const right=sum + ?((sum.offsetLeft+sum.offsetWidth)/Math.max(track.clientWidth,1)*100) :(parseFloat(lbl.style.left||0)+lbl.offsetWidth/Math.max(track.clientWidth,1)*100); overflow(track,lbl,ttl,right,8); } @@ -534,14 +536,14 @@ function pinBars(){ function pinFlags(){ const sc=document.querySelector(".gscroll"); if(!sc) return; const v0=sc.scrollLeft, pad=6; - sc.querySelectorAll(".gsumrow").forEach(row=>{ - const lbl=row.querySelector(".gsumlbl"), line=row.querySelector(".gsumline"); - if(!lbl||!line) return; - const ll=line.offsetLeft, lr=ll+line.offsetWidth; + sc.querySelectorAll(".gsumtrack").forEach(track=>{ + const lbl=track.querySelector(".gsumlbl"); + if(!lbl) return; + const ll=track.offsetLeft, lr=ll+track.offsetWidth; const pinW=lbl.offsetWidth||46; - let left=Math.max(ll,v0+pad); - left=Math.min(left,Math.max(lr-pinW-pad,ll)); - lbl.style.left=left+"px"; + let abs=Math.max(ll,v0+pad); + abs=Math.min(abs,Math.max(lr-pinW-pad,ll)); + lbl.style.left=(abs-ll)+"px"; }); } window.addEventListener("resize",()=>{ sizeScale(); applyTilt(lastTilt); defer(renderGantt); pinBars(); }); From ac4d301018e15af73f3524a764873bfb350fc223 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 16:52:26 +0000 Subject: [PATCH 4/5] Separate project title bubble from progress bar rows Stack the title bubble above the progress track in fixed rows so the label never overlays the gray/blue bar or percent text. Co-authored-by: Tanops --- index.html | 15 ++++++++------- src/app/main.js | 10 ++++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/index.html b/index.html index d0eeff9..8cf9cfc 100644 --- a/index.html +++ b/index.html @@ -275,25 +275,26 @@ /* project header: name on its own line, the progress bar on the line below it */ /* top-align so the row's extra height (the gap before the first task) stays BELOW the bar, instead of being centered and pushing the project name down away from the ribbon */ - .gsumrow{min-height:30px;align-items:flex-start;padding-top:4px} - /* project track: progress bar spans the full width; title bubble sits on the left cap */ - .gsumtrack{position:absolute;top:8px;min-height:28px} + .gsumrow{min-height:30px;align-items:flex-start;padding-top:2px} + /* project header: title bubble on row 1, progress bar + % on row 2 — never stacked */ + .gsumtrack{position:absolute;top:4px} + .gsumbar{position:absolute;left:0;right:0;top:30px;min-height:9px} .gsumline{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%); height:9px;border-radius:999px;background:#ccd4e2;pointer-events:none;z-index:0} .gsumfill{position:absolute;left:0;top:50%;transform:translateY(-50%); height:9px;border-radius:999px;background:#3b6ef6;pointer-events:none;z-index:1;min-width:0} - .gsumlbl{position:absolute;left:0;top:50%;transform:translateY(-50%);z-index:3; + .gsumlbl{position:absolute;top:0;left:0;z-index:2; display:inline-flex;align-items:center; background:var(--surface);border:1px solid var(--line);border-radius:999px; - padding:2px 9px 2px 2px;box-shadow:0 2px 8px rgba(20,24,35,.12); + padding:2px 9px 2px 2px;box-shadow:0 2px 8px rgba(20,24,35,.1); font:inherit;white-space:nowrap;cursor:grab; touch-action:none;-webkit-user-select:none;user-select:none} - .gsumlbl:hover{box-shadow:0 3px 11px rgba(20,24,35,.18)} + .gsumlbl:hover{box-shadow:0 3px 11px rgba(20,24,35,.15)} .gsumlbl .gpin{position:relative;left:0;top:auto;transform:none;padding-left:0; max-width:min(220px,38vw);pointer-events:none;gap:6px} .gsumlbl .gpin .ttl{color:var(--ink);font-size:12px;font-weight:700} .gsumlbl .gava .av{width:20px;height:20px;font-size:8.5px;box-shadow:0 0 0 2px var(--surface)} - .gsumpct{position:absolute;right:0;top:50%;transform:translate(100%,-50%);margin-left:7px; + .gsumpct{position:absolute;left:100%;top:50%;transform:translate(7px,-50%); font-size:11px;font-weight:700;color:var(--ink-3);white-space:nowrap;pointer-events:none;z-index:2} .pgroup.gdropover .gsumline{background:var(--accent)} .pgroup.insb{box-shadow:0 -3px 0 0 var(--accent)} diff --git a/src/app/main.js b/src/app/main.js index 20e4e31..49ae99d 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -413,15 +413,17 @@ function renderGantt(){ let pPts=0; flat([p],x=>{ if(x.children.length) return; pPts+=SIZE_PTS[x.size||"m"]; }); const ph=Math.max(7,Math.min(20,Math.round(5+Math.sqrt(pPts)*2.2))); rows.push(`
-
+
-
-
- ${ppc}% +
+
+
+ ${ppc}% +
${taskRows}
`); From 46b93fc1a887334af856045bc4ec637ebcf36261 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 16:53:42 +0000 Subject: [PATCH 5/5] Fix project label overlapping progress bar Use a flex column inside .gsumtrack so the title bubble sits above the progress bar with a 4px gap. Give project .gtrack an explicit height (ph+34px) instead of the default 26px task-row height, which was compressing both elements into the same vertical space. Co-authored-by: Tanops --- index.html | 7 ++++--- src/app/main.js | 6 +++--- tests/smoke.test.js | 8 ++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 8cf9cfc..299ea59 100644 --- a/index.html +++ b/index.html @@ -277,13 +277,14 @@ instead of being centered and pushing the project name down away from the ribbon */ .gsumrow{min-height:30px;align-items:flex-start;padding-top:2px} /* project header: title bubble on row 1, progress bar + % on row 2 — never stacked */ - .gsumtrack{position:absolute;top:4px} - .gsumbar{position:absolute;left:0;right:0;top:30px;min-height:9px} + .gsumrow .gtrack{height:auto;overflow:visible} + .gsumtrack{position:absolute;top:4px;display:flex;flex-direction:column;align-items:flex-start;gap:4px} + .gsumbar{position:relative;left:0;right:0;top:auto;width:100%;min-height:9px;flex-shrink:0} .gsumline{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%); height:9px;border-radius:999px;background:#ccd4e2;pointer-events:none;z-index:0} .gsumfill{position:absolute;left:0;top:50%;transform:translateY(-50%); height:9px;border-radius:999px;background:#3b6ef6;pointer-events:none;z-index:1;min-width:0} - .gsumlbl{position:absolute;top:0;left:0;z-index:2; + .gsumlbl{position:relative;top:auto;left:0;z-index:2;flex-shrink:0; display:inline-flex;align-items:center; background:var(--surface);border:1px solid var(--line);border-radius:999px; padding:2px 9px 2px 2px;box-shadow:0 2px 8px rgba(20,24,35,.1); diff --git a/src/app/main.js b/src/app/main.js index 49ae99d..aac326a 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -413,7 +413,7 @@ function renderGantt(){ let pPts=0; flat([p],x=>{ if(x.children.length) return; pPts+=SIZE_PTS[x.size||"m"]; }); const ph=Math.max(7,Math.min(20,Math.round(5+Math.sqrt(pPts)*2.2))); rows.push(`
-
+