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.23.0: moving a selection, and the drone sticky board
Alt+Up and Alt+Down move a whole chart selection as a group. Each selected row steps
one place, so the gaps survive: a non-contiguous selection stays non-contiguous
instead of compressing into a block on the way past its neighbours.
Order of operations is the trick. Going down the bottom-most selected row moves
first, so it never steps over a sibling that has not moved yet; going up the topmost
does. Backwards, and an adjacent pair swaps with itself instead of travelling.
Simulated on a copy of items first: if ANY selected row has nowhere to go the press
does nothing at all, rather than moving the ones that can and squashing the selection
against the end of the plan.
One undo entry per press covering the whole selection, keeping the unit the same as
single-row Alt+Arrow. It is an order snapshot carrying every phase the press changed,
the shape a lane drag already used, so 0.20.0's inverse machinery gives redo free.
Three presses, three undos, three redos. The changelog still coalesces the burst into
one line.
Crossing a group boundary re-homes only the row that crossed, exactly as a single-row
move and a drag do. The selection survives every move and still looks selected.
Also: the 14 drone planning stickies, added as one contiguous block after the
existing drone design work in the Workcells lane, owners as written, two
consolidations applied (Cork header plus three Cork stickies into one, two Faraday
stickies into one), placeholder dates and not-started for Drew to correct, provenance
the plain label 'Drone planning board (photo)' with no url because a photograph is
not addressable. Appended to the data file so no #row key renumbers, placed via the
sidecar order array, stateRev bumped.
5 files changed
+104−5
SKILL.md+1−1@@ -207,7 +207,7 @@ rather than a category. - **Up / Down** in the table move the selected row, following the visible order under any sort or filter. They never scroll the pane on their own: the browser's default is suppressed and the pane moves only enough to keep the selected row in view. The first press selects the first visible row. In the chart the arrows are left alone - **Enter** on the selected row opens its NAME editor, ready to type with the text selected. Inside a cell editor Enter commits and steps down one row, the spreadsheet convention, so a column of values is Enter, type, Enter, type; it does not open the next editor, and a second Enter does - **Shift+N** opens the New panel, gated on the app being idle: no editor, panel, menu or drag in progress, and nothing focused that a person could be typing into-- **Alt+Up / Alt+Down** moves one row one slot. The table moves its focused row (the row whose cell editor was touched last, marked with an accent bar on its left edge); the chart moves its selected task. In plan order the movement is free, and stepping over a group header re-homes the row into that lane, phase, bar colour and dashed rule together, exactly as a drag does. Under a column sort the movement is bounded to the contiguous run of rows the sort considers TIED with this one, so an owner-sorted Adonis task moves within the Adonis block and nowhere else: the visible sort stays truthful and what changes is the tie-break, which is plan position, which persists. A row alone in its sort key does not move and says so. Undo is per press; the changelog coalesces a burst into one line+- **Alt+Up / Alt+Down** moves one row one slot, or the WHOLE chart selection as a group when more than one row is selected: each selected row steps one place so the gaps between them survive, and if any of them has nowhere to go the press does nothing rather than squashing the selection against the end. One undo per press either way. The table moves its focused row (the row whose cell editor was touched last, marked with an accent bar on its left edge); the chart moves its selected task. In plan order the movement is free, and stepping over a group header re-homes the row into that lane, phase, bar colour and dashed rule together, exactly as a drag does. Under a column sort the movement is bounded to the contiguous run of rows the sort considers TIED with this one, so an owner-sorted Adonis task moves within the Adonis block and nowhere else: the visible sort stays truthful and what changes is the tie-break, which is plan position, which persists. A row alone in its sort key does not move and says so. Undo is per press; the changelog coalesces a burst into one line - **Tab / Shift+Tab** walks the selected row's fields at one keypress per column, including the select and date columns. A select cell shows its choices in the app's own list rather than the browser's popup, because that popup swallows the first Tab to close itself; the select keeps all its native keyboard behaviour. Date cells keep their native arrow keys for segments and values, and Tab leaves the field instead of walking its segments. With no editor open it opens the first editable cell (Shift+Tab the last); inside an editor it commits the field and opens the next editable cell, with the same ready-to-type conventions as a click (a select opens its list, short text is selected, notes get the caret at the end). It skips read-only cells and the delete button, follows the visible order under a sort, and wraps across rows and around both ends of the table - **Auto-save** to `<datafile>-state.json` sidecar (800ms debounce, flushed immediately if the page is closed or hidden, so the changelog and the sidecar cannot disagree) - **Auto-load** saved state on page refresh
docs/BUILD-NOTES.md+45@@ -3,6 +3,51 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-11 (0.23.0, moving a selection, and the sticky board)++**Alt+Up and Alt+Down move a whole chart selection as a group.** Each selected row+steps one place, so the gaps between them survive: a non-contiguous selection stays+non-contiguous instead of compressing into a block on the way past its neighbours.++Order of operations is the whole trick. Going down, the bottom-most selected row+moves first, so it never steps over a sibling that has not moved yet; going up, the+topmost does. Get that backwards and a pair of adjacent rows swaps with itself+instead of travelling.++The move is simulated on a copy of `items` first. If ANY selected row has nowhere to+go, the press does nothing at all, rather than moving the ones that can and quietly+squashing the selection against the end of the plan. Measured: a selection containing+the last row no-ops as a whole and says so.++**One undo entry per press, covering the whole selection**, which keeps the unit the+same as single-row Alt+Arrow: one press, one Ctrl+Z. It is an `order` snapshot+carrying every phase the press changed, the same shape a lane drag already used, so+0.20.0's inverse machinery gives redo for nothing. Three presses took three undos and+three redos. The changelog still coalesces the burst into one line through the same+600ms window.++Crossing a group boundary re-homes just the row that crossed, exactly as a single-row+move and a drag do, and the rows that did not cross keep their lane: verified by+watching the bar colours, one changing and its companion not. The selection survives+every move and still looks selected, because the user is going to press the key again.++**And the drone sticky board**, 14 items photographed off a physical board, added as+one contiguous block after the existing drone design work in the Workcells lane, with+the owners as written on the stickies. Two consolidations were applied before the+transcription: a Cork connectors header sticky plus three identical Cork stickies+became one row, and two Faraday cage stickies became one. Dates are placeholders and+every row is not started, for Drew to correct. Provenance is the plain label "Drone+planning board (photo)" with no url, because a photograph is not addressable and a+fake link is worse than none.++Same operational pattern as the long-lead adds, for the same reason: APPENDED to the+data file so no `#row` key below them renumbers, then placed by inserting their ids+into the sidecar's order array after the anchor row, with assertions that the block+is contiguous and in order, that the pre-existing order is untouched once the new ids+are removed, that no other sidecar key moved, and that the order still resolves to+exactly the surviving rows. `stateRev` bumped so a tab that loaded earlier gets a 409+rather than writing a copy without them back.+ ## Build notes, 2026-08-11 (0.22.4, the stuck teal outlines were not a selection) Reported as two rows stuck with the teal selection outline in the chart's label
gantt.html+55−1@@ -1317,13 +1317,67 @@ function flushKeyMove(){ clearTimeout(keyMoveTimer);keyMoveTimer=null; scheduleSave('key-move'); }+// Move a whole selection one slot, as a group. Each selected row steps one place,+// so the gaps between them survive: a non-contiguous selection stays non-contiguous+// rather than compressing into a block on the way. Order of operations matters and+// is the whole trick: going down, the bottom-most selected row moves first, so it is+// never stepping over a sibling that has not moved yet; going up, the topmost does.+//+// Simulated on a copy first. If ANY selected row has nowhere to go, the press does+// nothing at all rather than moving the ones that can and quietly squashing the+// selection against the end of the plan.+function planGroupMove(rows,dir){+ const arr=items.slice();+ const ordered=rows.slice().sort((a,b)=>arr.indexOf(a)-arr.indexOf(b));+ const seq=dir>0?ordered.slice().reverse():ordered;+ for(const r of seq){+ const idx=arr.indexOf(r);+ if(idx<0)return null;+ let j=idx+dir, crossed=false;+ while(j>=0&&j<arr.length&&arr[j].type==='group'){ crossed=true; j+=dir; }+ if(j<0||j>=arr.length)return null; // this one is at the end of the plan+ const target=arr[j];+ arr.splice(idx,1);+ const ti=arr.indexOf(target);+ if(ti<0)return null;+ arr.splice(((dir>0)!==crossed)?ti+1:ti,0,r);+ }+ return arr;+}+function moveSelectedRows(dir){+ const rows=items.filter(r=>r.id&&selected.has(r.id));+ if(rows.length<2)return false;+ const before=items.map(rowOrderKey);+ const fromPhases=rows.map(r=>({r,from:r.phase}));+ const next=planGroupMove(rows,dir);+ if(!next){ flash(saveInd,'the end of the plan'); return true; }+ items.length=0; items.push(...next);+ // One undo entry for the whole press, carrying every phase it changed, exactly+ // like a lane drag. The 0.20.0 inverse machinery gives redo for nothing.+ pushUndo({type:'order',order:before,phases:fromPhases});+ rows.forEach(r=>{+ const np=phaseAtPosition(r);+ if(np!==undefined&&np!==r.phase){+ r.phase=np;+ if(r.id&&barEls[r.id])barEls[r.id].bar.style.background=phaseColor(np);+ if(r._msLine)r._msLine.style.color=phaseColor(np);+ }+ });+ applyCollapse();reflowRows();applyFilter();refreshChartGeometry();renderTable();+ refreshSelVisuals(); // the selection survives, and still looks it+ moveHint=rows.length+' rows';+ scheduleKeyMoveSave();+ return true;+} function moveFocusedRow(dir){ const table=prefs.view==='table';+ // More than one row selected on the chart moves the lot, as a group.+ if(!table&&selected.size>1&&moveSelectedRows(dir))return; const r=table?rowItem(focusedKey):soleSelectedRow(); if(!r){ flash(saveInd, table ? 'click a cell first, then Alt+Up and Alt+Down move that row'- : (selected.size>1?'select one task to move it':'select a task first'));+ : 'select a task first'); return; } const sorted=table&&!manualOrder();
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.22.4",+ "version": "0.23.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.22.4",+ "version": "0.23.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]" }-}+}