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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

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

### Fixed
- Rebased `cursor/expand-size-range-0037` onto `main` (replacing the merge commit) so GitHub can update/rebase the branch cleanly.
- Bar menu keeps expanded `SIZE_KEYS` (S-XXL) from the PR and Start/End date fields from `main`.
- `updTask` keeps `normalizeSize` for size and `syncTaskDates` for start/end from `main`.

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

### Fixed
Expand Down
16 changes: 9 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
.pill.sz-m {font-size:13.5px;padding:9px 16px}
.pill.sz-l {font-size:15.5px;padding:13px 22px}
.pill.sz-xl{font-size:17px; padding:17px 28px}
.pill.sz-xxl{font-size:18px;padding:18px 30px}
.pill.late {background:var(--red-soft);color:var(--red)}
.pill.today{background:var(--accent-soft);color:var(--accent)}
.pill.soon {background:var(--green-soft);color:var(--green)}
Expand Down Expand Up @@ -307,10 +308,11 @@
.gbar.gsz-m .gava .av{width:26px;height:26px;font-size:11px}
.gbar.gsz-l .gava .av{width:36px;height:36px;font-size:14px}
.gbar.gsz-xl .gava .av{width:48px;height:48px;font-size:18px}
.gbar.gsz-s{height:26px}.gbar.gsz-m{height:34px}.gbar.gsz-l{height:44px}.gbar.gsz-xl{height:56px}
.gbar.gsz-xxl .gava .av{width:54px;height:54px;font-size:19px}
.gbar.gsz-s{height:26px}.gbar.gsz-m{height:34px}.gbar.gsz-l{height:44px}.gbar.gsz-xl{height:56px}.gbar.gsz-xxl{height:68px}
.gbar .ttl{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;
text-overflow:ellipsis;pointer-events:none;flex:1;min-width:0;text-align:left;padding-left:1px}
.gbar.gsz-s .ttl{font-size:10.5px}.gbar.gsz-l .ttl{font-size:13px}.gbar.gsz-xl .ttl{font-size:14px}
.gbar.gsz-s .ttl{font-size:10.5px}.gbar.gsz-l .ttl{font-size:13px}.gbar.gsz-xl .ttl{font-size:14px}.gbar.gsz-xxl .ttl{font-size:15px}
/* title rendered to the right of a too-narrow bar (plain black text, no background) */
.gttlout{position:absolute;top:50%;transform:translateY(-50%);font-size:12px;font-weight:700;
color:var(--ink);white-space:nowrap;pointer-events:none;z-index:3}
Expand All @@ -333,8 +335,8 @@
.gexp:hover{color:var(--accent)}
/* subtasks are OUTLINED (light fill, coloured border + text) vs solid filled tasks */
.gbar.gsub{box-shadow:0 1px 4px rgba(20,24,35,.12)}
.gbar.gsub .ttl{color:inherit;font-size:11px}
.gbar.gsub .gava .av{width:15px;height:15px;font-size:7.5px;box-shadow:0 0 0 2px #fff}
.gbar.gsub .ttl{color:inherit}
.gbar.gsub .gava .av{box-shadow:0 0 0 2px #fff}
/* subtask bars have a WHITE fill, so the white done-dot was invisible on them — give the dot
the bar's own colour on hover, and a solid green tick once checked */
.gbar.gsub .gdot{border-color:currentColor;color:currentColor}
Expand Down Expand Up @@ -381,7 +383,7 @@
.bm-rw .bm-lbl{margin:0;width:40px;flex:none}
.bm-rw input[type=date],.bm-rw select{flex:1;min-width:0;font:inherit;font-size:13px;border:1px solid var(--line);
border-radius:8px;padding:5px 8px;background:var(--bg);color:var(--ink)}
.bm-rw .szseg{flex:1;justify-content:space-between}
.bm-rw .szseg{flex:1;justify-content:space-between;flex-wrap:wrap;gap:4px}
/* the "today" band: ends with a hard vertical line — left of it = needs doing today */
/* today = a wider column with a faint tint, no box */
/* today tint + week shading stay in the chart body (below the 41px header:
Expand Down Expand Up @@ -474,8 +476,8 @@
.duepill input:focus{outline:none}
.duepill.sm input{width:96px;font-size:11.5px}
/* segmented t-shirt size */
.szseg{display:inline-flex;background:#eef0f4;border-radius:999px;padding:2px}
.szb{padding:3px 10px;border-radius:999px;font-size:11px;font-weight:800;color:var(--ink-3);line-height:1.5}
.szseg{display:inline-flex;flex-wrap:wrap;background:#eef0f4;border-radius:999px;padding:2px;gap:2px}
.szb{padding:3px 8px;border-radius:999px;font-size:10.5px;font-weight:800;color:var(--ink-3);line-height:1.5}
.szb.on{background:var(--accent);color:#fff}
.tc-sub{display:flex;align-items:center;gap:7px;margin-top:8px;padding-left:14px;position:relative}
.tc-sub::before{content:"";position:absolute;left:3px;top:4px;bottom:4px;width:2px;background:var(--line);border-radius:2px}
Expand Down
43 changes: 22 additions & 21 deletions src/app/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
PEOPLE, TODAY, HARDWARE_VOCAB, CLIENTS,
SIZE_PTS, SIZE_NAMES, LEAD, ZOOMS, GBAR_H,
SIZE_KEYS, SIZE_PTS, SIZE_NAMES, LEAD, ZOOMS, GBAR_H, normalizeSize, sizePts, barHeight,
R0G, R1G, SPAN_G, TODAY_PX,
C_LATE, C_TODAY, C_RADAR, C_LATER, C_DONE,
} from "../data/constants.js";
Expand Down Expand Up @@ -35,7 +35,7 @@ const fitsDepth = (node, destId) => fitsDepthIn(node, destId, DATA);

/* ================= helpers ================= */
/* size-weighted progress (0..1): done size-points / total size-points across the leaves */
const progFrac = (n) => { let done = 0, tot = 0; flat([n], (x) => { if (x.children.length) return; const w = SIZE_PTS[x.size || "m"]; tot += w; if (x.done) done += w; }); return tot ? done / tot : 0; };
const progFrac = (n) => { let done = 0, tot = 0; flat([n], (x) => { if (x.children.length) return; const w = sizePts(x.size); tot += w; if (x.done) done += w; }); return tot ? done / tot : 0; };
function dueChip(due,done){ if(!due||done) return "";
const dd=Math.round((parseLocalIso(due)-TODAY)/864e5);
const cls=dd<0?"overdue":dd<=3?"soon":"";
Expand Down Expand Up @@ -127,7 +127,7 @@ function renderDash(){
const da=taskDoneAt(n); if(!da) return;
const ago=Math.round((TODAY-parseLocalIso(da))/864e5);
if(ago>=0&&ago<=Math.max(HZ,0)) banked.push({n,proj:(path[0]||n).title,dn:true}); });
const PT=x=>SIZE_PTS[x.n.size||"m"];
const PT=x=>sizePts(x.n.size);
const pill=(x,cls,side)=>`<div class="pill ${cls} side-${side} sz-${x.n.size||'m'}" data-full="${x.n.title} — ${person(x.n.owner).name} · ${x.proj}">
<button class="pop ${x.dn?'on':''}" onclick="ding(4);toggleDone(${x.n.id})" aria-label="${x.dn?'Undo':'Mark done'}">${x.dn?'✓':''}</button>
<button class="pbody" onclick="openDetail(${x.n.id})"><span class="t">${x.n.title}</span></button>
Expand Down Expand Up @@ -426,8 +426,8 @@ function renderGantt(){
if(!n.due) return "";
const {s,e}=spanFor(n), done=isSub?n.done:taskDone(n);
if(e<R0G-14||s>R1G) return "";
const late=!done&&e<0, [tcs,tce]=barGeom(s,e,done), sz=n.size||"m";
const h=isSub?Math.max(15,Math.round(GBAR_H[sz]*0.62)):GBAR_H[sz]; // subtasks shorter than tasks
const late=!done&&e<0, [tcs,tce]=barGeom(s,e,done), sz=normalizeSize(n.size);
const h=barHeight(sz); // same thickness scale for tasks and subtasks
// a task with subtasks shows its duration-weighted completion as a darker fill inside its
// own bar (same two-tone idea as the project summary bar, applied in place)
const hasKids=!isSub&&n.children.length>0, donePct=hasKids?Math.round(progWD(n)*100):0;
Expand Down Expand Up @@ -485,7 +485,7 @@ function renderGantt(){
any=true;
const prog=progWD(p), ppc=Math.round(prog*100), spanW=gx(sce)-gx(scs);
// project bar thickness scales with the project's total weight (sum of its leaf size points)
let pPts=0; flat([p],x=>{ if(x.children.length) return; pPts+=SIZE_PTS[x.size||"m"]; });
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}">
<div class="grow gsumrow" style="min-height:${18+ph+16}px"><div class="gtrack">
Expand Down Expand Up @@ -513,7 +513,7 @@ function renderGantt(){
cand.push({n,e,root,par});
});
cand.sort((a,b)=>a.e-b.e
||SIZE_PTS[b.n.size||"m"]-SIZE_PTS[a.n.size||"m"]
||sizePts(b.n.size)-sizePts(a.n.size)
||PRW[a.n.priority||"med"]-PRW[b.n.priority||"med"]);
any=cand.length>0;
rows.push('<div class="pflat">');
Expand Down Expand Up @@ -867,7 +867,7 @@ function openBarMenu(id,anchor){
BM.innerHTML=`
<div class="bm-lbl">Owner</div>
<div class="bm-chips">${Object.entries(PEOPLE).map(([k,p])=>`<button class="bm-chip ${n.owner===k?'on':''}" title="${p.name}" onclick="updTask(${id},'owner','${k}',true);refreshBarMenu(${id})"><span class="av xs" style="background:${p.color}">${p.initials}</span></button>`).join("")}</div>
<div class="bm-rw"><span class="bm-lbl">Size</span><span class="szseg">${["s","m","l","xl"].map(z=>`<button class="szb ${n.size===z?'on':''}" onclick="updTask(${id},'size','${n.size===z?'':z}',true);refreshBarMenu(${id})">${SIZE_NAMES[z]}</button>`).join("")}</span></div>
<div class="bm-rw"><span class="bm-lbl">Size</span><span class="szseg">${SIZE_KEYS.map(z=>`<button class="szb ${n.size===z?'on':''}" onclick="updTask(${id},'size','${n.size===z?'':z}',true);refreshBarMenu(${id})">${SIZE_NAMES[z]}</button>`).join("")}</span></div>
<div class="bm-rw"><span class="bm-lbl">Start</span><input type="date" value="${n.start||''}" onchange="updTask(${id},'start',this.value,true)"></div>
<div class="bm-rw"><span class="bm-lbl">End</span><input type="date" value="${n.due||''}" onchange="updTask(${id},'due',this.value,true)"></div>`;
BM.classList.add("show"); BARMENU=id;
Expand Down Expand Up @@ -895,7 +895,7 @@ function updTask(id,f,v,quiet){ snap(); const n=findPath(id).pop();
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==="size") n.size=v||null;
else if(f==="size") n.size=v?normalizeSize(v):null;
renderAll(); if(!quiet&&f!=="title") 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;
Expand Down Expand Up @@ -966,10 +966,10 @@ function openDetail(id,opts){
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.
let _szPts=0; flat([n],x=>{ if(x.children.length) return; _szPts+=SIZE_PTS[x.size||"m"]; });
let _szPts=0; flat([n],x=>{ if(x.children.length) return; _szPts+=sizePts(x.size); });
const sizeFld=leaf
? `<div class="frow"><span class="lbl">Size</span><select onchange="updTask(${id},'size',this.value)">
<option value="">—</option>${["s","m","l","xl"].map(z=>`<option value="${z}" ${n.size===z?'selected':''}>${SIZE_NAMES[z]} · ${SIZE_PTS[z]} pts</option>`).join("")}</select></div>`
<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>`;
document.getElementById("dBody").innerHTML=`
<div class="frow"><span class="lbl">Owner</span>${av(n.owner)}<select onchange="updTask(${id},'owner',this.value)">
Expand All @@ -983,9 +983,9 @@ 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+=SIZE_PTS[x.size||"m"]; });
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>${["s","m","l","xl"].map(z=>`<option value="${z}" ${ch.size===z?"selected":""}>${SIZE_NAMES[z]}</option>`).join("")}</select>`
? `<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>`;
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>
Expand Down Expand Up @@ -1105,6 +1105,7 @@ function updateKeyBadge(){ const b=$id("keyBtn"); if(b) b.textContent=getKey()?"

/* swappable extraction: a live endpoint, then OpenAI direct (browser key), then the mock */
const OWNER_IDS=[...Object.keys(PEOPLE),null]; // owner must be a real teammate id, never free text
const SIZE_SCHEMA_ENUM=[...SIZE_KEYS,null];
const OAI_SCHEMA={type:"object",additionalProperties:false,
required:["intent","project","task","tasks","remove","owner","parentId","due","size","pending","ready","assistantSay"],
properties:{
Expand All @@ -1114,13 +1115,13 @@ const OAI_SCHEMA={type:"object",additionalProperties:false,
tasks:{type:"array",items:{type:"object",additionalProperties:false,
required:["title","owner","due","size","subs"],
properties:{title:{type:"string"},owner:{type:["string","null"],enum:OWNER_IDS},
due:{type:["string","null"]},size:{type:["string","null"],enum:["s","m","l","xl",null]},
due:{type:["string","null"]},size:{type:["string","null"],enum:SIZE_SCHEMA_ENUM},
subs:{type:"array",items:{type:"object",additionalProperties:false,
required:["title","owner","due","size"],
properties:{title:{type:"string"},owner:{type:["string","null"],enum:OWNER_IDS},
due:{type:["string","null"]},size:{type:["string","null"],enum:["s","m","l","xl",null]}}}}}}},
due:{type:["string","null"]},size:{type:["string","null"],enum:SIZE_SCHEMA_ENUM}}}}}}},
owner:{type:["string","null"],enum:OWNER_IDS}, parentId:{type:["integer","null"]},
due:{type:["string","null"]}, size:{type:["string","null"],enum:["s","m","l","xl",null]},
due:{type:["string","null"]}, size:{type:["string","null"],enum:SIZE_SCHEMA_ENUM},
pending:{type:["string","null"]}, ready:{type:"boolean"}, assistantSay:{type:"string"}}};
function captureContext(){ return {today:isoCap(TODAY),
people:Object.entries(PEOPLE).map(([id,p])=>({id,name:p.name,responsibility:p.role,aka:p.al})),
Expand Down Expand Up @@ -1158,7 +1159,7 @@ ${VOCAB_TEXT}
- Use intent create_task / create_subtask ONLY when adding to a project/task that ALREADY EXISTS in context.projects. Then set parentId to that existing id.
- "task" (singular) is only for create_task/create_subtask; for create_project leave "task" null and use "tasks".
- owner MUST be one of the provided people ids, or null. Names are frequently MIS-HEARD by voice transcription — map any spelling variant or mishearing listed in the responsibility map to the correct id (e.g. "Janice"/"Yannis"/"Ioannis" → Iannis "ia"; "Flo"/"Florine" → Florian "fd"; "Sankeet" → Sanket "sk"). Do NOT assign the work to a different real teammate just because the heard name is fuzzy; if you genuinely cannot resolve it, use null rather than guessing the wrong person.
- due: resolve relative dates ("Monday","tomorrow","in 3 days") to absolute YYYY-MM-DD using context.today; else null. size: s/m/l/xl if stated else null.
- due: resolve relative dates ("Monday","tomorrow","in 3 days") to absolute YYYY-MM-DD using context.today; else null. size: s/m/l/xl/xxl if stated else null.
- pending = the single most useful field still needed ("projectName","taskTitle","parent","owner"), or null if nothing required is missing. Required: create_project needs project(name); create_task/subtask need task(title) and parentId.
- ready = true when required fields are present (a project is ready once it has a name, even with zero tasks).
- assistantSay = one short, natural sentence confirming what you understood and asking the next thing (or noting it's ready). Talk like a helpful colleague, not a form. If you just appended a task, acknowledge it and invite another or Create.
Expand Down Expand Up @@ -1338,7 +1339,7 @@ function ownerPill(v,onch){ const col=v?person(v).color:"#c2c8d2";
return `<span class="opill" style="--oc:${col}"><span class="odot"></span>
<select onchange="${onch}">${ownerOpts(v)}</select></span>`; }
function duePill(v,onch,sm){ return `<span class="duepill ${sm?'sm':''}"><input type="date" value="${v||''}" onchange="${onch}"></span>`; }
function szSeg(v,onch){ return `<span class="szseg">${["s","m","l","xl"].map(z=>
function szSeg(v,onch){ return `<span class="szseg">${SIZE_KEYS.map(z=>
`<button class="szb ${v===z?'on':''}" onclick="${onch.replace('Z',"'"+z+"'")}">${SIZE_NAMES[z]}</button>`).join("")}</span>`; }
function taskCardHTML(tk,i){ tk.subs=tk.subs||[];
return `<div class="tcard">
Expand Down Expand Up @@ -1417,19 +1418,19 @@ const OAI_PROPOSAL_SCHEMA={type:"object",additionalProperties:false,
title:{type:"string"},
owner:{type:["string","null"],enum:OWNER_IDS},
due:{type:["string","null"]},
size:{type:["string","null"],enum:["s","m","l","xl",null]},
size:{type:["string","null"],enum:SIZE_SCHEMA_ENUM},
client:{type:["string","null"],enum:CLIENT_NAMES},
subs:{type:"array",items:{type:"object",additionalProperties:false,
required:["title","owner","due","size"],
properties:{title:{type:"string"},owner:{type:["string","null"],enum:OWNER_IDS},
due:{type:["string","null"]},size:{type:["string","null"],enum:["s","m","l","xl",null]}}}}
due:{type:["string","null"]},size:{type:["string","null"],enum:SIZE_SCHEMA_ENUM}}}}
}}}
}}}}};
async function openaiTranscript(text,key){
const sys=`You read a raw client/team conversation transcript and extract the NEW engineering projects and tasks it implies, for a 3-level planner (project > task > subtask).
LANGUAGE: the transcript may be English or French — ALL OUTPUT MUST BE IN ENGLISH.
- Group work into projects. A customer pilot becomes a project; set its "client" to the matching known client. Pure internal work has client=null.
- Each task: a concise imperative title (no leading article), owner, due (YYYY-MM-DD resolved from context.today, else null), size (s/m/l/xl or null), client (if the task is for a known client else null), and a subs array (usually empty).
- Each task: a concise imperative title (no leading article), owner, due (YYYY-MM-DD resolved from context.today, else null), size (s/m/l/xl/xxl or null), client (if the task is for a known client else null), and a subs array (usually empty).
- ASSIGNEE: infer each owner from this RESPONSIBILITY MAP using the task's content; only null if genuinely unclear:
${RESP_MAP_TEXT}
${VOCAB_TEXT}
Expand Down
19 changes: 15 additions & 4 deletions src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,20 @@ export const DOMAIN_RULES = [
{ o: "fd", kw: ["client", "outreach", "fundrais", "recruit", "hiring", "pilot", "sales", "demo", "investor", "contract"] },
];

export const SIZE_PTS = { s: 1, m: 2, l: 4, xl: 8 };
export const SIZE_NAMES = { s: "S", m: "M", l: "L", xl: "XL" };
export const LEAD = { s: 1, m: 3, l: 7, xl: 14 };
export const SIZE_KEYS = ["s", "m", "l", "xl", "xxl"];
export const SIZE_PTS = { s: 1, m: 2, l: 4, xl: 6, xxl: 8 };
export const SIZE_NAMES = { s: "S", m: "M", l: "L", xl: "XL", xxl: "XXL" };
export const LEAD = { s: 1, m: 3, l: 7, xl: 14, xxl: 28 };

/** Map legacy/invalid sizes onto the active scale (same rules for tasks and subtasks). */
export function normalizeSize(size) {
if (size && SIZE_KEYS.includes(size)) return size;
if (size === "xs") return "s";
return "m";
}

export const sizePts = (size) => SIZE_PTS[normalizeSize(size)];
export const barHeight = (size) => GBAR_H[normalizeSize(size)];

export const ZOOMS = [
{ l: "Day", h: 0, v: 3 },
Expand All @@ -47,7 +58,7 @@ export const ZOOMS = [
{ l: "6 weeks", h: 42, v: 42 },
];

export const GBAR_H = { s: 26, m: 34, l: 44, xl: 56 };
export const GBAR_H = { s: 26, m: 34, l: 44, xl: 56, xxl: 68 };
export const R0G = 0;
export const R1G = 90;
export const SPAN_G = R1G - R0G;
Expand Down
Loading
Loading