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.19.0: arrow keys walk the table
Up and Down move the selected row, Enter edits its name, and neither touches the
scrollbar.
The focused row from 0.17.0 was already the selection, so this is arrows driving a
model that existed rather than a second one beside it. The walk follows the rows
the table is SHOWING, which is what makes a sort or a filter change the meaning of
'the next row' without the walk knowing about either.
preventDefault is the feature, not a detail: the arrows are consumed whenever the
table can act on them, and the pane moves only when the chosen row is off the
edge. scrollRowIntoView does that by hand rather than via scrollIntoView, because
the header is sticky so the usable top of the pane is the header's bottom, and
because only this scroller should move. Measured: five presses with rows in view
leave scrollTop at exactly 0, and walking seven rows past the fold scrolls 215px
against a 29px row, which is the overshoot and nothing more.
The first press lands on the first visible row and moves nothing. If a row was
touched earlier it is already the focused row and the walk carries on from it.
Enter on a focused row opens the 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; a second Enter does, and the pair reads as 'done, next'.
Guards: nothing moves while any editor, panel, menu, picker or drag is live, or
while anything anywhere is focused that a person could be typing into. Probed
against all eight editable column types, the create panel's own name field, the
search box, and a mid-drag row. In the chart the arrows are not intercepted at all,
so the timeline keeps the browser's scrolling.
Alt is checked first, so the two arrow meanings never race: with Alt the plan
changes, without it only the selection does.
Fourteen groups of checks, zero errors. The 55-check keyboard and drag sweep and
the 35-step fixed-column battery both come back identical, and the 52-key
fingerprint is unchanged from 0.18.2 apart from the version string.
6 files changed
+143−6
README.md+2@@ -126,6 +126,8 @@ Omit `color` on a phase and it follows the Adom brand ramp, which adapts between | Key | Action | |-----|--------| | Ctrl+Z | Undo |+| Up / Down | Move the selected row in the table, following the visible order under sorts and filters. They never scroll the pane by themselves; it moves only enough to keep the selected row in view |+| Enter | Edit the selected row's name. Inside a cell, commit and step down one row | | Shift+N | Open the New panel. Fires only when nothing else is going on, so a capital N typed into a field stays a capital N | | Alt+Up / Alt+Down | Move a row one slot: the focused row in the table, the selected task in the chart. Crossing a lane boundary moves the row into that lane. Under a column sort the row stays inside the run of rows the sort considers tied with it | | Tab / Shift+Tab | In the table, commit the open cell editor and open the next one, ready to type. Wraps across rows and around the ends, and follows the visible order under a sort |
SKILL.md+2@@ -195,6 +195,8 @@ rather than a category. - **Click a bar** to open the date editor with start/end pickers and dependency management - **Drag bar edges** to resize (changes start or end date). Every timeline drag snaps to the grid the axis is currently drawing: day boundaries at the day cell size, week starts at weeks, month, quarter and year firsts at the coarser ones. Resizing the start leaves the end where it is and the mirror for the end; dragging the bar body snaps the START and preserves the DURATION, so the end lands at start plus the same number of days. The preview snaps live, line to line, and is exactly what gets committed. The shortest bar a resize can produce is the first grid line past the fixed edge, so it is never zero or negative. Milestones snap the same way. Dates set explicitly (table cells, the item modal, the batch editor, the project-start shift) are taken exactly as given and are never rounded - **Ctrl+Z** to undo (100-level stack)+- **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 - **Tab / Shift+Tab** inside a table cell editor 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
docs/BUILD-NOTES.md+57@@ -3,6 +3,63 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-10 (0.19.0, arrow keys walk the table)++"Up down arrow keys should move which row is selected. Hitting Enter on a row+should make the highlighted task's name editable. No value from up/down keys moving+the vertical scroll bar."++**The focused row from 0.17.0 was already the selection, so this is arrows driving+a model that existed rather than a second one beside it.** Up and Down walk the+rows the table is SHOWING, which is what makes a sort or a filter change the+meaning of "the next row" without the walk knowing anything about either. Measured+under plan order, under an owner sort, and with an owner filter down to 15 rows.++**preventDefault is the feature, not an implementation detail.** The request was+that the arrows stop nudging the scrollbar, so they are consumed whenever the table+can act on them, and the pane only moves when the row you chose is off the edge.+`scrollRowIntoView` does it by hand rather than through `scrollIntoView({block:+'nearest'})` for two reasons: the header is sticky, so the usable top of the pane+is the header's bottom and not the pane's, and `scrollIntoView` walks every+scrollable ancestor while only this one should move. Measured: five presses with+the rows in view leave `scrollTop` at exactly 0, and walking seven rows past the+fold scrolls 215px against a 29px row, which is the overshoot and nothing more,+with the focused row landing inside the pane and below the header.++**The first press lands on the first visible row and moves nothing.** A walk has to+start somewhere and starting from nowhere is the one case a user cannot predict. If+a row was touched earlier it is already the focused row and the walk carries on+from it, so the "last-touched" behaviour is there for free without a second rule.++**Enter, twice, is the loop.** On a focused row Enter opens the NAME editor, which+is the field a person almost always means and the one the chart's label click+already opens, 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 deliberately does NOT open the next editor: Enter+again does that, and the pair reads as "done, next" rather than trapping you in an+editing mode you have to escape from. On the last row it commits and stays.++**The guards are the part that could have gone wrong.** A native select and a date+input do their own thing with the arrows and they have to win, so nothing moves+while any editor, panel, menu, picker or drag is live, or while anything anywhere+is focused that a person could be typing into. Probed against all eight editable+column types, the create panel with its own name field focused, the table search+box, and a mid-drag row with the drop indicator showing: in every one of them the+selection did not move and the editor stayed open. In the chart the arrows are not+intercepted at all (`defaultPrevented` is false), so the timeline keeps the+browser's scrolling: the chart's selection is a multi-select set with an anchor and+modifier rules, and giving it arrows is a different feature with different+questions.++**The two arrow meanings never race** because Alt is checked first: with Alt the+plan changes, without it only the selection does. Verified in one sequence, that an+Alt+Down reorders while the focus stays on the row it moved, Ctrl+Z puts the order+back, and a plain Down then moves the selection with the order untouched.++Fourteen groups of checks, all passing, zero errors. The 55-check keyboard and drag+sweep and the 35-step fixed-column battery both come back identical, and the 52-key+fingerprint is unchanged from 0.18.2 apart from the version string.+ ## Build notes, 2026-08-10 (0.18.2, the table is a grid again) User report: "columns in table view seem to resize depending on where I click."
gantt.html+79−3@@ -1335,14 +1335,37 @@ function appIsIdle(){ function wireGlobalKeys(){ document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&e.key==='z'){e.preventDefault();undo();}- // Alt+Up and Alt+Down move a row. Not the bare arrows: those belong to whatever- // has the keyboard, and to scrolling when nothing does.+ // Alt+Up and Alt+Down MOVE a row. Checked first, so the two arrow meanings never+ // race: with Alt the plan changes, without it only the selection does. if(e.altKey&&!e.ctrlKey&&!e.metaKey&&(e.key==='ArrowUp'||e.key==='ArrowDown')){ if(isTypingTarget(document.activeElement))return; e.preventDefault(); moveFocusedRow(e.key==='ArrowDown'?1:-1); return; }+ // Plain Up and Down move the SELECTION, the way a spreadsheet does. The+ // preventDefault is the point of the feature: the arrows are for choosing a row,+ // not for nudging a scrollbar, and the pane only moves when the row you chose is+ // off the edge. Only in the table: the chart's selection is a multi-select set+ // with its own anchor and modifier rules, and giving it arrows is a different+ // feature with different questions, so it keeps the browser's scrolling.+ if(!e.altKey&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey&&(e.key==='ArrowUp'||e.key==='ArrowDown')){+ if(!tableNavReady())return; // an open editor, a panel or a drag wins+ e.preventDefault();+ moveRowFocus(e.key==='ArrowDown'?1:-1);+ return;+ }+ // Enter on the focused row edits its name, which is the field you almost always+ // want and the one the chart's label click already opens.+ if(e.key==='Enter'&&!e.altKey&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey){+ if(!tableNavReady())return;+ const tr=focusedRowEl();+ const td=tr&&tr.querySelector('td.c-name.editable');+ if(!td)return;+ e.preventDefault();+ startCellEdit(td);+ return;+ } // Shift+N opens the create panel, and only when the app is genuinely idle: a // capital N typed into a task name is a capital N and nothing else. if(e.shiftKey&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&(e.key==='N'||e.key==='n')){@@ -2642,6 +2665,8 @@ $('btnHelp').addEventListener('click',e=>{ <div class="dd-sep"></div> <div class="dd-label">keyboard</div> <div class="dd-item" style="cursor:default"><span style="color:var(--accent);min-width:90px;font-family:'JetBrains Mono',monospace;font-size:10px">Ctrl+Z</span>Undo</div>+ <div class="dd-item" style="cursor:default"><span style="color:var(--accent);min-width:90px;font-family:'JetBrains Mono',monospace;font-size:10px">Up/Down</span>Move the selected row in the table. The pane only scrolls to keep it in view</div>+ <div class="dd-item" style="cursor:default"><span style="color:var(--accent);min-width:90px;font-family:'JetBrains Mono',monospace;font-size:10px">Enter</span>Edit the selected row's name. In a cell, commit and step down a row</div> <div class="dd-item" style="cursor:default"><span style="color:var(--accent);min-width:90px;font-family:'JetBrains Mono',monospace;font-size:10px">Shift+N</span>New task or milestone</div> <div class="dd-item" style="cursor:default"><span style="color:var(--accent);min-width:90px;font-family:'JetBrains Mono',monospace;font-size:10px">Alt+Up/Down</span>Move the row one slot. Under a sort it stays inside its tied group</div> <div class="dd-item" style="cursor:default"><span style="color:var(--accent);min-width:90px;font-family:'JetBrains Mono',monospace;font-size:10px">Tab</span>Commit and open the next field. Shift+Tab goes back</div>@@ -3679,6 +3704,47 @@ function markFocusedRow(){ if(tr)tr.classList.add('row-focus'); } function setFocusedRow(k){ focusedKey=k||null; markFocusedRow(); }+function visibleRowEls(){ return [...tableBody.querySelectorAll('tr[data-key]')]; }+function focusedRowEl(){ return focusedKey?visibleRowEls().find(t=>t.dataset.key===focusedKey)||null:null; }+// Keep the focused row in view, and move the pane by the least it can get away+// with. The arrows themselves never scroll (preventDefault sees to that, which is+// the whole point of the request); this puts back exactly the amount of scrolling+// that walking off the edge actually needs. Done by hand rather than with+// scrollIntoView because the header is sticky, so the usable top of the pane is+// its bottom edge and not the pane's, and because only this scroller should move.+function scrollRowIntoView(tr){+ const sc=document.querySelector('.table-scroll');+ if(!sc||!tr)return;+ const pane=sc.getBoundingClientRect(), row=tr.getBoundingClientRect();+ const top=Math.max(pane.top, tableHead.getBoundingClientRect().bottom);+ if(row.top<top) sc.scrollTop-=(top-row.top);+ else if(row.bottom>pane.bottom) sc.scrollTop+=(row.bottom-pane.bottom);+}+function focusRowAt(i){+ const rows=visibleRowEls();+ if(!rows.length)return false;+ const tr=rows[Math.max(0,Math.min(rows.length-1,i))];+ setFocusedRow(tr.dataset.key);+ scrollRowIntoView(tr);+ return true;+}+// Up and Down walk the rows the table is SHOWING, so a sort or a filter changes+// what "the next row" means and the walk follows without knowing anything about+// either.+function moveRowFocus(dir){+ const rows=visibleRowEls();+ if(!rows.length)return;+ const i=rows.findIndex(t=>t.dataset.key===focusedKey);+ // Nothing focused yet, so the first press lands on the first visible row rather+ // than moving from nowhere. If a row was touched earlier it is already the+ // focused row and the walk simply carries on from it.+ if(i<0){ focusRowAt(0); return; }+ focusRowAt(i+dir);+}+// Arrow navigation belongs to the table and only when the table is otherwise idle,+// because a native select and a date input do their own thing with the arrows and+// they have to win.+function tableNavReady(){ return prefs.view==='table' && appIsIdle(); } // Tab commits the field and opens the next one to the right, ready to type. The // walk is over the rendered cells, so it follows the visible order under a sort, // skips the milestone start cell and the delete button because neither carries@@ -3774,7 +3840,17 @@ function startCellEdit(td){ // Ctrl+Z inside a field is the browser's own text undo, and Esc means "cancel // this cell", not "clear the chart selection", so the app's keys stop here. el.addEventListener('keydown',e=>{e.stopPropagation();- if(e.key==='Enter'){e.preventDefault();finish(true);}+ if(e.key==='Enter'){+ // Commit and step down a row, the spreadsheet convention, so a column of+ // values is Enter, type, Enter, type. It does NOT open the next editor:+ // Enter again does that, and the pair reads as "done, next".+ e.preventDefault();+ const fromKey=tr.dataset.key;+ finish(true);+ const rows=visibleRowEls();+ const i=rows.findIndex(t=>t.dataset.key===fromKey);+ if(i>=0)focusRowAt(i+1); // clamped, so the last row commits and stays+ } else if(e.key==='Escape'){e.preventDefault();finish(false);} else if(e.key==='Tab'){ // preventDefault here even inside a native select or a date input, or the
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.18.2",+ "version": "0.19.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.18.2",+ "version": "0.19.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]" }-}+}