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.47.0: compact 256px editor panels, a glowing Deps button instead of the info bar, and creating a row leaves dependency mode
6 files changed
+285−40
README.md+2−2@@ -42,7 +42,7 @@ Interactive Gantt chart viewer for project timelines. Data-driven, dark by defau **Rename a row in place.** Click the name text in the label column to edit it. Enter commits the new name, closes the field and leaves the row selected, so the rename hands straight over to the keyboard. Escape cancels. -**The task editor remembers where you put it.** The panel that opens on a bar is square and draggable by the strip at its top. Drag it once and every panel after it opens in that same spot on screen, whatever row it belongs to and however far the chart has been scrolled, until the page is reloaded. Nothing is saved to the plan; it is a property of the sitting, and a refresh restores the automatic placement beside the bar.+**The task editor remembers where you put it.** The panel that opens on a bar is a compact 256px square, draggable by the strip at its top. It is meant to be glanced at rather than filled in: the row's name across the top, the two dates, the note, the dependency list (the one part that flexes, and the one part that scrolls) and the buttons, at desktop density, so it sits over as little of the chart it is editing as it can. The full **Edit item** panel, with the name, owner, status and provenance fields, is the one you fill in and it is unchanged. Drag it once and every panel after it opens in that same spot on screen, whatever row it belongs to and however far the chart has been scrolled, until the page is reloaded. Nothing is saved to the plan; it is a property of the sitting, and a refresh restores the automatic placement beside the bar. **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. @@ -237,7 +237,7 @@ long editing run. | 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 | | Delete | Delete the selected row, undoable. Only when a row is selected and no field is open, so Delete inside an editor just edits text. Backspace is deliberately not bound |-| Shift+D | Dependency mode: click the task that comes first, then the one that waits on it. The row under the pointer lights up across both panes, tinted in its own phase colour, so you can see which task you are about to click. The armed first task wears the normal selection outline. Clicking a pair that already has a dependency removes it. Click an arrow to pick it, then Delete twice to remove that one. Stays on so you can chain them. Esc lets go of a picked arrow, then the pick, then leaves |+| Shift+D | Dependency mode: click the task that comes first, then the one that waits on it. The **Deps** button glows in the accent for as long as the mode is on, and its tooltip is the instruction. The row under the pointer lights up across both panes, tinted in its own phase colour, so you can see which task you are about to click. The armed first task wears the normal selection outline. Clicking a pair that already has a dependency removes it. Click an arrow to pick it, then Delete twice to remove that one. Stays on so you can chain them. Esc lets go of a picked arrow, then the pick, then 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, and a lane that is collapsed OPENS the moment the moving row lands in it, so you can see what you are moving among. Under a column sort the row stays inside the run of rows the sort considers tied with it. The pane follows the row it is moving, keeping five rows visible ahead of it in the direction of travel, and both columns stay locked. With several rows selected the whole set moves as a group; if the selection has gaps in it, the FIRST press closes it up into one block at the topmost selected row and every press after that moves the block | | 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. In the edit panels it is one keypress per field the same way, including the date fields, which Tab leaves whole instead of walking their day, month and year segments |
SKILL.md+2−2@@ -221,7 +221,7 @@ rather than a category. - **Auto-schedule** button, with the plan-level controls next to Fit, in the chart and the table lenses. One press lays the WHOLE plan out under two rules at once: every task with dependencies starts BACK TO BACK with its latest predecessor, on that predecessor's end date, and no task starts or ends on a Saturday, a Sunday or a US federal holiday. Back to back is a statement about the CHART: a bar runs from its start to its end and the end is the boundary it stops at, so a successor starting on that date draws flush against it with no gap and no overlap. Tasks move EARLIER as well as later, so slack is pulled in and a conflict is pushed out. A task with no dependencies is an ANCHOR and keeps its start (shifted off a weekend or a holiday if it was on one), because that is the one thing in a plan this cannot derive. Duration is preserved in WORKING days, not calendar days, so a five-day task laid across a weekend, or across Labor Day, still takes five working days: a predecessor ending on the Friday hands off on the Friday and the weekend falls inside the successor's own span, never between the two. Milestones take the same rule with no duration: off a weekend or a holiday to the next working day, and on the day their last dependency ends. The graph crosses sections freely and so does the schedule; a section's displayed range just follows its rows. A dependency CYCLE is left exactly as it is, along with anything downstream of it, and the toast names the rows: there is no honest answer for a row that waits on itself. The whole pass is ONE undo entry and ONE changelog event (`action: "auto-schedule"`, with a count and the names of what moved), and a press that moves nothing writes nothing. Deterministic. Disabled, with a tooltip that says why, when the plan has no dependencies - **Critical** button, next to it: a toggle that lights up the chain of work with no slack in it, the one that decides the finish date. Classic CPM forward and backward over BUSINESS days, on the dates the plan has right now, so it is independent of Auto-schedule and never moves anything. It uses the SAME finish-to-start lag Auto-schedule schedules with (a successor's earliest start is its predecessor's early finish, not the day after), so the two buttons can never disagree about the same plan: auto-schedule it and the chain that was just laid out tight reads as zero float. Total float of zero is critical; only rows in the dependency graph can be. The bars, the arrows along the path and any milestone on it glow white and breathe on a 2.6s cycle, in a saturated indigo in light mode and on paper, and the table lens marks the same rows on their left edge. Purely additive: selection outlines, dependency-mode amber and armed red all still read on a glowing row, and hit-testing is untouched. It recomputes live as dates and dependencies change. Disabled with the same reasoning when the plan has no dependencies - **Click legend** to filter phases, milestones, dependency arrows, or the today line-- **Click a bar** to open the date editor with start/end pickers and dependency management+- **Click a bar** to open the date editor with start/end pickers and dependency management. It is a compact 256px square (`--ed-sq`, one token for both dimensions): the row's name across the top, the two dates, the note, the dependency list and the buttons, at desktop density, so it covers as little of the chart as it can. The dependency list is the only part that flexes and the only part that scrolls; the buttons are pinned to the bottom edge. The milestone panel and the batch panel take the same compact metrics; the full **Edit item** panel, which is filled in rather than glanced at, keeps the full ones - **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 - **Sign in with Adom is required** for every page load and every change. Unauthenticated visitors get a sign-in page and no plan data. Sessions are an HttpOnly cookie, 30 days, and survive a restart. `--no-auth` turns it off for single-user local use - **Direct-loopback GETs of `/data`, `/state`, `/changelog` and `/export/*` need no session**, so an AI agent's read loop keeps working; everything that changes the plan needs a signed-in person from any origin. A request is "direct loopback" only if it comes from 127.0.0.1 with no `x-forwarded-*`/`cf-*` headers and a loopback `Host`, because the workspace proxy also arrives from 127.0.0.1@@ -229,7 +229,7 @@ rather than a category. - **New items default their provenance to "Added by <your display name>"**, still an ordinary editable field - **Delete** removes the selected table row (undoable, redoable). Only when a row is selected and no editor, panel, search box or drag is live, so Delete inside a field only edits text. Backspace is not bound. Inert in the chart - **Saving is whole-state and last-write-wins.** Two people editing at once WILL overwrite each other. Each save carries the revision it was based on and a stale save is refused with 409, at which point that tab stops autosaving and offers a reload rather than clobbering the other person-- **Dependency mode** (the Deps button with the chart controls, or **Shift+D**): click the task that comes FIRST, then the task that waits on it, and the dependency is drawn. **The row under the pointer lights up**, name cell and chart band together, washed in that row's own phase colour, so a 20px bar and the name a pane away read as one row and you can see which task you are about to click. One row at a time, and it goes the instant the mode does. The armed first task wears the normal selection outline so you can see what the pair is waiting on. The pair resets and the mode stays on, so a run of dependencies is one sitting. Clicking a pair that ALREADY has a dependency REMOVES it, which is the same removal as the editor's ×: one undo entry, one changelog event. Refuses anything that would make a cycle, naming the shortest loop path. Milestones are not endpoints: a dep is stored as a task id and a milestone in the data file has none. **Clicking an arrow picks it**, in this mode only, and **Delete** twice removes the dependency it stands for (the first press arms and says so, the second commits; any other key, a click or Esc withdraws the question). Outside the mode the arrows take no clicks at all. Esc lets go of a picked arrow, then a half-made pick, then leaves the mode. While the mode is on, bar clicks mean dependencies: no editor, no drag, no reorder. Ctrl+Z undoes the last add or removal without leaving the mode+- **Dependency mode** (the Deps button with the chart controls, or **Shift+D**): click the task that comes FIRST, then the task that waits on it, and the dependency is drawn. **The Deps button glows** in the accent, breathing on the same 2.6s the critical path does, for exactly as long as the mode is on, and its tooltip carries the instruction in and out of the mode: "Adding dependencies. Click the task that comes first, then the one that waits on it. Click an arrow to pick it. Esc leaves the mode." There is no info bar under the legend any more, so entering and leaving the mode moves nothing on the page. **Creating a row leaves the mode first**: Shift+N and the New button both exit it (armed source dropped, wash cleared, picked arrow released, glow off) and then open the create panel, so you never end up typing a name over a chart that is still armed for a pairing. **The row under the pointer lights up**, name cell and chart band together, washed in that row's own phase colour, so a 20px bar and the name a pane away read as one row and you can see which task you are about to click. One row at a time, and it goes the instant the mode does. The armed first task wears the normal selection outline so you can see what the pair is waiting on. The pair resets and the mode stays on, so a run of dependencies is one sitting. Clicking a pair that ALREADY has a dependency REMOVES it, which is the same removal as the editor's ×: one undo entry, one changelog event. Refuses anything that would make a cycle, naming the shortest loop path. Milestones are not endpoints: a dep is stored as a task id and a milestone in the data file has none. **Clicking an arrow picks it**, in this mode only, and **Delete** twice removes the dependency it stands for (the first press arms and says so, the second commits; any other key, a click or Esc withdraws the question). Outside the mode the arrows take no clicks at all. Esc lets go of a picked arrow, then a half-made pick, then leaves the mode. While the mode is on, bar clicks mean dependencies: no editor, no drag, no reorder. Ctrl+Z undoes the last add or removal without leaving the mode - **Copy IDs**: with rows selected, the table's tools row and the chart's selection chip (and the batch panel) copy the selected items' stable ids, one per line as `id<TAB>name`, for pasting into an AI chat to request batch edits. Ctrl+C does the same when nothing is focused and no text is selected. If the clipboard is refused the ids appear in a selectable box rather than failing silently - **Row multi-select in the table**: ctrl-click toggles a row, shift-click takes the range from the anchor following the VISIBLE order under sorts and filters. It is the SAME selection as the chart's, so rows picked in one lens are selected in the other. Modified clicks never open a cell editor and never start a native text selection; unmodified click-drag still selects cell text. Alt+Up/Down moves the whole set (plan order only; refused under a sort), and Delete removes the whole set as one undo. A selection with GAPS in it groups itself up on the first Alt+Arrow press rather than moving: the selected rows close up into one contiguous block at the topmost one's slot - **Arrows steer clear of the bars.** A dependency leaves the source bar's END sideways, drops, and arrives at the next bar's START sideways again, with rounded elbows, so which end is the cause is always readable. It routes AROUND the bars in between rather than through them: the drop moves to the nearest clear channel, and a long or backward route runs along the empty lane between two rows rather than across a task that has nothing to do with it. Where a bar lies clean across the only gap, the arrow goes round the end of it. Nothing to configure; it is re-decided on every redraw, including live during a drag
docs/BUILD-NOTES.md+137@@ -3,6 +3,143 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-20 (0.47.0, compact panels and a glowing Deps button)++Three changes from Drew, all of them client-side. `gantt.html` only. `server.js` did not+move: nothing about the sidecar, the changelog or the save protocol changed. **No live+server was restarted and no live plan was opened at any point.** Every suite runs on+synthetic fixtures on its own port.++### 1. Creating a row leaves dependency mode first++Dependency mode holds the chart. A click on a bar is half of a link, the row under the+pointer wears a wash, and an armed source is outlined waiting for its pair. Opening the+create panel on top of all that left the user typing a name into a panel while the chart+behind it was still armed for something else, and their first click after Create landed+as half a dependency they had not asked for.++So creating now LEAVES the mode, by the same door Escape uses, and then creates. One+line, in one place:++```js+function openCreateEditor(){+ if(depMode)setDepMode(false);+ closeEditor();+```++`openCreateEditor` is the only create path in the app: Shift+N calls it and the New+button calls it, and `setDepMode(false)` is the same exit Escape's last rung takes, so+the armed source, the hover wash, any picked arrow and the arming inside it, the+`body.dep-mode` class and the button's own state all go together and none of them can+be forgotten separately. Nothing is refused and nothing is deferred: the mode goes, the+panel opens, the row is created.++Creating while NOT in the mode is untouched, which is asserted rather than assumed: the+suite creates a row on a page that never entered the mode and checks the button, the+glow and the mode flag are all exactly as they were.++### 2. The info bar is gone; the button glows and carries the sentence++Entering the mode used to put a line of prose in a `div.dep-caption` between the legend+and the chart. Two things were wrong with it. It was in the WRONG PLACE: the question+"what does this button do" is asked at the button, and the answer was a pane away. And+it MOVED THE CHART, because the div was in normal flow, empty out of the mode and a line+of text in it, so entering the mode pushed every row down and leaving pulled them back.++The div, its one stylesheet reference and `syncDepCaption`'s markup are all gone. In+their place:++- **The tooltip.** The bar's sentence, verbatim, on `#btnDeps`, present in and out of the+ mode because it explains what the button does as well as what it is doing:+ "Adding dependencies. Click the task that comes first, then the one that waits on it.+ Click an arrow to pick it. Esc leaves the mode." The old tooltip's `Shift+D` is not+ repeated; that lives on the keyboard-shortcuts pulldown with every other shortcut.+- **The glow.** `#btnDeps.on` was already being set and no rule had ever read it, so+ until now the button did not change at all when the mode it armed came on. It takes+ the accent for its text and its border and a two-band breathing box-shadow on the same+ 2.6s `ease-in-out infinite` the critical path uses, from a new `--dep-glow` /+ `--dep-halo` token pair tuned per theme (softer on paper, which has less room for a+ bloom). `prefers-reduced-motion` holds it at the bright end of the same breath, the way+ every other live state in this app does.++It is deliberately NOT the `.on` treatment `.plan-ctl` gives Critical, which swaps the+fill: a glow sits on top of the button's own hover without either one hiding the other.++The two CONTEXTUAL lines the bar also carried ("Now click the task that waits on X",+"Arrow picked: X needs Y") went with it. Both were restatements of marks that are already+on the chart, next to the thing being picked: the armed source wears `dep-pending` on its+own bar and the picked arrow wears the amber `.sel`. The `flash` line that names a+dependency the moment it is made is unchanged.++Layout: the div was unstyled, so out of the mode it was a zero-height block and the chart+never moved for its removal. That is measured rather than reasoned, twice: `v47.mjs`+records the chart's top before, during and after a mode toggle in both themes, and+`ab47.sh` records every row's box under 0.46.0 and under 0.47.0 and diffs the two.++### 3. The compact panels, 370px square down to 256px++`--ed-sq` goes 370 -> 256 (69.2% of the old side) and a new `.editor.compact` block takes+the type down a step and the gaps down about a third.++The panel is GLANCED at: the row's name, two dates, a note, the links, three buttons.+At 370 it was the size of a dialog and sat over about a quarter of the chart it was+editing, which is the one thing the reader needs to keep seeing. Nothing was removed to+get it smaller; it is the same object at desktop density.++Measured, at 256: fixed chrome 225px, dependency list 31px, which is one row of the+smaller list showing with the next one starting under it, so a task with more links than+that says so and scrolls. The 0.40.0 structure is intact: title header, fields, flexing+scroll region, pinned button row.++Floors, chosen and asserted rather than left to fall out: **24px on every input, select+and button**, which is what keeps a date field a target rather than a hairline, and+9.5px on the labels, which is the smallest type already in this app (the dropdown's+`dd-label`). `v47.mjs` fails if anything in a compact panel measures under 24px tall.++**Which panels.** `showEditor`'s third argument stopped being a boolean and became a+shape name, because there are three shapes now and not two:++| shape | panel | what it gets |+|---|---|---|+| `'square'` | the task editor | `.square` + `.compact`: fixed 256 side, both dimensions from the one token |+| `'compact'` | the milestone panel, the batch panel | `.compact` only: the dense metrics, its own height |+| anything else | the full item modal, the create card | neither |++The full **Edit item** panel and the **New item** card carry a name, an owner, a status+and a provenance field. They are FILLED IN rather than glanced at, and 0.47.0 leaves+their metrics alone by construction: every compact rule is scoped to `.editor.compact`+and never to `.editor`. `closeEditor` drops `compact` with `square`, so no panel can+inherit the last one's metrics.++The milestone panel takes `min-width: var(--ed-sq)`, so it is the compact panel's width+and its own (shorter) height, which is what makes the two read as one family.++### What the suites say++| suite | result |+|---|---|+| v37 / regress / abc | 203 / 26 / 75 pass |+| v38 / v39 / v40 | 249 / 176 / 138 pass |+| v41 / v41big / v42 / v42big / v43 | 75 / 23 / 103 / 33 / 41 pass |+| v44a / v44b / v44c | 54 / 48 / 19 pass |+| hol-unit + crosscheck | 66 pass, three identical |+| v45 / v46 | 107 / 160 pass |+| **v47 (new)** | **116 pass** |+| legacy injected | five OK, mssel + msphase A/B byte-identical |++**Assertions deliberately changed**, five files, each because the thing it read no longer+exists rather than because it started failing:++| where | was | now |+|---|---|---|+| `gw/abc.mjs` | `square is big enough` floor `>=320` | `>=240`. The real "big enough" test is two lines below it and unchanged: the panel must not scroll itself and Apply must be inside the box |+| `g39/v39.mjs` | caption names the armed task | the armed task is named on its own bar (`.bar.dep-pending` `data-title`), and the mode is said by the button's glow |+| `g39/v39.mjs` | caption says what to do with a picked arrow | the button carries the sentence, checked verbatim |+| `g40/v40.mjs` | `--ed-sq === 370`, list `=== 60` | `=== 256`, list `=== 31` |+| `g40/v40.mjs` | caption names the armed task / the picked arrow | the pending bar, and the button's glow |+| `g41/v41.mjs` | caption names the picked dependency | the button's glow, while the pick (already asserted off the arrow) stands |+| `w/deps.js` | `#depCaption` visible and its text | `#btnDeps` box-shadow and title, plus a new `noInfoBar` readout asserting the element is gone |+ ## Build notes, 2026-08-20 (0.46.0, the row under the pointer in dependency mode) One change from Drew, and the whole of it is visual. `gantt.html` only. `server.js` did
gantt.html+141−33@@ -67,6 +67,13 @@ (a breathing halo around the same elements) and swaps the band, to a saturated indigo that clears 4.5:1 against the page and is not in the phase ramp. */ --crit: #ffffff; --crit-glow: rgba(255,255,255,0.85); --crit-halo: rgba(255,255,255,0.35);+ /* Dependency mode, said on the button that arms it. Same two-band shape as the+ critical path (a tight ring plus a soft bloom, breathing), in the ACCENT rather+ than in white, because the mode is one of this app's own controls being held down+ and not a reading of the plan. Softer than the path on purpose: the path is the+ answer to a question, the mode is a state you are working in and has to sit next+ to a header full of other buttons for as long as you are in it. */+ --dep-glow: rgba(0,184,177,0.55); --dep-halo: rgba(0,184,177,0.22); --on-accent: #05221f; --grid-line: rgba(48,54,61,0.18); --weekend: rgba(255,255,255,0.022);@@ -90,13 +97,23 @@ height in CSS and for the placement maths in JS, so the two can never disagree about how big the panel is when it clamps itself to the viewport. - 344 in 0.36.0, 370 since 0.40.0. The header that names the row costs the panel 26px- of height (an 18px line, less its 2px lift, plus its 10px gap), and the dependency- list is the only part that flexes, so at 344 the header would have been paid for out- of the list: measured, it went from 60px to 34px, half a row shorter than 0.36.0- sized it. The side grows by the same 26 instead, which gives the list back exactly- what the header took AND keeps the panel square, since one token is both dimensions. */- --ed-sq: 370px;+ 344 in 0.36.0, 370 in 0.40.0, 256 since 0.47.0.++ 0.40.0's 370 was 344 plus the 26px the header that names the row cost the panel: the+ dependency list is the only part that flexes, so paying for the header out of the+ list would have taken it from 60px to 34px, half a row shorter than 0.36.0 sized it,+ and growing the side instead gave the list back exactly what the header took.++ 0.47.0 shrinks the whole thing. This panel is GLANCED at (two dates, a note, the+ links, three buttons) and it sits on top of the chart it is editing, which is the one+ thing the reader needs to keep seeing; at 370 it was the size of a dialog and covered+ a quarter of the pane. The .editor.compact block in the stylesheet is the other half+ of the change: type down a step, gaps down about a third, floors of 24px on every+ control so a date stays a target. Measured at 256, the fixed chrome costs 225 and the+ list is left 31px, which is one row of the smaller list showing with the next one+ starting under it, so a task with more links than that says so and scrolls. Nothing+ was removed to get here: the panel is the same object at desktop density. */+ --ed-sq: 256px; color-scheme: dark; } :root[data-theme="light"] {@@ -110,6 +127,10 @@ 5:1 against the page. */ --sig: #b45309; --sig-edge: #ffffff; --crit: #4c1d95; --crit-glow: rgba(76,29,149,0.95); --crit-halo: rgba(76,29,149,0.40);+ /* The same bloom on paper, in the light theme's deeper accent. A glow on white has+ less room than one on near-black, so both bands come down rather than up: at the+ dark theme's alphas the bloom would read as a smudge around the button. */+ --dep-glow: rgba(0,133,127,0.42); --dep-halo: rgba(0,133,127,0.18); --on-accent: #ffffff; --grid-line: rgba(31,35,40,0.10); --weekend: rgba(31,35,40,0.04);@@ -134,6 +155,7 @@ --ph1:#00a49d; --ph2:#8c6bf7; --ph3:#3b95ff; --sig:#b45309; --sig-edge:#ffffff; --crit:#4c1d95; --crit-glow:rgba(76,29,149,0.95); --crit-halo:rgba(76,29,149,0.40);+ --dep-glow:rgba(0,133,127,0.42); --dep-halo:rgba(0,133,127,0.18); color-scheme: light; } html, body { overflow: visible !important; height: auto !important; } .labels-col, .timeline-col { overflow: visible !important; }@@ -201,6 +223,26 @@ header .actions { display: flex; align-items: center; gap: 4px; margin-left: aut header .act-btn { background: none; border: 1px solid var(--border); border-radius: 6px; color: var(--text2); font-family: 'Satoshi', sans-serif; font-size: 11px; padding: 4px 10px; cursor: pointer; transition: background 0.15s, color 0.15s; display: flex; align-items: center; gap: 4px; } header .act-btn:hover { background: rgba(255,255,255,0.04); color: var(--text); } header .act-btn svg { width: 14px; height: 14px; }+/* ── Dependency mode is ON ──+ Until 0.47.0 the mode announced itself with a sentence in a bar under the legend and+ the button that armed it did not change at all: the .on class was set on it and no+ rule read it. The bar is gone (its sentence is the button's tooltip now, where the+ question "what does this do" is actually asked), and the button is where the state+ lives, which is also where the click that leaves the mode is.+ Same two-band breath the critical path uses, on the same 2.6s, so the two live+ states in this app move alike; the accent rather than the path's white, and softer,+ because this one sits in the header for as long as the user is adding links. It is+ NOT the .on treatment .plan-ctl gives Critical (a filled swap to --crit): a glow can+ sit on top of the button's own hover without either one hiding the other. */+header .act-btn.on#btnDeps { color: var(--accent); border-color: var(--accent); background: var(--elevated); animation: depOn 2.6s ease-in-out infinite; }+@keyframes depOn {+ 0%, 100% { box-shadow: 0 0 0 1px var(--dep-halo), 0 0 5px 0 var(--dep-halo); }+ 50% { box-shadow: 0 0 0 1px var(--dep-glow), 0 0 12px 2px var(--dep-glow); }+}+/* Held at the bright end of the same breath, the way every other live state here is. */+@media (prefers-reduced-motion: reduce) {+ header .act-btn.on#btnDeps { animation: none; box-shadow: 0 0 0 1px var(--dep-glow), 0 0 10px 1px var(--dep-glow); }+} /* Their labels change length ("Lines"/"Diamonds", "Light"/"Dark"). .actions is right-aligned, so a width change there drags everything left of it, including the Fit button. Pin them to their widest label. */@@ -539,6 +581,49 @@ body.dep-mode .bar-row.dep-hover { background: rgb(var(--dep-hl-rgb) / var(--dep .editor.square .de-dep-section .dep-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; } .editor.square .de-dep-section > select { flex: none; } .editor.square .de-actions { flex: none; margin-top: 10px; }+/* ── The compact metrics, 0.47.0 ──+ The square panel and the milestone panel are GLANCED at: two dates, a note, a list of+ links, three buttons. At 370 they were the size of a dialog and sat over a quarter of+ the chart they are editing, which is the one thing the reader needs to keep seeing.+ Everything below is the same panel at desktop density: the type comes down a step,+ every gap comes down about a third, and nothing is removed, so the panel reads as the+ same object shrunk rather than as a different one.++ Scoped to .compact and never to .editor, because the full item modal and the create+ card render into this same element and are NOT compact: they carry a name, an owner, a+ status and a provenance field and are filled in rather than glanced at.++ Floors, deliberately: 24px on every date, number and text input and on every button,+ which is what keeps a date field a target rather than a hairline, and 9.5px on the+ labels, which is the smallest type already in this app (the dropdown's dd-label). */+.editor.compact { padding: 0 10px 8px; min-width: var(--ed-sq); }+.editor.compact .drag-bar { height: 16px; margin: 0 -10px 4px; padding: 0 10px; }+.editor.compact .drag-bar::after { width: 24px; height: 2px; }+.editor.compact .de-title { font-size: 11px; margin: -1px 0 6px; }+.editor.compact label { font-size: 9.5px; line-height: 1.3; margin-bottom: 3px; }+.editor.compact input, .editor.compact textarea, .editor.compact select,+.editor.compact .de-note-row input { font-size: 11px; padding: 3px 6px; border-radius: 5px; min-height: 24px; }+.editor.compact input[type="date"], .editor.compact input[type="number"] { width: 104px; }+.editor.compact .de-row { gap: 7px; margin-bottom: 4px; }+.editor.compact .de-note-row { margin-top: 5px; }+.editor.compact .de-hint { font-size: 9px; }+.editor.compact .de-dep-section { margin-top: 5px; padding-top: 5px; }+.editor.compact .de-actions { margin-top: 5px; gap: 6px; }+.editor.compact .de-btn { padding: 4px 10px; font-size: 11px; border-radius: 5px; min-height: 24px; }+.editor.compact .dep-list { margin-top: 3px; }+.editor.compact .dep-list li { font-size: 10px; padding: 2px 0; }+.editor.compact .dep-list .dep-rm { font-size: 12px; }+.editor.compact .batch-title { font-size: 11px; margin-bottom: 6px; }+.editor.compact .batch-hint { font-size: 10px; margin-bottom: 6px; }+.editor.compact .batch-row { gap: 6px; margin-bottom: 5px; }+.editor.compact .batch-row label { min-width: 54px; }+.editor.compact .batch-row input[type="number"] { width: 58px; }+.editor.compact .batch-row span { font-size: 10px; }+/* The square sets its own margin on this row a few rules up, at the same specificity as+ the compact one, so which of the two wins would be a question of source order. Said+ once here at a specificity neither can reach, so it cannot become one. The pinning+ itself is the flex:none up there and is untouched. */+.editor.square.compact .de-actions { margin-top: 5px; } /* While the panel is being dragged nothing on the page should be selecting text: the pointer is moving a window, not sweeping a paragraph. Scoped to the drag so the inputs inside the panel keep normal selection the rest of the time. */@@ -735,7 +820,6 @@ body.row-dragging .data-table tbody tr:hover { background: none; } :root[data-view="analytics"] .analytics-view { display: flex; } :root[data-view="analytics"] .chart-wrap, :root[data-view="analytics"] .legend,-:root[data-view="analytics"] .dep-caption, :root[data-view="analytics"] header .start-ctl, :root[data-view="analytics"] #btnMsStyle, :root[data-view="analytics"] #btnCrit,@@ -1042,7 +1126,11 @@ table.data-table { font-size: calc(12px * var(--fs)); min-width: calc(900px * va <div class="app"> <header id="hdr"></header> <div class="legend" id="legend"></div>-<div class="dep-caption" id="depCaption"></div>+ <!-- The dependency-mode info bar used to sit here, between the legend and the chart.+ It was an unstyled block that was empty out of the mode and a sentence in it, so+ entering the mode pushed the whole chart down by a line and leaving it pulled the+ chart back up. Since 0.47.0 the sentence is the Deps button's tooltip and the+ state is the button's glow, so nothing between the legend and the chart moves. --> <div class="chart-wrap"> <div class="labels-col" id="labels"></div> <div class="col-resizer" id="colResizer" title="Drag to resize the name column. Double-click to fit the longest name."></div>@@ -1656,7 +1744,11 @@ $('hdr').innerHTML = ` </select> <button class="act-btn" id="btnFit" title="Fit the whole plan into the pane: the finest cell size that shows every date without scrolling sideways"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5v14M21 5v14"/><path d="M7 12h10"/><path d="M10 9l-3 3 3 3M14 9l3 3-3 3"/></svg>Fit</button>- <button class="act-btn" id="btnDeps" title="Add dependencies · Shift+D. Click the task that comes first, then the one that waits on it. Esc clears the pick, Esc again leaves the mode">+ <!-- The tooltip IS the info bar, moved. Same sentence, on the control it describes,+ so it is there before the mode is entered (what does this button do) as well as+ during it (what am I meant to click now). Shift+D is not repeated here; it is on+ the keyboard-shortcuts pulldown with every other one. -->+ <button class="act-btn" id="btnDeps" title="Adding dependencies. Click the task that comes first, then the one that waits on it. Click an arrow to pick it. Esc leaves the mode."> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5" cy="6" r="2"/><circle cx="19" cy="18" r="2"/><path d="M7 6h6a3 3 0 0 1 3 3v7"/><path d="M14 14l2 3 2-3"/></svg><span class="btn-label">Deps</span></button> </div> <div class="plan-ctl">@@ -4771,7 +4863,7 @@ function openMilestoneEditor(uid){ <button class="de-btn secondary" id="meClose">Cancel</button> <button class="de-btn danger" id="meDelete" title="Delete this milestone from the plan. Undoable with Ctrl+Z">Delete</button></div>`; const x=r._msHit?r._msHit.getBoundingClientRect():{left:window.innerWidth/2,bottom:200};- showEditor(x.left-150,x.bottom+8,false);+ showEditor(x.left-150,x.bottom+8,'compact'); $('meApply').addEventListener('click',()=>{ snapshotMs(r); const nd=$('meDate').value;@@ -4792,7 +4884,7 @@ function openSingleEditor(id,anchor){ const r=items.find(d=>d.id===id);if(!r)return; edSubject=r; const allTasks=items.filter(d=>d.id&&d.id!==id),deps=r.deps||[];- editorEl.innerHTML=`<div class="drag-bar"></div><div class="de-title" id="deTitle"></div><div class="de-row"><div><label>start</label><input type="date" id="deStart" value="${r.start}"></div><div><label>end</label><input type="date" id="deEnd" value="${r.end}"></div></div><div class="de-note-row"><label>note <span class="de-hint">shown on the bar</span></label><input type="text" id="deNote" maxlength="24" value="${esc(r.note||'')}" placeholder="e.g. 6m"></div><div class="de-dep-section"><label>dependencies</label><ul class="dep-list" id="deDepList"></ul><select id="deAddDep" style="margin-top:6px;"><option value="">+ add dependency...</option>${allTasks.filter(t=>!deps.includes(t.id)).map(t=>`<option value="${esc(t.id)}">${esc(t.name)}</option>`).join('')}</select></div><div class="de-actions"><button class="de-btn primary" id="deApply">Apply</button><button class="de-btn secondary" id="deClose">Cancel</button><button class="de-btn danger" id="deDelete" title="Delete this task from the plan. Undoable with Ctrl+Z">Delete</button></div>`;+ editorEl.innerHTML=`<div class="drag-bar"></div><div class="de-title" id="deTitle"></div><div class="de-row"><div><label>start</label><input type="date" id="deStart" value="${r.start}"></div><div><label>end</label><input type="date" id="deEnd" value="${r.end}"></div></div><div class="de-note-row"><label>note <span class="de-hint">shown on the bar</span></label><input type="text" id="deNote" maxlength="24" value="${esc(r.note||'')}" placeholder="e.g. 6m"></div><div class="de-dep-section"><label>dependencies</label><ul class="dep-list" id="deDepList"></ul><select id="deAddDep" style="margin-top:4px;"><option value="">+ add dependency...</option>${allTasks.filter(t=>!deps.includes(t.id)).map(t=>`<option value="${esc(t.id)}">${esc(t.name)}</option>`).join('')}</select></div><div class="de-actions"><button class="de-btn primary" id="deApply">Apply</button><button class="de-btn secondary" id="deClose">Cancel</button><button class="de-btn danger" id="deDelete" title="Delete this task from the plan. Undoable with Ctrl+Z">Delete</button></div>`; setEdTitle(r); renderDepList(id); // Anchored to the bar in the chart. In the table the chart is display:none and@@ -4804,7 +4896,7 @@ function openSingleEditor(id,anchor){ const sq=edSquareSide(); let edX=rect.right+8; if(edX+sq>window.innerWidth) edX=rect.left-sq-8;- showEditor(edX,rect.top,true);+ showEditor(edX,rect.top,'square'); $('deAddDep').addEventListener('change',function(){if(this.value){snapshot(id);if(!r.deps)r.deps=[];r.deps.push(this.value);renderDepList(id);drawDeps();scheduleSave('editor');const o=this.querySelector(`option[value="${r.deps[r.deps.length-1]}"]`);if(o)o.remove();this.value='';}}); $('deApply').addEventListener('click',()=>{snapshot(id);const ns=$('deStart').value,ne=$('deEnd').value;if(ns)r.start=ns;if(ne)r.end=ne;r.note=$('deNote').value;updateBar(id);closeEditor();scheduleSave('editor');}); $('deClose').addEventListener('click',closeEditor);@@ -4858,7 +4950,7 @@ function openBatchEditor(){ <div class="batch-row"><label>shift end</label><input type="number" id="bShiftE" value="0" step="1"> <span>days</span></div> </div> <div class="de-actions"><button class="de-btn primary" id="bApply">Apply</button><button class="de-btn secondary" id="bClose">Cancel</button></div>`;- showEditor(window.innerWidth/2-155,window.innerHeight/2-130,false);+ showEditor(window.innerWidth/2-155,window.innerHeight/2-130,'compact'); $('bApply').addEventListener('click',()=>{ const newStart=$('bStart').value, newEnd=$('bEnd').value;@@ -4910,10 +5002,10 @@ function refreshMsChrome(r){ // description restored from the sidecar (or edited in the table) left the bar // hovering the version that was in the data file. function updateBar(id){const r=items.find(d=>d.id===id),be=barEls[id];if(!r||!be)return;if(r._noteEl)r._noteEl.textContent=r.note||'';const x1=toX(r.start),x2=toX(r.end);be.bar.style.left=x1+'px';be.bar.style.width=Math.max(x2-x1,6)+'px';be.bar.style.background=phaseColor(r.phase);be.bar.dataset.dates=fmt(r.start)+' → '+fmt(r.end);be.bar.dataset.title=r.name;be.bar.dataset.body=r.description||'';be.bar.dataset.prov=provLabel(r);syncEditorDates(r);drawDeps();}-// The shape goes with the content: every opener declares whether it wants the square,-// so closing has to leave the element without one rather than let the last panel's-// aspect ride along into the next one.-function closeEditor(){edSubject=null;editorEl.classList.remove('show','square');editorEl.innerHTML='';}+// The shape goes with the content: every opener declares which one it wants, so closing+// has to leave the element without any of them rather than let the last panel's aspect+// or its metrics ride along into the next one.+function closeEditor(){edSubject=null;editorEl.classList.remove('show','square','compact');editorEl.innerHTML='';} function closeMenus(){exportMenu.classList.remove('show');importMenu.classList.remove('show');} // ── Drag (edge-resize, body-move horizontal, reorder vertical) ──@@ -5221,7 +5313,6 @@ function setDepPending(id){ if(depPending){const b=barEls[depPending];if(b)b.bar.classList.remove('dep-pending');} depPending=id||null; if(depPending){const b=barEls[depPending];if(b)b.bar.classList.add('dep-pending');}- syncDepCaption(); } // ── Picking a dependency arrow ── //@@ -5268,25 +5359,23 @@ function setDepSel(sel){ depSel=sel||null; disarmDepDelete(); paintDepSel();- syncDepCaption(); }-function syncDepCaption(){- const cap=$('depCaption'); if(!cap)return;+// Everything the mode says OUTSIDE the chart. Until 0.47.0 that was a sentence in a bar+// under the legend, rewritten on every pick; the sentence is the Deps button's tooltip+// now and the state is the button's glow, so this reads depMode and nothing else. That+// is why setDepSel and setDepPending no longer call it: neither of them can change what+// it paints, and the marks they DO paint (the amber arrow, the pending bar) are on the+// chart, next to the thing being picked, where they always were.+function syncDepMode(){ document.documentElement.setAttribute('data-dep',depMode?'on':'off'); const btn=$('btnDeps'); if(btn)btn.classList.toggle('on',depMode);- if(!depMode){cap.innerHTML='';return;}- cap.innerHTML=depSel- ? `Arrow picked: <b>${esc(depName(depSel.to))}</b> needs <b>${esc(depName(depSel.from))}</b>. <kbd>Delete</kbd> twice removes it, <kbd>Esc</kbd> lets it go.`- : depPending- ? `Now click the task that waits on <b>${esc(depName(depPending))}</b>. Clicking one that already does removes that link. <kbd>Esc</kbd> clears the pick, <kbd>Esc</kbd> again leaves.`- : `Adding dependencies. Click the task that comes <b>first</b>, then the one that waits on it. Click an arrow to pick it. <kbd>Esc</kbd> leaves the mode.`; } function setDepMode(on){ depMode=!!on; if(!depMode){setDepPending(null);setDepSel(null);clearDepHover();} document.body.classList.toggle('dep-mode',depMode); if(depMode){closeEditor();closeMenus();clearSelection();}- syncDepCaption();+ syncDepMode(); } function toggleDepMode(){ setDepMode(!depMode); } // ── The row under the pointer ──────────────────────────────────────────────────@@ -6206,6 +6295,18 @@ function openItemModal(r,carriedName){ // that actually produced a row does. let lastCreated=null; function openCreateEditor(){+ // Making a row and linking rows are two different jobs, and the second one holds the+ // chart: in dependency mode a click on a bar is half of a link, the row under the+ // pointer wears a wash, and an armed source is outlined waiting for its pair. Opening+ // the create panel on top of all that leaves the user typing a name into a panel while+ // the chart behind it is still armed for something else, and their first click after+ // Create lands as half a dependency they did not ask for.+ //+ // So creating LEAVES the mode first, by the same door Escape uses, and then creates.+ // Every create path in the app comes through here (Shift+N, the New button), so this+ // is the one place it has to be said. Nothing is refused and nothing is deferred: the+ // mode goes, the panel opens.+ if(depMode)setDepMode(false); closeEditor(); const today=isoDay(new Date()); // One week is the default span for a new task: most things people add here are a@@ -6585,7 +6686,7 @@ let edDrag=null, edManualPos=null; // square side is the fallback for a panel that has not been laid out yet. function edSize(){ const r=editorEl.getBoundingClientRect();- const sq=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--ed-sq'),10)||370;+ const sq=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--ed-sq'),10)||256; return {w:r.width||sq, h:r.height||sq}; } // The one place a panel is positioned. Callers pass where they WOULD like it; if the@@ -6609,7 +6710,7 @@ function placeEditor(x,y){ // The square side, as a number, for the auto-placement maths. Read from the same // custom property the stylesheet sizes the panel with, so there is one source of it. function edSquareSide(){- return parseInt(getComputedStyle(document.documentElement).getPropertyValue('--ed-sq'),10)||370;+ return parseInt(getComputedStyle(document.documentElement).getPropertyValue('--ed-sq'),10)||256; } // Show the panel in the shape this caller needs, then position it. Shown BEFORE // placing on purpose: a display:none box measures zero, and the clamp is only honest@@ -6651,11 +6752,18 @@ function syncEditorDates(r){ set('deStart',r.start); set('deEnd',r.end); set('imStart',r.start); set('imEnd',r.end); }-function showEditor(x,y,square){+// The shape a caller wants, named rather than flagged. 'square' is the fixed-side task+// panel; 'compact' is the same dense metrics without the fixed aspect, for the panels+// whose content is a different shape (the milestone panel is short and wide, the batch+// panel is a list of shifts). Anything else is a full-size panel: the item modal and the+// create card carry a name, an owner, a status and a provenance field, they are read and+// filled rather than glanced at, and 0.47.0 deliberately leaves their metrics alone.+function showEditor(x,y,shape){ // Every panel in the app opens through here, so this is the one place an armed // Delete has to be withdrawn when the user's attention moves to a panel instead. disarmDelete();- editorEl.classList.toggle('square',!!square);+ editorEl.classList.toggle('square',shape==='square');+ editorEl.classList.toggle('compact',shape==='square'||shape==='compact'); editorEl.classList.add('show'); placeEditor(x,y); }
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.46.0",+ "version": "0.47.0", "description": "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, and one more lights up the critical path. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for total, remaining and average hours, one row per person carrying both their hours and their status counts, per-section totals, milestones, dependency stats and what is overdue. 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.46.0",+ "version": "0.47.0", "description": "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, and one more lights up the critical path. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for total, remaining and average hours, one row per person carrying both their hours and their status counts, per-section totals, milestones, dependency stats and what is overdue. 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": {@@ -85,4 +85,4 @@ "name": "Drew Owens", "email": "[email protected]" }-}+}