Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## Draggable start on overdue and due-today bars (2026-06-22)

### Fixed
- Overdue and due-today task bars no longer collapse to a fixed today box; they show the real start date through today so the left resize ear can change the start date on the chart.

## Independent date editing (2026-06-22)

### Changed
- Removed sort-by-start-date controls and automatic parent/subtask date coupling (no more expanding or clipping dates across levels).
- Start and end dates on a project, task, or subtask no longer snap together when edited; each field updates on its own.
- Projects, tasks, and subtasks can each have start and end dates extended or shortened independently via the detail sheet, bar menu, or gantt drag handles.

## Parent task priority over subtasks (2026-06-22)

### Changed
- Parent tasks keep their own t-shirt size in the detail sheet; subtask sizes no longer replace it.
- Press Enter in the subtask input on the detail sheet to add a subtask without clicking Add.
- Detail pop-ups for projects, tasks, and subtasks include a Save button that flushes pending edits, persists, and closes the sheet.
- Clicking the chart background inside a project opens that project's detail sheet and focuses the new-task input.
- Projects still roll up dates and size points across all descendant leaves.

## Rebase PR #14 onto main (2026-06-19)

### Fixed
Expand Down
15 changes: 14 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,11 @@
background-repeat:no-repeat;background-position:right 5px center;background-size:15px}
.tbox .frow select:hover{background-color:#f0f2f6}
.tbox .danger{color:var(--red);background:var(--red-soft);border-radius:11px;padding:12px 18px;
font-weight:700;font-size:14px;margin-top:18px;width:100%}
font-weight:700;font-size:14px;margin-top:10px;width:100%}
.tbox .dsave{width:100%;margin-top:18px;padding:12px 18px;border-radius:11px;background:var(--accent);
color:#fff;font-weight:700;font-size:15px}
.tbox .dsave:active{transform:scale(.98)}
.tbox .dsave.saved{background:var(--green)}
.closex{position:absolute;top:14px;right:16px;width:36px;height:36px;
border-radius:50%;background:#eef0f4;color:var(--ink-2);font-size:16px;font-weight:700;z-index:1}
.searchwrap{position:relative;flex:1;max-width:430px}
Expand Down Expand Up @@ -278,6 +282,15 @@
/* project group: a thin summary bar spanning the project's task range, then its tasks */
.pgroup{position:relative;margin-top:22px;padding:2px 0 8px}
.pgroup:first-of-type{margin-top:0;padding-top:0}
/* clickable project wash — empty chart space opens the project to add tasks */
.pgclick{position:absolute;inset:0;z-index:0;cursor:pointer;border:none;padding:0;background:transparent;font:inherit}
.pgwash{position:absolute;top:0;bottom:0;background:rgba(59,110,246,.05);border-radius:10px;
transition:background .15s;pointer-events:none}
.pgclick:hover .pgwash{background:rgba(59,110,246,.1)}
.pgroup>.grow{position:relative;z-index:1;pointer-events:none}
.pgroup .gtrack{pointer-events:none}
.pgroup .gbar,.pgroup .gsumlbl,.pgroup .gsumpct,.pgroup .gsumline,.pgroup .gsumfill,
.pgroup .gexp,.pgroup .gexpw,.pgroup .ear,.pgroup .gdot,.pgroup .gttlout{pointer-events:auto}
/* project header: name on its own line, the progress bar on the line below it */
/* top-align so the row's extra height (the gap before the first task) stays BELOW the bar,
instead of being centered and pushing the project name down away from the ribbon */
Expand Down
89 changes: 61 additions & 28 deletions src/app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ let dayN, dayIso, barSpan, workDays, barColor, barGeom,
rollupSpan, spanFor, leafWeight, progWD, isUrgent, fmtD, commitBarDrag;
function syncDateHelpers() {
({ dayN, dayIso, barSpan, workDays, barColor, barGeom,
rollupSpan, spanFor, leafWeight, progWD, isUrgent, fmtD, commitBarDrag } = createDateHelpers(TODAY));
rollupSpan, spanFor, leafWeight, progWD, isUrgent, fmtD, commitBarDrag } = createDateHelpers(TODAY, () => DATA));
}
syncDateHelpers();

Expand Down Expand Up @@ -275,8 +275,8 @@ function scheduleTodayRefresh() {
}
/* bars carry only FOUR urgency colors (owner identity is in the bubble) */
/* vivid, candy-bright status palette (like the reference) */
/* a parent task's bar always spans its subtasks (start = earliest sub start, due = latest sub due),
so a parent can never appear to start after its own subtasks */
/* a parent task's bar uses its own dates/size; subtask edits do not change it.
Projects still roll up across all descendant leaves. */
/* effort weight = working days (Mon–Fri) inside a leaf's bar span, so the team doesn't have
to size every item — a longer subtask simply weighs more. A set size still counts, because
size feeds the bar's span via LEAD, which feeds this. Min 1 so a single-day or weekend-only
Expand All @@ -290,9 +290,9 @@ function scheduleTodayRefresh() {
let TW = 2.2, SPAN_EFFV = SPAN_G + 1.2;
const uDay=t=>t<0?t:(t<1?t*TW:TW+(t-1)); // day → stretched-day units
const gx=t=>(uDay(t)-R0G)/SPAN_EFFV*100; // day → % position on the track
/* a due date means END of that day, and open work never lives in the past:
/* a due date means END of that day:
— done tasks keep their historical span
late and due-today tasks ALL span exactly the today box, ending ON the today line
overdue / due-today tasks show their real start through today so resize ears work
— future tasks start today at the earliest and end at the end of their due day */
/* re-renders triggered by clicks are deferred out of the input event: mutating the DOM
while Chrome is still dispatching the click can wedge its hover/input pipeline
Expand Down Expand Up @@ -489,12 +489,22 @@ function renderGantt(){
let pPts=0; flat([p],x=>{ if(x.children.length) return; pPts+=sizePts(x.size); });
const ph=Math.max(7,Math.min(20,Math.round(5+Math.sqrt(pPts)*2.2)));
rows.push(`<div class="pgroup" data-pid="${p.id}">
<button type="button" class="pgclick" onclick="openProjectChart(${p.id})"
title="Open ${p.title.replace(/"/g,"&quot;")} — add tasks"
aria-label="Open ${p.title.replace(/"/g,"&quot;")} — add tasks">
<span class="pgwash" style="left:${gx(scs)}%;width:${spanW}%"></span>
</button>
<div class="grow gsumrow" style="min-height:${18+ph+16}px"><div class="gtrack">
<button class="gsumlbl" style="left:${gx(scs)}%" onpointerdown="projDown(event,${p.id})"
oncontextmenu="barContext(event,${p.id},this.getBoundingClientRect())"
data-full="${p.title} — ${ppc}% done · ${pPts} pts · ${open} open · due ${p.due?fmtD(p.due):"no date"} — click to manage, drag to reorder">${p.title}</button>
<div class="gsumline" style="left:${gx(scs)}%;width:${spanW}%;height:${ph}px" onclick="openDetail(${p.id})" title="Open ${p.title.replace(/"/g,"&quot;")}"></div>
<div class="gsumfill" style="left:${gx(scs)}%;width:${spanW*prog}%;height:${ph}px" onclick="openDetail(${p.id})" title="Open ${p.title.replace(/"/g,"&quot;")}"></div>
<button type="button" class="gsumpct" style="left:${gx(sce)}%;top:${Math.round(18+ph/2-6)}px" onclick="openDetail(${p.id})" title="Open ${p.title.replace(/"/g,"&quot;")}">${ppc}%</button>
<div class="gsumline" style="left:${gx(scs)}%;width:${spanW}%;height:${ph}px"
onclick="openProjectChart(${p.id})" oncontextmenu="barContext(event,${p.id},this.getBoundingClientRect())"
title="Open ${p.title.replace(/"/g,"&quot;")}"></div>
<div class="gsumfill" style="left:${gx(scs)}%;width:${spanW*prog}%;height:${ph}px"
onclick="openProjectChart(${p.id})" oncontextmenu="barContext(event,${p.id},this.getBoundingClientRect())"
title="Open ${p.title.replace(/"/g,"&quot;")}"></div>
<button type="button" class="gsumpct" style="left:${gx(sce)}%;top:${Math.round(18+ph/2-6)}px" onclick="openProjectChart(${p.id})" title="Open ${p.title.replace(/"/g,"&quot;")}">${ppc}%</button>
</div></div>
${taskRows}</div>`);
});
Expand Down Expand Up @@ -884,25 +894,19 @@ function refreshBarMenu(id){ if(BARMENU===id) openBarMenu(id); }
function closeBarMenu(){ BM.classList.remove("show"); BARMENU=null; }
document.addEventListener("pointerdown",e=>{ if(BARMENU&&!e.target.closest("#barMenu")) closeBarMenu(); },true);

function syncTaskDates(n,field){
if(!n.start||!n.due) return;
const s=dayN(n.start), e=dayN(n.due);
if(isNaN(s)||isNaN(e)||s<=e) return;
if(field==="start") n.due=n.start; else n.start=n.due;
}
function updTask(id,f,v,quiet){ snap(); const n=findPath(id).pop();
function updTask(id,f,v,quiet){ snap(); const path=findPath(id); const n=path.pop();
if(f==="title") n.title=v.trim()||n.title;
else if(f==="owner") n.owner=v;
else if(f==="priority") n.priority=v;
else if(f==="due"){ n.due=v||null; syncTaskDates(n,"due"); }
else if(f==="start"){ n.start=v||null; syncTaskDates(n,"start"); }
else if(f==="due") n.due=v||null;
else if(f==="start") n.start=v||null;
else if(f==="size") n.size=v?normalizeSize(v):null;
else if(f==="comment") n.comment=v.trim()||null;
renderAll(); if(!quiet&&f!=="title"&&f!=="comment") openDetail(id); }
function deleteTask(id){ const n=findPath(id).pop();
if(typeof confirm!=="undefined"&&!confirm('Delete "'+n.title+'"'+(n.children.length?" and its subtasks":"")+"?")) return;
snap(); detach(id); closeSheet(); renderAll(); }
function addChild(id){
function addChild(id, quiet){
const el=document.getElementById("dSubNew");
if(!el) return;
const v=el.value.trim();
Expand All @@ -918,9 +922,30 @@ function addChild(id){
n.open=true;
if(path.length>1&&!subOpen(id)){ if(subsAll) COL.delete(id); else EXP.add(id); } // show new subtask rows on the gantt
renderAll();
if(quiet) return child;
openDetail(id,{reveal:"bottom"});
requestAnimationFrame(()=>document.getElementById("dSubNew")?.focus());
revealGanttTask(child.id);
}
function saveDetail(){
if(DETAIL_ID==null) return;
const id=DETAIL_ID;
snap();
const path=findPath(id);
if(!path) return;
const n=path[path.length-1];
const titleEl=document.getElementById("dTitle");
if(titleEl){ const t=titleEl.value.trim(); if(t) n.title=t; }
const commentEl=document.querySelector(".dcomment");
if(commentEl) n.comment=commentEl.value.trim()||null;
if(document.getElementById("dSubNew")?.value.trim()) addChild(id,true);
requestSave();
renderAll();
const btn=document.querySelector(".dsave");
if(btn){ btn.textContent="Saved ✓"; btn.classList.add("saved"); btn.disabled=true; }
ding(1);
setTimeout(closeSheet,400);
}
function addProject(){
snap();
const proj=T("New project","fd",{d:dayIso(LEAD.l),open:true});
Expand Down Expand Up @@ -948,6 +973,9 @@ function revealGanttTask(id){
if(typeof requestAnimationFrame!=="undefined") requestAnimationFrame(()=>requestAnimationFrame(run));
else run();
}
function openProjectChart(id){
openDetail(id,{focusTask:true,reveal:"bottom"});
}
function openDetail(id,opts){
const path=findPath(id);
if(!path){ if(DETAIL_ID===id) closeSheet(); DETAIL_ID=null; return; }
Expand All @@ -966,10 +994,10 @@ function openDetail(id,opts){
flat(DATA,(x,depth)=>{ if(contains(n,x.id))return;
if(depth+1+heightOf(n)>2) return; // keep the 3-level hierarchy
mopts.push(`<option value="${x.id}" ${x.id===par?'disabled':''}>${"&nbsp;".repeat(depth*3)}${x.title}${x.id===par?" (current)":""}</option>`); });
// Size: leaves carry an editable t-shirt size; projects/parent tasks SHOW the rolled-up
// point total (sum of their leaves' size points) — not a field the user fills in.
// Size: subtasks and parent tasks carry an editable t-shirt size; projects show rolled-up points.
let _szPts=0; flat([n],x=>{ if(x.children.length) return; _szPts+=sizePts(x.size); });
const sizeFld=leaf
const sizeEditable=leaf||path.length===2;
const sizeFld=sizeEditable
? `<div class="frow"><span class="lbl">Size</span><select onchange="updTask(${id},'size',this.value)">
<option value="">—</option>${SIZE_KEYS.map(z=>`<option value="${z}" ${normalizeSize(n.size)===z?'selected':''}>${SIZE_NAMES[z]} · ${SIZE_PTS[z]} pts</option>`).join("")}</select></div>`
: `<div class="frow"><span class="lbl">Size</span><span style="flex:1;font-size:15px;font-weight:700;color:var(--ink)">${_szPts} pts</span></div>`;
Expand All @@ -989,24 +1017,29 @@ function openDetail(id,opts){
<div class="frow"><span class="lbl">Move to</span><select onchange="moveTask(${id},this.value)">${mopts.join("")}</select></div>
${path.length>=3?"":`<div class="subhdr">${path.length>1?"Subtasks":"Tasks — grip ⠿ to drag onto another project"}</div>`}
${n.children.map(ch=>{ const lp=pct(ch), lleaf=!ch.children.length;
let _cp=0; flat([ch],x=>{ if(x.children.length) return; _cp+=sizePts(x.size); });
const szCtl=lleaf
? `<select class="rowsz" title="Weight (t-shirt size)" onchange="updTask(${ch.id},'size',this.value,true);openDetail(${id})"><option value="">–</option>${SIZE_KEYS.map(z=>`<option value="${z}" ${normalizeSize(ch.size)===z?"selected":""}>${SIZE_NAMES[z]} · ${SIZE_PTS[z]} pts</option>`).join("")}</select>`
: `<span class="rowpts" title="Rolled up from subtasks">${_cp} pts</span>`;
const szCtl=`<select class="rowsz" title="Weight (t-shirt size)" onchange="updTask(${ch.id},'size',this.value,true);openDetail(${id})"><option value="">–</option>${SIZE_KEYS.map(z=>`<option value="${z}" ${normalizeSize(ch.size)===z?"selected":""}>${SIZE_NAMES[z]} · ${SIZE_PTS[z]} pts</option>`).join("")}</select>`;
return `<div class="ptask" data-cid="${ch.id}">
<button class="grip2" onpointerdown="rowDown(event,${ch.id})" aria-label="Drag onto a project pill in the timeline">${GRIP_SVG}</button>
<button class="check ${lleaf?(ch.done?'on':''):(lp===100?'on':'')}" onclick="toggleDone(${ch.id});openDetail(${id})">${(lleaf?ch.done:lp===100)?'✓':''}</button>
<button class="t" style="text-align:left" onclick="openDetail(${ch.id})">${ch.title}</button>
${ownerPill(ch.owner,`updTask(${ch.id},'owner',this.value,true);openDetail(${id})`)}
${szCtl}
${dueChip(ch.due,lleaf&&ch.done)}</div>`;}).join("")}
${path.length>=3?"":`<div class="subadd" style="margin-top:10px"><input id="dSubNew" placeholder="Add a ${path.length>1?"subtask":"task"}…"
onkeydown="if(event.key==='Enter'){event.preventDefault();addChild(${id});}"><button type="button" onclick="addChild(${id})">Add</button></div>`}
${path.length>=3?"":`<div class="subadd" style="margin-top:10px"><input id="dSubNew" placeholder="Add a ${path.length>1?"subtask":"task"}…">
<button type="button" onclick="addChild(${id})">Add</button></div>`}
<button type="button" class="dsave" onclick="saveDetail()">Save</button>
<button class="danger" onclick="deleteTask(${id})">Delete ${path.length===1?"project":path.length>=3?"subtask":"task"}</button>`;
document.getElementById("tmodal").classList.add("show");
document.getElementById("scrim").classList.add("show");
if(opts?.reveal) revealDetailScroll(box,opts.reveal);
else restoreDetailScroll(box,savedScroll);
const subNew=document.getElementById("dSubNew");
if(subNew) subNew.onkeydown=e=>{
if(e.key!=="Enter") return;
e.preventDefault();
addChild(id);
};
if(opts?.focusTask) requestAnimationFrame(()=>document.getElementById("dSubNew")?.focus());
}
function closeSheet(){ DETAIL_ID=null;
document.getElementById("tmodal").classList.remove("show");
Expand Down Expand Up @@ -1665,7 +1698,7 @@ const _globals = {
toggleSearch, openTeam, micFabTap, openTranscript, toggleSettings, toggleSidebar, closeSettings,
toggleFlyout, toggleFocus, toggleShowDone, toggleSubs, closeCapture, toggleCapLang, minimizeCapture,
sendTurn, restoreCapture, skipKey, saveKey, clearKey, closeTranscript, runTranscript, closeReview,
closeTeam, closeSheet, setFilter, setScaleView, ding, toggleDone, openDetail, setZoom, setGView,
closeTeam, closeSheet, saveDetail, openProjectChart, setFilter, setScaleView, ding, toggleDone, openDetail, setZoom, setGView,
toggleExp, updTask, refreshBarMenu, addChild, addProject, deleteTask, addCapTask, barDown, barContext, pickSearch,
projDown, rowDown,
uploadPhoto, removePhoto, rvToggle, rvText, rvOwner, rvDue, rvSize, pushApproved, attachTranscript,
Expand Down
Loading
Loading