← Commit history

Publish 0.74.2

Drew Owens ·a4cf340fad · ·parent e443917
6 files changed +65−6
README.md+1−1
@@ -74,7 +74,7 @@ A plain on/off toggle. Each lane is answered on its OWN subgraph, so a dependenc  **Current and Overdue.** Two chips in the legend, for reviewing a plan with the team. **Current** (0.70.0) shows only what has started (start date today or earlier) and is not complete or cancelled: the work under way, plus anything that should have started and has not. Future and finished rows are hidden, in the chart and the table. **Overdue** shows only what is past its deadline and not done. Turn both on for just the overdue rows. Each counts tasks and milestones separately, and neither changes the plan. -**Legend filtering.** Click any phase, "Milestone", "Dependencies" or "Today" in the legend bar to toggle visibility. Dimmed items fade to 15% opacity; hiding "Today" removes the marker line. The legend wraps onto as many lines as the plan needs, so a plan with twenty sections shows all twenty chips. When the sections carry tags (0.59.0), the legend leads with a chip per tag and the twenty section chips fold behind one "20 sections" chip; pick a tag and only the sections carrying it are shown, in the chart and the table, with their own chips back in the legend. Pick several to see any of them; Clear tags shows everything. It is your view only, and it survives a refresh. Since 0.64.0 the legend also leads with an **Owner** pick (every owner with their row count, plus **Unassigned**, shared with the table's owner filter) and an **Overdue** chip: rows whose end date or milestone date has passed without being marked complete or cancelled get a red edge and ring, and the chip shows only them. Overdue is worked out from the dates every time the page draws and at midnight; it is never written into the plan. Changing any of these filters, or Collapse all and Expand all, keeps your place: the row at the top of the chart stays where it is, or, if the filter hides it, the nearest row above it that still shows takes its spot. A row someone else adds or deletes above you does not move your view either (0.74.1).+**Legend filtering.** Click any phase, "Milestone", "Dependencies" or "Today" in the legend bar to toggle visibility. Dimmed items fade to 15% opacity; hiding "Today" removes the marker line. The legend wraps onto as many lines as the plan needs, so a plan with twenty sections shows all twenty chips. When the sections carry tags (0.59.0), the legend leads with a chip per tag and the twenty section chips fold behind one "20 sections" chip; pick a tag and only the sections carrying it are shown, in the chart and the table, with their own chips back in the legend. Pick several to see any of them; Clear tags shows everything. It is your view only, and it survives a refresh. Since 0.64.0 the legend also leads with an **Owner** pick (every owner with their row count, plus **Unassigned**, shared with the table's owner filter) and an **Overdue** chip: rows whose end date or milestone date has passed without being marked complete or cancelled get a red edge and ring, and the chip shows only them. Overdue is worked out from the dates every time the page draws and at midnight; it is never written into the plan. Changing any of these filters, or Collapse all and Expand all, keeps your place: the row at the top of the chart stays where it is, or, if the filter hides it, the nearest row above it that still shows takes its spot. A row someone else adds or deletes above you does not move your view either (0.74.1). Scrolled all the way up, you stay at the top, and a section's chevron stays under the pointer when it folds (0.74.2).  **How long, and how long left.** The header says **Project Duration** and **Time Remaining**, worked out from the plan's own first and last dates and today, never from the cell size, leaving cancelled work out, and updated on every edit. They stay whole at every window width: the header sheds button words, then wraps, before it lets them be cut. A printed chart names the plan in a title line of its own. Hover either for the exact dates and the calendar and working-day counts. The plan itself is named once, in the brand block at the far left; its title and description are on that block's tooltip. 
docs/BUILD-NOTES.md+24
@@ -3,6 +3,30 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-10-28 (0.74.2, the top stays the top, a pinned header keeps its chevron)++The full suite's coverage walk failed on 0.74.1 (a section left folded). Tracing it found+three things a person would hit too.++At the top of the plan (scrollTop 0) there is no place to keep. 0.74.1 held the first row in view even at+scrollTop 0, and the first header counts as pinned there, so Expand all from the top held+the SECOND header and scrolled the first section's rows out of view above, and clearing a+filter from the top jumped into the middle of the plan. placeMark returns nothing at the top.++The link buttons (0.73.0, z-index 11) and initials chips (0.74.0, z 10) of the row sliding+under a pinned section header (z 9) painted over it and took its clicks: the chevron of the+pinned section could not be clicked. Each non-header row is now its own stacking context+(z-index 0), so its buttons stay inside it.++Folding the pinned section by its chevron slid the next header up over it, so the chevron+that would open it again was gone. The chevron's header is now held where it is drawn+(keepPlace on that header), and the same spot opens it again. A single fold no longer forces+syncLabelScroll (the arrow-layer fix keeps both panes the same height); and the names column,+when it scrolls on its own (keyboard focus to a control out of view), now brings the chart+along instead of leaving it behind.++Suite: "scroll sync" gains the pinned-fold checks; the coverage walk passes again.+ ## Build notes, 2026-10-28 (0.74.1, names and bars stay together after a filter)  Drew: "super out of sync when I clicked to filter unassigned" (the names and the bars showed
gantt.html+24−3
@@ -1473,6 +1473,10 @@ a.prov-chip:hover { border-color: var(--accent); background: rgba(0,184,177,0.08    name column never resizes, same rule the table cells follow. */ .label-edit { position: absolute; inset: 3px 6px; width: auto; background: var(--bg); border: 1px solid var(--accent); border-radius: 4px; color: var(--text); font-family: 'Satoshi', sans-serif; font-size: calc(12px * var(--fs)); padding: 0 6px; outline: none; z-index: 12; box-sizing: border-box; } .label-row { position: relative; }+/* A row keeps its buttons and chips to itself (0.74.1): the link button (z 11) and the+   initials (z 10) sat above the pinned section header (z 9) as their row slid under+   it, showed through it, and took the click meant for its chevron. */+.label-row:not(.group) { z-index: 0; } /* Appears only under the cursor, absolutely positioned so it never moves the    name text or changes the row's height. */ .row-edit { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; padding: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 4px; color: var(--text2); cursor: pointer; opacity: 0; pointer-events: none; transition: opacity 0.12s, color 0.12s, border-color 0.12s; z-index: 11; }@@ -2436,7 +2440,7 @@ function setCollapsed(ph,on,persist){   if(persist!==false){ prefs.collapsed=[...collapsed]; savePrefs(); }   applyCollapse();   layoutMsCaps();sizeTailroom();drawDeps();-  if(prefs.view==='gantt'){ syncLabelScroll(); updatePinnedHeader(); }+  if(prefs.view==='gantt')updatePinnedHeader(); } function wireLaneToggle(){ labelsEl.addEventListener('click',e=>{@@ -2444,7 +2448,17 @@ labelsEl.addEventListener('click',e=>{   if(!btn)return;   e.stopPropagation();   const g=items.find(r=>r._laneBtn===btn);-  if(g)setCollapsed(g.phase,!isCollapsed(g.phase));+  if(g){+    // The chevron stays under the pointer (0.74.1). Folding the section whose header is+    // pinned slid the next header up over it, and the chevron that would open it again+    // was gone. The header is held where it is drawn, so it lands at its own place.+    // The names may have just scrolled on their own (focus, a find) and the click landed+    // where they are, so the bars catch up before anything is measured.+    if(Math.abs(labelsEl.scrollTop-timelineEl.scrollTop)>0.5)timelineEl.scrollTop=labelsEl.scrollTop;+    const y=prefs.view==='gantt'&&g._lDiv?g._lDiv.getBoundingClientRect().top:null;+    setCollapsed(g.phase,!isCollapsed(g.phase));+    if(y!==null)keepPlace({r:g,i:items.indexOf(g),y:y-timelineEl.getBoundingClientRect().top});+  }   // A lane folding away above the pointer slides a different row under it without   // moving it, which is the same stale-row problem a scroll causes and the same answer.   depHoverResync();@@ -5705,7 +5719,9 @@ function unpinnedRead(fn){   try{ return fn(); }finally{ bars.classList.remove('unpinned'); } } function placeMark(){-  if(prefs.view!=='gantt')return null;+  // At the very top there is nothing above to keep in place: the reader stays at the+  // top, where the first section starts, and a fold or a filter reads from there.+  if(prefs.view!=='gantt'||timelineEl.scrollTop<1)return null;   // Read before the lift: the band ends under the header that is pinned now.   const paneTop=timelineEl.getBoundingClientRect().top, band=chartViewBand().top;   return unpinnedRead(()=>{@@ -5735,6 +5751,11 @@ function keepPlace(mark){ function mountLabelTailroom(){ labelTail.className='tailroom';labelsEl.appendChild(labelTail); timelineEl.addEventListener('scroll',()=>{syncLabelScroll();updatePinnedHeader();});+// The names can be scrolled on their own: focus moving to a control out of view (Tab+// to a lane chevron, a rename field) scrolls its nearest scroller, which is this column,+// and the bars stayed where they were. The timeline follows them instead (0.74.1); its+// own scroll then mirrors back, already equal, and stops there.+labelsEl.addEventListener('scroll',()=>{ if(Math.abs(labelsEl.scrollTop-timelineEl.scrollTop)>0.5)timelineEl.scrollTop=labelsEl.scrollTop; }); } // Which section header is currently stuck under the axis. Pure CSS does the // pinning and the swap; this only adds the class the drop-shadow hangs on, so a
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.74.1",+  "version": "0.74.2",   "description": "Interactive Gantt chart viewer for project timelines, with a table view and an analytics page 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. One click auto-schedules the whole plan so every dependency lines up finish-to-start on working days, skipping weekends and US federal holidays, pushing work LATER only so a lead-time gap you set on purpose is never closed, and one more lights up the critical path, meaning the longest run of work inside every section at once. Cancelled work is left out of both: it never moves, it constrains nothing and it can never glow. Dependency arrows are coloured by the phase of the task they come out of, so where a chain starts reads at a glance. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for total, remaining and average hours, one row per person carrying both their hours and their status counts, per-section totals, milestones, dependency stats and what is overdue. Copy and paste rows: Ctrl+C the selected tasks and milestones, Ctrl+V to drop clones directly below a selected row or at the end of the lane you are looking at, with dependencies AMONG the copied rows remapped onto the copies so a duplicated chain is still a chain, links pointing outside it left naming the originals, and the whole paste one undo and one changelog line. Five statuses in lifecycle order, not started, in progress, complete, blocked, cancelled, with cancelled keeping the row and its history on the plan while taking its hours out of every total. 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+1−1
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.74.1",+  "version": "0.74.2",   "description": "Interactive Gantt chart viewer for project timelines, with a table view and an analytics page 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. One click auto-schedules the whole plan so every dependency lines up finish-to-start on working days, skipping weekends and US federal holidays, pushing work LATER only so a lead-time gap you set on purpose is never closed, and one more lights up the critical path, meaning the longest run of work inside every section at once. Cancelled work is left out of both: it never moves, it constrains nothing and it can never glow. Dependency arrows are coloured by the phase of the task they come out of, so where a chain starts reads at a glance. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for total, remaining and average hours, one row per person carrying both their hours and their status counts, per-section totals, milestones, dependency stats and what is overdue. Copy and paste rows: Ctrl+C the selected tasks and milestones, Ctrl+V to drop clones directly below a selected row or at the end of the lane you are looking at, with dependencies AMONG the copied rows remapped onto the copies so a duplicated chain is still a chain, links pointing outside it left naming the originals, and the whole paste one undo and one changelog line. Five statuses in lifecycle order, not started, in progress, complete, blocked, cancelled, with cancelled keeping the row and its history on the plan while taking its hours out of every total. 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": {
test/ui_test.mjs+14
@@ -3668,6 +3668,20 @@ async function scrollSyncFlow(browser) {   await page.click('#btnCollapseAll'); await settle(page, 250);   v = await look();   check('Expand all: row for row, and that section stays put', v.worst === 0 && v.l === v.t && v.at['Lane 15'] === y && v.at[P] > y, JSON.stringify({ worst: v.worst, l: v.l, t: v.t, lane: v.at['Lane 15'], first: v.at[P], y }));+  // Folding the section whose header is pinned: the next header used to slide up over it,+  // hiding the chevron that opens it again. The header stays where it is drawn, so the+  // same spot, clicked again, opens it.+  v = await putAtTop(R);+  const pinTop = v.band - await page.$eval('#labels .label-row.group.is-pinned', e => e.offsetHeight);+  const chev = await page.$eval('#labels .label-row.group.is-pinned .lane-toggle', e => { const b = e.getBoundingClientRect(); return { x: b.left + b.width / 2, y: b.top + b.height / 2 }; });+  await page.mouse.click(chev.x, chev.y); await settle(page, 250);+  v = await look();+  let shut = await page.$$eval('.lane-toggle.collapsed', e => e.length);+  check('folding the pinned section keeps its header in place, the next one below it', shut === 1 && v.worst === 0 && v.l === v.t && v.at['Lane 15'] === pinTop && v.at['Lane 16'] > pinTop, JSON.stringify({ shut, worst: v.worst, l: v.l, t: v.t, lane: v.at['Lane 15'], next: v.at['Lane 16'], pinTop }));+  await page.mouse.click(chev.x, chev.y); await settle(page, 250);+  v = await look();+  shut = await page.$$eval('.lane-toggle.collapsed', e => e.length);+  check('and the same spot clicked again opens it, rows under it', shut === 0 && v.worst === 0 && v.at['Lane 15'] === pinTop && v.at[P] > pinTop, JSON.stringify({ shut, worst: v.worst, lane: v.at['Lane 15'], first: v.at[P], pinTop }));   // Another tab deletes the plan's first row, far above this tab's view. The live merge   // takes it away here too, and the row this tab is looking at does not move.   v = await putAtTop(R);