← Commit history

0.32.0: clickable status pill in the gantt label column, reusing the table status editor

Drew Owens ·6d8c06211d · ·parent 6a28d6f
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]"   }-}\ No newline at end of file+}