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.32.0: clickable status pill in the gantt label column, reusing the table status editor
5 files changed
+83−5
README.md+2@@ -30,6 +30,8 @@ Interactive Gantt chart viewer for project timelines. Data-driven, dark by defau **Click-to-edit.** Click any bar to open an inline editor with date pickers and dependency management. Drag bar edges to resize start/end dates. Drag bar body to slide dates while preserving duration. +**Status is editable from the chart.** Click a task's status pill in the label column to change it from a small on-brand dropdown (not started / in progress / blocked / complete). It is the same edit as the table's status cell: one undo, logged to the changelog, reflected in both lenses.+ **Multi-select.** Ctrl+click to toggle individual rows, Shift+click for range select (Windows Explorer style), in either lens. Tasks and milestones both, since both have a permanent id to be named by; lane headers do not join. The batch editor applies relative date shifts across the selected tasks without flattening dates, and says how many milestones it is leaving alone: a milestone has one date and no duration, so shifting its end has no meaning. **Vertical reorder.** Drag bars or labels up/down to reorder rows. Drop indicator shows insertion point. Multi-select reorder moves all selected items together.
docs/BUILD-NOTES.md+35@@ -3,6 +3,41 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-17 (0.32.0, the status pill is editable in the chart)++The little status pill next to a task name in the label column ("not started",+"blocked", ...) was display-only. It is a button now: click it and the four-state+list opens right at the pill; pick one and the status changes.++**It reuses the table's editor, it does not grow a second one.** The list is the+same custom option-list `openCellList` builds for the table selects (0.22.2/0.28.2),+so it is on-brand and there is no native `<select>` popup to double up. The commit+goes through `commitCell(r, 'status', v)`, the SAME function the table's status+cell calls, so the two are one edit: one undo snapshot, the chart label and bar+refreshed, and a save whose diff writes the status event to the changelog. Changed+in the chart or changed in the table, it is the same path, and it shows in both+lenses at once and after a reload.++**The pill and the name are adjacent, so the hit-test is exact.** The pill is its+own element; a plain click on it edits the status and stops there (stopPropagation),+so it never also opens the name editor, selects the row, or arms a drag. A click on+the name still edits the name (0.14.0); a modified click on the pill still falls+through to selection. A mousedown on the pill does not start a lane/row drag, so a+click-and-hold to reorder still works everywhere except on the pill itself.++**Colours were already there.** The per-state pill classes (complete green, blocked+warn, in progress accent, not started neutral) existed for the table; the chart pill+just wears the same ones. The only new styling is the pointer cursor and a subtle+inset outline on hover, so it reads as clickable. Milestones have no status and no+pill, and are untouched.++Verified headless: the pill opens the custom list with four options and the current+one marked, a pick updates the pill and its colour class, the table shows the same+status, the changelog logs it, undo reverts it, and it survives a reload; clicking+the name still edits the name; the pill click neither selects the row nor arms a+drag; the table's own status editor is unchanged. Fourteen suites green on clean+data, zero JS errors.+ ## 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
gantt.html+43−2@@ -225,6 +225,8 @@ body.col-resizing { cursor: col-resize; user-select: none; } .label-row.dim { opacity: 0.15; } .label-row.multi-sel { background: rgba(0,184,176,0.06); } .tag { display: inline-block; font-family: 'JetBrains Mono', monospace; font-size: 9px; padding: 1px 5px; border-radius: 4px; margin-left: 6px; vertical-align: middle; }+.label-row .status-pill { cursor: pointer; }+.label-row .status-pill:hover { box-shadow: inset 0 0 0 1px currentColor; filter: brightness(1.12); } .tag-not-started { background: rgba(139,148,158,0.10); color: var(--text2); } .tag-in-progress { background: rgba(0,184,177,0.14); color: var(--accent); } .tag-blocked { background: rgba(210,153,34,0.15); color: var(--warn); }@@ -2492,7 +2494,7 @@ function buildRowDom(r,idx){ lDiv.appendChild(Object.assign(document.createElement('span'),{className:'row-name'})); lDiv.appendChild(makeEditBtn(r)); r._nameSpan=lDiv.querySelector('.row-name');- const sk=statusOf(r);const tg=sk?`<span class="tag ${statusTagFor(sk)}">${esc(statusTextFor(sk))}</span>`:'';+ const sk=statusOf(r);const tg=sk?`<span class="tag status-pill ${statusTagFor(sk)}" title="Click to change status">${esc(statusTextFor(sk))}</span>`:''; r._nameSpan.innerHTML=esc(r.name)+tg; bDiv.className='bar-row';const x1=toX(r.start),x2=toX(r.end),b=document.createElement('div');b.className='bar'+(r.striped?' bar-striped':'');b.style.left=x1+'px';b.style.width=Math.max(x2-x1,6)+'px';b.style.background=phaseColor(r.phase);b.dataset.title=r.name;b.dataset.dates=fmt(r.start)+' → '+fmt(r.end);b.dataset.body=r.description||'';b.dataset.prov=provLabel(r);b.dataset.id=r.id; const nt=document.createElement('div');nt.className='bar-note';nt.textContent=r.note||'';b.appendChild(nt);r._noteEl=nt;@@ -2905,6 +2907,17 @@ labelsEl.addEventListener('click',e=>{ // the next real click. if(Date.now()-dragEndedAt<150)return; if(e.target.closest('.lane-toggle')||e.target.closest('.row-edit'))return;+ // The status pill is its own control. A plain click on it edits the status and+ // goes no further, so it never also opens the name editor or selects the row. A+ // modified click falls through to selection, the same as clicking the name.+ const pill=e.target.closest('.status-pill');+ if(pill&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey){+ e.stopPropagation();+ const prow=e.target.closest('.label-row[data-id]');+ const pr=prow&&items.find(x=>x._lDiv===prow);+ if(pr&&pr.id)openStatusPill(pr,pill);+ return;+ } const row=e.target.closest('.label-row[data-id], .label-row.ms, .label-row.group'); // Empty space in the label column is a click on nothing, which is how every list // in the world drops a selection.@@ -3030,6 +3043,7 @@ function hideDropIndicators(){ function wireLabelDragArm(){ labelsEl.addEventListener('mousedown',e=>{+ if(e.target.closest('.status-pill'))return; // editing status, not starting a drag const row=e.target.closest('.label-row[data-id]'); if(!row)return; gesture={id:row.dataset.id,startX:e.clientX,startY:e.clientY,src:'label'};@@ -4732,7 +4746,7 @@ function rowItem(key){ function refreshLabelRow(r){ if(!r||!r._lDiv||!r.id)return; const sk=statusOf(r);- const tg=sk?`<span class="tag ${statusTagFor(sk)}">${esc(statusTextFor(sk))}</span>`:'';+ const tg=sk?`<span class="tag status-pill ${statusTagFor(sk)}" title="Click to change status">${esc(statusTextFor(sk))}</span>`:''; // into the name span only: rewriting the row would take the edit button with it if(r._nameSpan)r._nameSpan.innerHTML=esc(r.name)+tg; else r._lDiv.innerHTML=esc(r.name)+tg;@@ -5366,6 +5380,33 @@ function syncCellList(){ if(on&&o.scrollIntoView)o.scrollIntoView({block:'nearest'}); }); }+// The status pill in the label column is editable in place. It reuses the table's+// custom option list (openCellList) rather than a native <select> popup, for the+// same reason the table does (no double-list bug, on-brand styling), and it commits+// through commitCell, the SAME path the table's status editor uses: one snapshot+// for undo, the chart and bar refreshed, and a save whose diff logs the status+// event. So a status changed here and one changed in the table are the same edit.+function openStatusPill(r,pillEl){+ if(!r||!r.id)return;+ closeCellList(); closeLabelEditors(true);+ const sel=document.createElement('select');+ sel.innerHTML=STATUS_OPTS.map(x=>`<option value="${x}">${statusLabel[x]}</option>`).join('');+ const cur=statusOf(r);+ if(cur&&!STATUS_OPTS.includes(cur))sel.innerHTML+=`<option value="${esc(cur)}">${esc(cur)}</option>`;+ sel.value=cur;+ // Offscreen: the list reads its options and current value from it, but nothing+ // ever focuses or shows the select itself.+ sel.style.cssText='position:fixed;left:-9999px;top:0;opacity:0';+ document.body.appendChild(sel);+ openCellList(sel,pillEl);+ const cleanup=()=>{ closeCellList(); if(sel.parentNode)sel.remove();+ document.removeEventListener('mousedown',outside,true); document.removeEventListener('keydown',esc,true); };+ const outside=ev=>{ if(!ev.target.closest('.cell-list'))cleanup(); };+ const esc=ev=>{ if(ev.key==='Escape'){ ev.stopPropagation(); cleanup(); } };+ sel.addEventListener('change',()=>{ const v=sel.value; cleanup(); commitCell(r,'status',v); renderTable(); });+ // Deferred, so the click that opened the list does not immediately close it.+ setTimeout(()=>{ document.addEventListener('mousedown',outside,true); document.addEventListener('keydown',esc,true); },0);+} function openCellList(sel,td){ closeCellList(); const el=document.createElement('div');
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.31.1",+ "version": "0.32.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.31.1",+ "version": "0.32.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]" }-}+}