app
Adom Gantt
Public Made by Adomby adom
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, push
← Commit history
0.31.0: sticky date axis + sticky per-section headers; drop the 3 seeded phase leads
6 files changed
+92−4
README.md+2@@ -72,6 +72,8 @@ Interactive Gantt chart viewer for project timelines. Data-driven, dark by defau **Clean group labels.** A trailing date range in a group label is dropped at render, since the axis already shows the dates and the text goes stale as soon as the plan moves. +**Sticky axis and section headers.** The date axis stays pinned at the top while you scroll the plan, and the header of whichever section you are in pins just beneath it, handing off to the next section's header at the boundary. Horizontal scroll still slides the axis with the bars so dates stay over their columns. A pinned header is the real row: click it to rename the section, use its chevron to collapse, drag it to reorder.+ **Authored default view.** `project.view` in the data file pins how the chart opens: lens (`mode`), grain, milestone style, today marker, theme and name-column width. The HTML export bakes it in and ignores the viewer's saved preferences, so a shared snapshot always matches the image that invited the click. In the live app your own choices still win. **Changelog.** Every edit is appended to `<datafile>-changelog.jsonl` with the item, the before and after dates, the day shift, and which gesture made it. An AI picking the project up later reads it and builds on your changes instead of overwriting them. The rev counter in the bottom-left corner is the handle: an agent notes it, then asks for only what changed since.
docs/BUILD-NOTES.md+42@@ -3,6 +3,48 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## 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,+and the header of whichever section you are currently inside.++**One synced scroll made this a CSS job.** 0.30.3 already drove both columns from+one wheel handler with identical scrollTop, so `position: sticky` in each column,+at matching top offsets, pins the same rows at the same place and they stay+row-aligned for free. The de-risk measured it: the label header and its timeline+band pin at the same pixel and the two columns' pinned tops are identical arrays.++**The axis is sticky on TOP only**, so a horizontal scroll still slides it sideways+in lockstep with the bars and every date stays over its column, measured at a fine+grain where the timeline overflows: axis and bars both moved 300px. It gained an+opaque background because bars now scroll underneath it, in both themes.++**Section headers are a run of sticky siblings**, which is the classic behaviour:+each header pins just under the axis, and as the next section reaches the top its+header takes the slot. With a flat row structure (no per-section wrapper) the swap+is a REPLACE rather than a slide-and-push: the arriving header, opaque and later in+the DOM, paints over the outgoing one at the boundary. That satisfies "replaced by+the next phase header"; a slide would need every section's rows wrapped in their+own element, which is a refactor this did not need. Verified the pinned section+walks Workcells to Logistics to Probing to Laser to Drone as you scroll, with the+`is-pinned` shadow tracking the one on top.++**A pinned header is the real row, so it stays fully interactive.** Clicking it+renames the phase (and the legend follows), its collapse chevron folds the section,+and dragging it reorders the sections (measured: a pinned NSF header dragged to the+top, server order matching the UI). The today line, milestones and dep arrows keep+rendering under the pinned overlays, which sit above the bars and below nothing+that needs clicking through. Fit, font-scale (the sticky offsets are in `--fs`+units, so they scale with the axis) and collapse all still work, in both themes.+The table view is untouched: its own container scrolls and its `thead` has its own+sticky rule.++**And a tiny data drop rode along.** The three sections seeded with a lead from the+old group labels in 0.29.0 (Workcells "the long pole (Adonis)", Logistics "Dan",+NSF "John") had those cleared at the user's request; the lead FEATURE is untouched,+so a section can still carry one from the editor. Only those three `lead` fields+changed, every row byte-identical.+ ## Build notes, 2026-08-17 (0.30.3, the whole gantt body scrolls) The plan scrolled only when the cursor sat over the timeline side of a task row.
gantt.html+41−1@@ -665,6 +665,26 @@ table.data-table { font-size: calc(12px * var(--fs)); min-width: calc(900px * va everything downstream of it. */ .fs-ctl .fs-pct { min-width: 46px; font-family: 'JetBrains Mono', monospace; font-size: 10px; border-left: 1px solid var(--border); border-right: 1px solid var(--border); } .fs-ctl .fs-pct.off-default { color: var(--accent); }++/* ── Sticky axis and section headers (0.31.0) ──────────────────────────────+ The whole gantt body is one synced vertical scroll (the timeline scrolls, the+ label column mirrors its scrollTop), so position:sticky pins the same rows in+ both columns at the same offsets and they stay row-aligned. The axis is sticky+ at the very top; each phase header is sticky just beneath it, and a run of+ sticky siblings gives the iOS behaviour for free: the next section's header+ pushes the current one up as it reaches the top.+ The axis is sticky on TOP only, so a horizontal scroll still slides it sideways+ with the bars and the dates stay over their columns. */+.q-row, .m-row, .label-h-q, .label-h-m { position: sticky; z-index: 10; background: var(--bg); }+.q-row, .label-h-q { top: 0; }+.m-row, .label-h-m { top: calc(22px * var(--fs)); }+.label-row.group, .bar-row.group {+ position: sticky; top: calc(46px * var(--fs)); z-index: 9;+ /* opaque already (var(--surface)); the shadow lifts a pinned header off the+ rows sliding under it without a hard line that would double the row border. */+}+:root[data-view="gantt"] .label-row.group.is-pinned,+:root[data-view="gantt"] .bar-row.group.is-pinned { box-shadow: 0 3px 6px -3px rgba(0,0,0,0.35); } </style> </head> <body>@@ -1329,6 +1349,7 @@ function reflowRows(){ items.filter(r=>r.type==='milestone'&&r._msLine).forEach(r=>bars.appendChild(r._msLine)); recolorByPhase(); keepTailLast();+ updatePinnedHeader(); } // Bar fill and milestone-rule colour are a PURE FUNCTION of the row's current // phase, nothing else. Before this they were set imperatively at build time and@@ -2517,7 +2538,25 @@ barTail.className='tailroom';timelineEl.appendChild(barTail); const labelTail=document.createElement('div'); function mountLabelTailroom(){ labelTail.className='tailroom';labelsEl.appendChild(labelTail);-timelineEl.addEventListener('scroll',()=>{labelsEl.scrollTop=timelineEl.scrollTop;});+timelineEl.addEventListener('scroll',()=>{labelsEl.scrollTop=timelineEl.scrollTop;updatePinnedHeader();});+}+// 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+// pinned header lifts off the rows sliding beneath it. Cheap: one pass over the+// group rows on scroll.+let pinnedGroup=null;+function updatePinnedHeader(){+ if(prefs.view!=='gantt')return;+ const axis=46*(prefs.fs||1);+ const top=timelineEl.scrollTop;+ 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; } function wireChartWheel(){ const wrap=timelineEl.closest('.chart-wrap')||timelineEl.parentElement;@@ -2540,6 +2579,7 @@ function wireChartWheel(){ // engines), so the labels have to be pulled along in the same tick the wheel is // handled or they lag a frame behind the bars. labelsEl.scrollTop=timelineEl.scrollTop;+ updatePinnedHeader(); // Own the gesture: the label column has no scroll of its own, and the page // behind must never rubber-band because the pointer was over a header. e.preventDefault();
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.30.3",+ "version": "0.31.0", "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.30.3",+ "version": "0.31.0", "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]" }-}+}
server.js+4@@ -503,6 +503,10 @@ function describeEvent(e) { const leads = e.leads && Object.keys(e.leads).length ? `, carrying the old header asides across as section leads (${Object.keys(e.leads).join(', ')})` : ''; return `${who} made sections and phases one thing: ${e.phases || 0} section(s) now own their own headers, and ${e.groupsRetired || 0} group ROW(s) stopped being rows${leads}. Their uids (${(e.retiredUids || []).join(', ')}) are retired for good. The saved order went from ${e.orderBefore || 0} to ${e.orderAfter || 0} entries because headers are derived and no longer stored${via}`; }+ if (action === 'plan.leaddrop') {+ const names = Array.isArray(e.cleared) && e.cleared.length ? e.cleared.join(', ') : 'some sections';+ return `${who} cleared the seeded lead text on ${names}, so those section headers show just the name${via}`;+ } if (action === 'plan.freight') { const nm = Array.isArray(e.names) && e.names.length ? `: ${e.names.join('; ')}` : ''; const en = Array.isArray(e.enriched) && e.enriched.length ? `. Enriched ${e.enriched.join(', ')} rather than duplicating what they already covered` : '';