← Commit history

0.24.1: toasts you can actually catch, and a one-week default span

Drew Owens ·14703f419a · ·parent 8749dec
Checked the durations before changing them and the complaint was half right. The general confirmation path, flash(), had a 1500ms default, under the two-second floor. But the toasts that already passed a duration explicitly were 2400 to 5000ms, so duration alone did not explain gone-before-I-notice. The other half was that a confirmation was bare 10px --text2 text on the page ground in the bottom right with an opacity-only fade. It was quiet, not brief. It is a pill now with the same treatment as the undo toast: --elevated ground, a hairline border, a 3px teal left edge, body text on --text, and a teal-tinted shadow. The accent is a splash and an edge, never a fill. Both rise 8px as they fade in over 180ms ease-out, no bounce. Hover pauses the timer, which is the part that actually answers the complaint. Every toast goes through one showToast() that owns its timer, so the pause is one implementation rather than three; a paused toast restarts its full duration on mouse-out. Durations: general confirmation 1500 to 2800; explicit hints unchanged at 2400-2600, already over the floor; undo and redo 1800 to 3000; delete, single and multi, 3200 to 4500; the save-conflict prompt with its Reload button keeps no timer at all. One latent bug fixed in passing: .undo-toast carried pointer-events none, so the Reload button in the save-conflict prompt could never have been clicked. Also the New panel's default span is one week, today to today plus seven. The 0.18.1 carried values still win over it, because the user set those.
4 files changed +81−18
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-12 (0.24.1, toasts you can actually catch)++**Checked the durations before changing them, and the complaint was half right.** The+general confirmation path, `flash()`, had a **1500ms** default, under the two-second+floor. But the toasts that already passed explicitly were 2400 to 5000ms, so duration+alone did not explain "gone before I notice". The other half was that a confirmation+was bare 10px `--text2` text sitting directly on the page ground in the bottom right+corner, with an opacity-only fade. It was quiet, not brief.++So it is a pill now, with the same treatment as the undo toast: `--elevated` ground,+a hairline border, a **3px teal left edge**, body text on `--text` rather than+`--text2`, and a teal-tinted shadow. The accent is a splash and an edge, never a fill+and never a wall of teal text. Both rise 8px as they fade in over 180ms, ease-out, no+bounce.++**Hover pauses the timer**, which is the part that actually answers the complaint: a+toast you are reading no longer counts down while you read it. Every toast now goes+through one `showToast()` that owns its timer, so the pause is one implementation+rather than three. A paused toast restarts its full duration on mouse-out, measured+at 2800ms after leaving.++| Toast | Before | After |+|---|---|---|+| General confirmation, `flash()` default | 1500ms | **2800ms** |+| Explicit hints (sort bounds, refusals) | 2400 to 2600ms | unchanged, already over the floor |+| Undo and redo, naming a keystroke | 1800ms | **3000ms** |+| Delete, single and multi, naming Ctrl+Z | 3200ms | **4500ms** |+| Save-conflict prompt with a Reload button | no timer | unchanged, waits for a decision |++**One latent bug fixed in passing.** `.undo-toast` carried `pointer-events: none`,+which meant the Reload button in the save-conflict prompt could never have been+clicked. Pointer events are enabled while a toast is shown, which both hover-pause+and that button need.++**And the New panel's default span is one week**, start today to today plus seven.+Fourteen days was never more than a round number and most things people add here are+a week's work. The 0.18.1 carried values still win over it unchanged, because the+user set those: verified that a pristine panel offers today to today+7 and that a+panel carrying 2026-09-01 to 2026-09-30 keeps them. Milestones are unaffected, they+have one date and no span.+ ## Build notes, 2026-08-12 (0.24.0, the table can select rows too)  **The blue swath was the absence of a feature, not a bug in one.** The table had no
gantt.html+37−15
@@ -338,8 +338,11 @@ body.col-resizing { cursor: col-resize; user-select: none; } .dropdown .dd-label { padding: 4px 14px; font-family: 'JetBrains Mono', monospace; font-size: 9px; color: var(--text2); }  /* ── Toasts ── */-.undo-toast { position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); background: var(--elevated); border: 1px solid var(--border); border-radius: 8px; padding: 8px 16px; font-size: 12px; color: var(--text2); box-shadow: 0 8px 24px rgba(0,0,0,0.5); z-index: 99998; opacity: 0; transition: opacity 0.15s; pointer-events: none; }-.undo-toast.show { opacity: 1; }+/* The accent is a splash, not a fill: a teal left edge and a teal-tinted shadow, with+   the body text on --text so it reads as a message rather than a teal wall. It rises+   a few pixels as it fades in, which is enough to catch the eye without bouncing. */+.undo-toast { position: fixed; bottom: 16px; left: 50%; transform: translate(-50%, 8px); background: var(--elevated); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 8px; padding: 9px 16px; font-size: 12px; color: var(--text); box-shadow: 0 8px 24px rgba(0,0,0,0.5), 0 0 0 1px rgba(0,184,177,0.16), 0 2px 14px rgba(0,184,177,0.12); z-index: 99998; opacity: 0; transition: opacity 0.18s ease-out, transform 0.18s ease-out; pointer-events: none; }+.undo-toast.show { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; } .undo-toast kbd { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text2); } /* A plan that could not be fully rendered says so, and keeps saying so: a silently    shorter plan is worse than an ugly bar. */@@ -350,8 +353,10 @@ body.col-resizing { cursor: col-resize; user-select: none; } .data-warn .dw-close { position: absolute; top: 6px; right: 10px; background: none; border: none; color: var(--text2); font-size: 14px; cursor: pointer; line-height: 1; } .data-warn .dw-close:hover { color: var(--text); } @media print { .data-warn { display: none !important; } }-.save-indicator { position: fixed; bottom: 16px; right: 16px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text2); z-index: 99998; opacity: 0; transition: opacity 0.3s; }-.save-indicator.show { opacity: 1; }+/* Was bare 10px --text2 text on the page ground, which is most of why these went+   unnoticed. It is a pill now, with the same teal left edge as the toast. */+.save-indicator { position: fixed; bottom: 16px; right: 16px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text); background: var(--elevated); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 7px; padding: 6px 11px; box-shadow: 0 6px 18px rgba(0,0,0,0.42), 0 0 0 1px rgba(0,184,177,0.14); max-width: 44vw; z-index: 99998; opacity: 0; transform: translateY(8px); transition: opacity 0.18s ease-out, transform 0.18s ease-out; pointer-events: none; }+.save-indicator.show { opacity: 1; transform: translateY(0); pointer-events: auto; } /* The version is chrome: present for orientation, never competing with the plan. */ header .subject-meta .app-ver { color: var(--text3); } .sel-count { position: fixed; bottom: 16px; left: 16px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--accent); z-index: 99998; opacity: 0; transition: opacity 0.15s; }@@ -1523,10 +1528,9 @@ function timeTravel(from,to,via,label,chord){   to.push(inv);   if(to.length>100)to.splice(0,to.length-100);   if(prefs.view==='table')renderTable();-  clearTimeout(undoTimer);   undoToast.innerHTML=label+' &middot; <kbd>'+chord+'</kbd>';-  undoToast.classList.add('show');-  undoTimer=setTimeout(()=>{undoToast.classList.remove('show');undoToast.innerHTML=UNDO_TOAST_DEFAULT;},1800);+  // 3000, was 1800. It names a keystroke, so it needs long enough to read AND act.+  showToast(undoToast,3000,()=>{undoToast.innerHTML=UNDO_TOAST_DEFAULT;});   // Tagged so the changelog says which direction it was. The markdown renderer   // already puts the via in brackets on every line, so this reads as   // "user reassigned X from A to B (redo)" with no server change.@@ -1614,10 +1618,8 @@ document.addEventListener('keydown',e=>{       pushUndo({type:'multi',steps});       clearSelection();       keepTailLast();applyFilter();refreshChartGeometry();renderTable();-      clearTimeout(undoTimer);       undoToast.innerHTML='Deleted '+steps.length+' rows &middot; <kbd>Ctrl+Z</kbd>';-      undoToast.classList.add('show');-      undoTimer=setTimeout(()=>{undoToast.classList.remove('show');undoToast.innerHTML=UNDO_TOAST_DEFAULT;},3200);+      showToast(undoToast,4500,()=>{undoToast.innerHTML=UNDO_TOAST_DEFAULT;});       scheduleSave('table-delete');       focusRowAt(Math.min(i<0?0:i,visibleRowEls().length-1));       return;@@ -1993,7 +1995,7 @@ function saveToFile(){ // on screen, so nothing is taken from them without their say-so. function onSaveConflict(d){   saveBlocked=true;-  clearTimeout(undoTimer);+  clearTimeout(undoTimer); clearTimeout(undoToast.__t);   // this one waits for a decision   undoToast.innerHTML='Someone else changed this plan. Your edits are not being saved. '+     '<button type="button" id="reloadPlan" class="de-btn primary" style="margin-left:8px;padding:2px 10px">Reload</button>';   undoToast.classList.add('show');@@ -2001,7 +2003,24 @@ function onSaveConflict(d){   if(b)b.addEventListener('click',()=>location.reload());   flash(saveInd,'not saved, plan changed elsewhere',5000); }-function flash(el,text,ms){el.textContent=text;el.classList.add('show');setTimeout(()=>el.classList.remove('show'),ms||1500);}+// Every toast's timer lives here so hover can pause it. Gone-before-you-noticed was+// half duration and half that a toast you are reading keeps counting down; pointing+// at one now stops the clock until you look away, which is what every toast in every+// other app does.+const TOAST_MS=2800;          // general confirmations, was a 1500ms default+function showToast(el,ms,onDone){+  clearTimeout(el.__t);+  el.__ms=ms||TOAST_MS; el.__onDone=onDone||null; el.__paused=false;+  if(!el.__hoverWired){+    el.__hoverWired=true;+    el.addEventListener('mouseenter',()=>{ if(el.classList.contains('show')){ clearTimeout(el.__t); el.__paused=true; } });+    el.addEventListener('mouseleave',()=>{ if(el.__paused&&el.classList.contains('show')){ el.__paused=false; el.__t=setTimeout(el.__hide,el.__ms); } });+    el.__hide=()=>{ el.classList.remove('show'); if(el.__onDone)el.__onDone(); };+  }+  el.classList.add('show');+  el.__t=setTimeout(el.__hide,el.__ms);+}+function flash(el,text,ms){ el.textContent=text; showToast(el,ms); }  // ── Legend ── const legendEl = $('legend');@@ -3354,7 +3373,10 @@ let lastCreated=null; function openCreateEditor(){   closeEditor();   const today=isoDay(new Date());-  const plus=addDays(today,14);+  // One week is the default span for a new task: most things people add here are a+  // week's work, and 14 days was never anything more than a round number. Carried+  // values from the last creation still win over this, because the user set those.+  const plus=addDays(today,7);   // Name and notes always start empty: they are what makes an item that item, and   // carrying them over would offer to create the same row twice. Status and   // provenance keep their own defaults and phase is untouched.@@ -4642,8 +4664,8 @@ function deleteRow(r){   renderTable();   clearTimeout(undoTimer);   undoToast.innerHTML='Deleted '+esc(label)+' &middot; <kbd>Ctrl+Z</kbd>';-  undoToast.classList.add('show');-  undoTimer=setTimeout(()=>{undoToast.classList.remove('show');undoToast.innerHTML=UNDO_TOAST_DEFAULT;},3200);+  // 4500, was 3200. Something just left the plan and this is how you get it back.+  showToast(undoToast,4500,()=>{undoToast.innerHTML=UNDO_TOAST_DEFAULT;});   scheduleSave('table-delete'); } function wireTableFilters(){
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.24.0",+  "version": "0.24.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.24.0",+  "version": "0.24.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]"   }-}\ No newline at end of file+}