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.22.1: Tab stopped escaping into the table
Reported with a screenshot: a row selected, no editor open, press Tab, and the
focus ring lands on a provenance link near the top of the table. The 0.17.0 Tab
cycling only ever engaged from inside an open editor, so with nothing open the key
fell through to the browser's own focus walk, which finds the first tabbable thing
in document order and that is an anchor in row one.
Tab now means the same thing whether or not a field is open: walk this row's
fields. With a row selected and no editor it opens the FIRST editable cell, and
Shift+Tab the LAST, both with the ready-to-type conventions a click gives, and the
cycle carries on exactly as before. Starting the cycle rather than merely
swallowing the key is the spreadsheet answer and makes the keystroke worth
pressing; swallowing it would have fixed the ring and left a dead key.
Only when nothing else holds focus. With the active element on a header control the
user is walking the page's chrome deliberately, so that is left alone; Esc drops the
row focus and hands plain tabbing back. With no row selected Tab is untouched,
because walking to the header controls is normal page tab order and breaking it to
fix a stray ring would trade one accessibility bug for a worse one.
Belt and braces, and this is the part that stops it coming back: the provenance
anchors and the row delete buttons now carry tabindex=-1, so the browser's walk
cannot land inside the table body in ANY state, not just the reported one. Measured
after the change: zero tabbable elements in #tableBody across 63 provenance links
and every delete button. Both stay mouse affordances and still work on click, which
is what they are for here; the keyboard reaches those rows through the row focus and
the delete button's job is covered by the Delete key.
Verified: the reported case prevented with the first cell open and the active
element an input rather than an anchor; Shift+Tab landing on the last editable cell;
the full eight-field in-editor cycle and its wrap unchanged; no-selection Tab
untouched; chrome tabbing untouched; provenance links still anchors with href and
target and still reached by a click. The 55-check keyboard sweep, the 35-step column
battery and the dependency-mode suite all pass unchanged, and the fingerprint moves
only in the version string.
6 files changed
+74−7
README.md+1−1@@ -169,7 +169,7 @@ long editing run. | Shift+D | Dependency mode: click the task that comes first, then the one that waits on it. Stays on so you can chain them. Esc clears the pick, Esc again leaves | | 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 |+| Tab / Shift+Tab | In the table, walk the selected row's fields. With no editor open it opens the first editable cell (Shift+Tab the last); with one open it commits and moves to the next. Wraps across rows and around the ends, and follows the visible order under a sort. With no row selected, Tab walks the page's own controls as usual | | Esc | Cancel the open field, then clear the row focus, then clear the selection | | Ctrl+click | Toggle item in selection | | Shift+click | Range select |
SKILL.md+1−1@@ -208,7 +208,7 @@ rather than a category. - **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+- **Tab / Shift+Tab** walks the selected row's fields. 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 - **Dynamic date range** computed from the data every render
docs/BUILD-NOTES.md+41@@ -3,6 +3,47 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-11 (0.22.1, Tab stopped escaping into the table)++Reported with a screenshot: a row selected, no editor open, press Tab, and the focus+ring lands on a provenance link near the top of the table. The 0.17.0 Tab cycling+only ever engaged from INSIDE an open editor, so with nothing open the key fell+through to the browser's own focus walk, which goes looking for the first tabbable+thing in document order and finds an anchor in row one.++**Tab now means the same thing whether or not a field is open**: walk this row's+fields. With a row selected and no editor, Tab opens the FIRST editable cell and+Shift+Tab opens the LAST, both with the ready-to-type conventions a click gives, and+the cycle carries on from there exactly as before. Starting the cycle rather than+merely swallowing the key is the spreadsheet answer and it makes the keystroke worth+pressing; swallowing it would have fixed the ring and left the user with a dead key.++**Only when nothing else holds focus.** If the active element is a header control+the user is walking the page's chrome deliberately, and that is legitimate keyboard+access, so it is left alone. Verified: with focus parked on the New button, Tab is+not intercepted, no editor opens and the button keeps focus. Esc drops the row focus+and hands plain tabbing back.++**Belt and braces, and this is the part that makes it not come back.** The provenance+anchors and the row delete buttons now carry `tabindex="-1"`, so the browser's walk+cannot land inside the table body in ANY state, not just the one that was reported.+Measured after the change: zero tabbable elements in `#tableBody`, across 63+provenance links and every delete button. Both remain mouse affordances and still+work on click, which is what they are for here; the keyboard reaches those rows+through the row focus, and the delete button's job is covered by the Delete key.++With no row selected, Tab is deliberately left alone: walking to the header controls+is normal page tab order and breaking it to fix a stray ring would have traded one+accessibility bug for a worse one.++Verified: the reported case prevented with the first cell open and the active element+an input rather than an anchor, Shift+Tab landing on the last editable cell, the full+eight-field in-editor cycle and its wrap unchanged, no-selection Tab untouched, chrome+tabbing untouched, provenance links still anchors with their href and target and still+reached by a click. The 55-check keyboard sweep, the 35-step column battery and the+dependency-mode suite all pass unchanged, and the fingerprint moves only in the+version string.+ ## Build notes, 2026-08-11 (0.22.0, dependency mode) Drawing a dependency used to mean opening one bar's editor and picking from a
gantt.html+28−2@@ -1500,6 +1500,29 @@ document.addEventListener('keydown',e=>{ focusRowAt(Math.min(i,visibleRowEls().length-1)); return; }+ // Tab with a row selected but NO editor open used to fall straight through to the+ // browser's own focus walk, which lands on the first tabbable thing inside the+ // table: a provenance link several rows up, ringed and miles from what the user+ // was looking at. Once a row is selected, Tab means "walk this row's fields",+ // which is exactly what it already means once an editor is open. Shift+Tab starts+ // from the last field so the two directions stay symmetric.+ //+ // Only when nothing else holds focus. If the user has tabbed onto a header+ // control they are walking the page's chrome on purpose and that is legitimate+ // keyboard access, so it is left alone; Esc drops the row focus and hands plain+ // tabbing back.+ if(e.key==='Tab'&&!e.altKey&&!e.ctrlKey&&!e.metaKey){+ if(!tableNavReady())return;+ const ae=document.activeElement;+ if(ae&&ae!==document.body&&ae!==document.documentElement)return;+ const tr=focusedRowEl();+ if(!tr)return; // no row selected: the page's tab order is right+ const cells=[...tr.querySelectorAll('td.editable[data-field]')];+ if(!cells.length)return;+ e.preventDefault();+ startCellEdit(e.shiftKey?cells[cells.length-1]:cells[0]);+ 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){@@ -3642,7 +3665,10 @@ function provCell(r){ const p=provOf(r); if(!p)return ''; const tip=esc(p.label+(p.url?', '+p.url:''));- if(p.url) return `<a class="prov-chip" href="${esc(p.url)}" target="_blank" rel="noopener noreferrer" title="${tip}">${esc(p.label)}</a>`;+ // tabindex -1: a provenance link is a mouse affordance in this table, and leaving+ // it in the browser's tab order is what let a stray Tab land three rows up. It+ // stays clickable; the keyboard reaches the row through the row focus instead.+ if(p.url) return `<a class="prov-chip" href="${esc(p.url)}" target="_blank" rel="noopener noreferrer" tabindex="-1" title="${tip}">${esc(p.label)}</a>`; return `<span class="prov-chip" title="${tip}">${esc(p.label)}</span>`; } @@ -3695,7 +3721,7 @@ function renderTable(){ +`<td class="c-deps${(()=>{const d=depsOf(r);return (d.needs.length||d.blocks.length)?'':' empty';})()}">${depCell(r)}</td>` +`<td class="c-notes${ed}${r.description?'':' empty'}" data-field="notes"><span class="trunc">${r.description?esc(r.description):'none'}</span></td>` +`<td class="c-prov${ed}" data-field="prov">${provCell(r)}</td>`- +`<td class="c-del">${editable?`<button class="row-del" type="button" title="Delete this row from the plan">×</button>`:''}</td>`+ +`<td class="c-del">${editable?`<button class="row-del" type="button" tabindex="-1" title="Delete this row from the plan">×</button>`:''}</td>` +'</tr>'; }).join(''); markFocusedRow();
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.22.0",+ "version": "0.22.1", "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.0",+ "version": "0.22.1", "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]" }-}+}