Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -333,15 +339,14 @@
.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 */
.gbar.gsub .gdot{border-color:currentColor;color:currentColor}
.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}
Expand Down
29 changes: 23 additions & 6 deletions src/app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,7 @@ function renderGantt(){
?`background:#fff;border:2px solid ${col};color:${col}`
:fillBg}">
<i class="ear el" onpointerdown="barDown(event,${n.id},'l')"></i>
<span class="gava">${av(n.owner,"xs")}</span>
<span class="ttl">${n.title}</span>
<div class="gpin"><span class="gava">${av(n.owner,"xs")}</span><span class="ttl">${n.title}</span></div>
<button class="gdot ${done?'on':''}" onpointerdown="event.stopPropagation()"
onclick="event.stopPropagation();ding(4);toggleDone(${n.id})" aria-label="${done?'Undo done':'Mark done'}">${done?'✓':''}</button>
<i class="ear er" onpointerdown="barDown(event,${n.id},'r')"></i>
Expand Down Expand Up @@ -457,10 +456,10 @@ function renderGantt(){
(any?"":'<div class="grow"><span style="color:var(--ink-3);font-size:13.5px;padding:6px 0">No scheduled tasks for this filter.</span></div>')+
`</div></div>`;
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")
Expand All @@ -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<full*0.5){
ttl.style.visibility="hidden";
Expand All @@ -501,6 +500,23 @@ function placeOverflowTitles(){
});
});
}
function pinGanttScroll(){ pinFlags(); pinBars(); }
/* keep avatar + title visible when a bar extends beyond the horizontal viewport */
function pinBars(){
const sc=document.querySelector(".gscroll"); if(!sc) return;
const v0=sc.scrollLeft, v1=v0+sc.clientWidth, pad=6;
sc.querySelectorAll(".gbar").forEach(bar=>{
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;
Expand All @@ -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");
Expand Down Expand Up @@ -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); }
Expand Down
6 changes: 6 additions & 0 deletions tests/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading