← Commit history

0.31.1: pinned header no longer covers milestone caps/today tag; top band clears the sticky overlays

Drew Owens ·cb94290bc8 · ·parent 8436246
4 files changed +73−11
docs/BUILD-NOTES.md+30
@@ -3,6 +3,36 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-17 (0.31.1, nothing hides under a pinned band)++0.31.0's sticky section header was opaque and z-above the content, and the+line-style milestone caps all live in a band at the TOP of the timeline. The+first section's header pins in exactly that band, so at rest it covered the caps+sitting under it: milestone tags ("Full dry run done...", "Capitol Hill+reception") were clipped at their tops. Measured: 3 caps hidden under the pinned+header at scroll 0.++**Two changes, both about the top band clearing the pinned overlays.** First, the+caps band and the today tag now START below the pinned header, offset by the+header's height (read from a real group row, so it tracks the font scale). At rest+they sit fully below the pinned axis+header instead of under it. Second, a cap or+the today tag that scrolls UP into the pinned band is hidden the instant its top+crosses the band's bottom edge, and reappears whole once it is fully below again.+That is the clean-edge version of "content scrolls up and vanishes under the+pinned header": a tall two-line tag never shows a clipped sliver, because it is+either wholly below the band or gone, never straddling it.++The today line, the dashed milestone rules and the dep arrows still pass beneath+the pinned overlays, which is fine; only TEXT (tags, the today pill) is masked so+it is never cut.++Proven with a rect-clearance sweep at six scroll depths: zero milestone tags and+zero today tags straddle the pinned axis or header at any of them, the first task+row and the first cap sit fully below the header at rest, all ten caps are visible+at rest and reappear after a scroll round trip, and it holds under a two-step font+scale, a theme switch and the diamond style. Thirteen suites green including the+per-cap drag and the sticky-swap sweep, zero JS errors.+ ## Build notes, 2026-08-17 (0.31.0, the axis and the section header stay put)  Two things now stay pinned while the plan scrolls: the date axis at the top,
gantt.html+40−8
@@ -278,7 +278,7 @@ body.col-resizing { cursor: col-resize; user-select: none; }    over every bar it crosses and eat their clicks, which is a worse trade than a    tooltip. The tag is the marker's affordance, so it carries the tooltip, with a    little more padding to make it a comfortable target. */-.today-tag { position: absolute; top: 4px; transform: translateX(-50%); font-family: 'JetBrains Mono', monospace; font-size: 9px; color: var(--today); background: var(--bg); padding: 2px 6px; border-radius: 3px; border: 1px solid var(--border); pointer-events: auto; cursor: default; z-index: 9; white-space: nowrap; }+.today-tag { position: absolute; top: calc(var(--group-h) + 4px); transform: translateX(-50%); font-family: 'JetBrains Mono', monospace; font-size: 9px; color: var(--today); background: var(--bg); padding: 2px 6px; border-radius: 3px; border: 1px solid var(--border); pointer-events: auto; cursor: default; z-index: 9; white-space: nowrap; } .today-tag:hover { border-color: var(--today); } .dep-svg { position: absolute; top: 0; left: 0; pointer-events: none; z-index: 4; overflow: visible; } .dep-svg path { fill: none; stroke: var(--accent); stroke-width: 1.5; opacity: 0.5; transition: opacity 0.15s; }@@ -2552,11 +2552,31 @@ function updatePinnedHeader(){   const groups=items.filter(isDerivedGroup);   let active=null;   groups.forEach(g=>{ if(g._lDiv && (g._lDiv.offsetTop-top)<=axis+0.5) active=g; });-  if(active===pinnedGroup)return;-  groups.forEach(g=>{ const on=g===active;-    if(g._lDiv)g._lDiv.classList.toggle('is-pinned',on);-    if(g._bDiv)g._bDiv.classList.toggle('is-pinned',on); });-  pinnedGroup=active;+  if(active!==pinnedGroup){+    groups.forEach(g=>{ const on=g===active;+      if(g._lDiv)g._lDiv.classList.toggle('is-pinned',on);+      if(g._bDiv)g._bDiv.classList.toggle('is-pinned',on); });+    pinnedGroup=active;+  }+  maskTopBand(active);+}+// The milestone caps and the today tag live in a band at the top of the timeline.+// A cap that has scrolled up INTO the pinned axis+header band would otherwise show+// a clipped sliver of its (sometimes two-line) label under the overlay. Hide any+// whose top has crossed the pinned band's bottom edge; they reappear, whole, the+// moment they are fully below it again. This is the clean-edge version of "content+// scrolls up and disappears under the pinned header".+function maskTopBand(active){+  const boundary=(active&&active._bDiv)?active._bDiv.getBoundingClientRect().bottom+                 :timelineEl.getBoundingClientRect().top+46*(prefs.fs||1);+  if(prefs.msStyle==='line'){+    items.forEach(r=>{ if(r.type!=='milestone'||!r._msLine)return;+      const cap=r._msLine.querySelector('.ms-cap'); if(!cap)return;+      cap.style.visibility=(cap.getBoundingClientRect().top>=boundary-1)?'':'hidden'; });+  }+  if(todayTagEl&&todayTagEl.style.display!=='none'){+    todayTagEl.style.visibility=(todayTagEl.getBoundingClientRect().top>=boundary-1)?'':'hidden';+  } } function wireChartWheel(){   const wrap=timelineEl.closest('.chart-wrap')||timelineEl.parentElement;@@ -2625,12 +2645,24 @@ function relayout(){ // Wrapping narrows a long milestone title but cannot stop two adjacent ones from // colliding, so caps that still overlap drop to the next row. Measured left-to-right // in date order, each cap takes the first row it fits on.+// The pinned section header, opaque and sticky since 0.31.0, sits in the same top+// band the milestone caps live in. So the caps band starts BELOW it: at rest, a+// cap under the first section's header is no longer hidden by it, and a cap that+// scrolls up still slides under the header (it is z-below) rather than being+// clipped in place. `base` is the header height plus a hair, read from a real+// group row so it tracks the font scale.+function capBandBase(){+  const g=items.find(isDerivedGroup);+  const h=(g&&g._bDiv&&g._bDiv.offsetHeight)|| (28*(prefs.fs||1));+  return Math.round(h)+4;+} function layoutMsCaps(){   if(prefs.msStyle!=='line')return;+  const base=capBandBase();   const caps=[];   items.filter(r=>r.type==='milestone'&&r._msLine&&!r._collapsed).forEach(r=>{     const cap=r._msLine.querySelector('.ms-cap');-    if(cap){cap.style.top='2px';caps.push({cap,x:toX(r.date)});}+    if(cap){cap.style.top=base+'px';caps.push({cap,x:toX(r.date)});}   });   caps.sort((a,b)=>a.x-b.x);   const GAP=6;@@ -2658,7 +2690,7 @@ function layoutMsCaps(){       n++;     }     rows[n].push([l,r]);-    c.cap.style.top=(2+n*rowH)+'px';+    c.cap.style.top=(base+n*rowH)+'px';   }); } 
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.31.0",+  "version": "0.31.1",   "description": "Interactive Gantt chart viewer for project timelines, with a table view over the same data. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser: drag bars to move or resize, multi-select and batch-shift dates, manage dependency arrows inline, reorder rows, filter by phase, and undo. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place. Every edit is recorded in an append-only changelog so an AI can read what a human changed and build on it. Imports JSON, CSV, MS Project XML and Jira CSV; exports JSON, CSV, SVG and a self-contained HTML snapshot.",   "main": "server.js",   "bin": {
page.json+2−2
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.31.0",+  "version": "0.31.1",   "description": "Interactive Gantt chart viewer for project timelines, with a table view over the same data. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser: drag bars to move or resize, multi-select and batch-shift dates, manage dependency arrows inline, reorder rows, filter by phase, and undo. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place. Every edit is recorded in an append-only changelog so an AI can read what a human changed and build on it. Imports JSON, CSV, MS Project XML and Jira CSV; exports JSON, CSV, SVG and a self-contained HTML snapshot.",   "main": "server.js",   "bin": {@@ -63,4 +63,4 @@     "name": "Drew Owens",     "email": "[email protected]"   }-}\ No newline at end of file+}