From f0709aa5e0b4cae6a20e65df215f293165e8d3a2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 19 Jun 2026 07:43:20 +0000 Subject: [PATCH 1/4] Expand task sizes from XS to XXL for bar thickness and duration Add xs and xxl to the size scale (XS through XXL), with matching bar heights, lead times, weight points, UI selectors, and voice parsing. Existing S/M/L/XL values are unchanged. Co-authored-by: Tanops --- index.html | 16 +++++++++++----- src/app/main.js | 25 +++++++++++++------------ src/data/constants.js | 9 +++++---- src/lib/capture.js | 21 +++++++++++++++------ tests/capture.test.js | 2 ++ tests/constants.test.js | 18 ++++++++++++++++++ 6 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 tests/constants.test.js diff --git a/index.html b/index.html index 530b7cb..5218999 100644 --- a/index.html +++ b/index.html @@ -124,10 +124,12 @@ .pill .pop.on{background:var(--green);border-color:var(--green);color:#fff;opacity:1} .pill .pbody{display:inline-flex;align-items:center;gap:7px;color:inherit;font:inherit;min-width:0;padding:0} .pill .pbody:active{opacity:.6} + .pill.sz-xs{font-size:11px; padding:5px 10px} .pill.sz-s {font-size:12px; padding:6px 12px} .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)} @@ -303,14 +305,16 @@ space around it reads the same top, bottom and left regardless of bar height */ .gava{flex:none;position:relative;z-index:2;line-height:0} .gava .av{box-shadow:0 0 0 2px rgba(255,255,255,.92)} + .gbar.gsz-xs .gava .av{width:16px;height:16px;font-size:8px} .gbar.gsz-s .gava .av{width:18px;height:18px;font-size:8.5px} .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-xs{height:18px}.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-xs .ttl{font-size:10px}.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} @@ -319,8 +323,10 @@ display:flex;align-items:center;justify-content:center; opacity:0;width:0;margin:0;border-width:0;transition:opacity .12s} .gbar:hover .gdot{opacity:1;width:16px;border-width:2px;margin-right:0} + .gbar.gsz-xs:hover .gdot{width:12px} .gbar.gsz-s:hover .gdot{width:13px} .gdot.on{opacity:1;width:16px;border-width:2px;background:#fff;border-color:#fff;color:var(--green)} + .gbar.gsz-xs .gdot{width:12px;height:12px} .gbar.gsz-s .gdot{width:13px;height:13px} .gavb{flex:none;position:relative;z-index:2;margin-left:auto;width:18px;height:18px;border-radius:50%; background:#fff;font-size:8.5px;font-weight:800;pointer-events:none; @@ -381,7 +387,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: @@ -474,8 +480,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} diff --git a/src/app/main.js b/src/app/main.js index a128d51..95f5583 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -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, R0G, R1G, SPAN_G, TODAY_PX, C_LATE, C_TODAY, C_RADAR, C_LATER, C_DONE, } from "../data/constants.js"; @@ -867,7 +867,7 @@ function openBarMenu(id,anchor){ BM.innerHTML=`
Owner
${Object.entries(PEOPLE).map(([k,p])=>``).join("")}
-
Size${["s","m","l","xl"].map(z=>``).join("")}
+
Size${SIZE_KEYS.map(z=>``).join("")}
Start
End
`; BM.classList.add("show"); BARMENU=id; @@ -969,7 +969,7 @@ function openDetail(id,opts){ let _szPts=0; flat([n],x=>{ if(x.children.length) return; _szPts+=SIZE_PTS[x.size||"m"]; }); const sizeFld=leaf ? `
Size
` + ${SIZE_KEYS.map(z=>``).join("")}` : `
Size${_szPts} pts
`; document.getElementById("dBody").innerHTML=`
Owner${av(n.owner)}${["s","m","l","xl"].map(z=>``).join("")}` + ? `` : `${_cp} pts`; return `
@@ -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:{ @@ -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})), @@ -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: xs/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. @@ -1338,7 +1339,7 @@ function ownerPill(v,onch){ const col=v?person(v).color:"#c2c8d2"; return ` `; } function duePill(v,onch,sm){ return ``; } -function szSeg(v,onch){ return `${["s","m","l","xl"].map(z=> +function szSeg(v,onch){ return `${SIZE_KEYS.map(z=> ``).join("")}`; } function taskCardHTML(tk,i){ tk.subs=tk.subs||[]; return `
@@ -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 (xs/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} diff --git a/src/data/constants.js b/src/data/constants.js index cc8ac4f..426ce25 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -36,9 +36,10 @@ 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 = ["xs", "s", "m", "l", "xl", "xxl"]; +export const SIZE_PTS = { xs: 1, s: 1, m: 2, l: 4, xl: 8, xxl: 16 }; +export const SIZE_NAMES = { xs: "XS", s: "S", m: "M", l: "L", xl: "XL", xxl: "XXL" }; +export const LEAD = { xs: 1, s: 1, m: 3, l: 7, xl: 14, xxl: 28 }; export const ZOOMS = [ { l: "Day", h: 0, v: 3 }, @@ -47,7 +48,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 = { xs: 18, s: 26, m: 34, l: 44, xl: 56, xxl: 68 }; export const R0G = 0; export const R1G = 90; export const SPAN_G = R1G - R0G; diff --git a/src/lib/capture.js b/src/lib/capture.js index 8051a0b..4d2bcc8 100644 --- a/src/lib/capture.js +++ b/src/lib/capture.js @@ -61,18 +61,27 @@ export function findDue(t, today = TODAY) { } export function findSize(t) { - const m = t.match(/\b(extra large|x-?large|xl|small|medium|large)\b/); + const m = t.match(/\b(extra extra large|xx-?large|xxl|extra large|x-?large|xl|extra small|x-?small|xs|small|medium|large)\b/i); if (!m) return null; + const key = m[1].toLowerCase().replace(/\s+/g, " "); return ( { + "extra extra large": "xxl", + "xx-large": "xxl", + xxlarge: "xxl", + xxl: "xxl", + "extra large": "xl", + "x-large": "xl", + xlarge: "xl", + xl: "xl", + "extra small": "xs", + "x-small": "xs", + xsmall: "xs", + xs: "xs", small: "s", medium: "m", large: "l", - xl: "xl", - "x-large": "xl", - xlarge: "xl", - "extra large": "xl", - }[m[1]] || null + }[key] || null ); } diff --git a/tests/capture.test.js b/tests/capture.test.js index 4c03dd4..054125f 100644 --- a/tests/capture.test.js +++ b/tests/capture.test.js @@ -38,6 +38,8 @@ Install RS03 motors for JCDecaux.`; it("parses t-shirt sizes", () => { expect(findSize("this is a large task")).toBe("l"); expect(findSize("extra large integration")).toBe("xl"); + expect(findSize("extra small fix")).toBe("xs"); + expect(findSize("xxl platform rollout")).toBe("xxl"); }); it("extracts projects and tasks from a conversation offline", () => { diff --git a/tests/constants.test.js b/tests/constants.test.js new file mode 100644 index 0000000..83b4d9b --- /dev/null +++ b/tests/constants.test.js @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { GBAR_H, LEAD, SIZE_KEYS, SIZE_NAMES, SIZE_PTS } from "../src/data/constants.js"; + +describe("size constants", () => { + it("defines XS through XXL with increasing bar height and lead time", () => { + expect(SIZE_KEYS).toEqual(["xs", "s", "m", "l", "xl", "xxl"]); + for (const k of SIZE_KEYS) { + expect(SIZE_NAMES[k]).toBeTruthy(); + expect(SIZE_PTS[k]).toBeGreaterThan(0); + expect(GBAR_H[k]).toBeGreaterThan(0); + expect(LEAD[k]).toBeGreaterThan(0); + } + expect(GBAR_H.xs).toBeLessThan(GBAR_H.s); + expect(GBAR_H.xxl).toBeGreaterThan(GBAR_H.xl); + expect(LEAD.xs).toBeLessThanOrEqual(LEAD.s); + expect(LEAD.xxl).toBeGreaterThan(LEAD.xl); + }); +}); From 075601cbeaaf718409eb9733664ddd34d7004732 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 19 Jun 2026 08:10:21 +0000 Subject: [PATCH 2/4] =?UTF-8?q?Adjust=20size=20scale=20to=20S=E2=80=93XXL?= =?UTF-8?q?=20with=20updated=20point=20weights?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop XS and set points to S=1, M=2, L=4, XL=6, XXL=8. Map legacy xs sizes to S on board load and in voice parsing. Co-authored-by: Tanops --- index.html | 8 ++------ src/app/main.js | 4 ++-- src/data/constants.js | 10 +++++----- src/lib/capture.js | 8 ++++---- src/lib/tree.js | 1 + tests/capture.test.js | 2 +- tests/constants.test.js | 12 +++++------- tests/tree.test.js | 6 ++++++ 8 files changed, 26 insertions(+), 25 deletions(-) diff --git a/index.html b/index.html index 5218999..be94bc5 100644 --- a/index.html +++ b/index.html @@ -124,7 +124,6 @@ .pill .pop.on{background:var(--green);border-color:var(--green);color:#fff;opacity:1} .pill .pbody{display:inline-flex;align-items:center;gap:7px;color:inherit;font:inherit;min-width:0;padding:0} .pill .pbody:active{opacity:.6} - .pill.sz-xs{font-size:11px; padding:5px 10px} .pill.sz-s {font-size:12px; padding:6px 12px} .pill.sz-m {font-size:13.5px;padding:9px 16px} .pill.sz-l {font-size:15.5px;padding:13px 22px} @@ -305,16 +304,15 @@ space around it reads the same top, bottom and left regardless of bar height */ .gava{flex:none;position:relative;z-index:2;line-height:0} .gava .av{box-shadow:0 0 0 2px rgba(255,255,255,.92)} - .gbar.gsz-xs .gava .av{width:16px;height:16px;font-size:8px} .gbar.gsz-s .gava .av{width:18px;height:18px;font-size:8.5px} .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-xxl .gava .av{width:54px;height:54px;font-size:19px} - .gbar.gsz-xs{height:18px}.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.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-xs .ttl{font-size:10px}.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} + .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} @@ -323,10 +321,8 @@ display:flex;align-items:center;justify-content:center; opacity:0;width:0;margin:0;border-width:0;transition:opacity .12s} .gbar:hover .gdot{opacity:1;width:16px;border-width:2px;margin-right:0} - .gbar.gsz-xs:hover .gdot{width:12px} .gbar.gsz-s:hover .gdot{width:13px} .gdot.on{opacity:1;width:16px;border-width:2px;background:#fff;border-color:#fff;color:var(--green)} - .gbar.gsz-xs .gdot{width:12px;height:12px} .gbar.gsz-s .gdot{width:13px;height:13px} .gavb{flex:none;position:relative;z-index:2;margin-left:auto;width:18px;height:18px;border-radius:50%; background:#fff;font-size:8.5px;font-weight:800;pointer-events:none; diff --git a/src/app/main.js b/src/app/main.js index 95f5583..1d06647 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -1159,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: xs/s/m/l/xl/xxl 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. @@ -1430,7 +1430,7 @@ 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 (xs/s/m/l/xl/xxl 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} diff --git a/src/data/constants.js b/src/data/constants.js index 426ce25..27fd1de 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -36,10 +36,10 @@ export const DOMAIN_RULES = [ { o: "fd", kw: ["client", "outreach", "fundrais", "recruit", "hiring", "pilot", "sales", "demo", "investor", "contract"] }, ]; -export const SIZE_KEYS = ["xs", "s", "m", "l", "xl", "xxl"]; -export const SIZE_PTS = { xs: 1, s: 1, m: 2, l: 4, xl: 8, xxl: 16 }; -export const SIZE_NAMES = { xs: "XS", s: "S", m: "M", l: "L", xl: "XL", xxl: "XXL" }; -export const LEAD = { xs: 1, s: 1, m: 3, l: 7, xl: 14, xxl: 28 }; +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 }; export const ZOOMS = [ { l: "Day", h: 0, v: 3 }, @@ -48,7 +48,7 @@ export const ZOOMS = [ { l: "6 weeks", h: 42, v: 42 }, ]; -export const GBAR_H = { xs: 18, s: 26, m: 34, l: 44, xl: 56, xxl: 68 }; +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; diff --git a/src/lib/capture.js b/src/lib/capture.js index 4d2bcc8..fa5cb05 100644 --- a/src/lib/capture.js +++ b/src/lib/capture.js @@ -74,10 +74,10 @@ export function findSize(t) { "x-large": "xl", xlarge: "xl", xl: "xl", - "extra small": "xs", - "x-small": "xs", - xsmall: "xs", - xs: "xs", + "extra small": "s", + "x-small": "s", + xsmall: "s", + xs: "s", small: "s", medium: "m", large: "l", diff --git a/src/lib/tree.js b/src/lib/tree.js index 6d3e155..9868c69 100644 --- a/src/lib/tree.js +++ b/src/lib/tree.js @@ -42,6 +42,7 @@ export function findPath(id, nodes, path = []) { export function normalizeTaskTree(nodes) { nodes.forEach((n) => { if (!Array.isArray(n.children)) n.children = []; + if (n.size === "xs") n.size = "s"; normalizeTaskTree(n.children); }); } diff --git a/tests/capture.test.js b/tests/capture.test.js index 054125f..9933364 100644 --- a/tests/capture.test.js +++ b/tests/capture.test.js @@ -38,7 +38,7 @@ Install RS03 motors for JCDecaux.`; it("parses t-shirt sizes", () => { expect(findSize("this is a large task")).toBe("l"); expect(findSize("extra large integration")).toBe("xl"); - expect(findSize("extra small fix")).toBe("xs"); + expect(findSize("extra small fix")).toBe("s"); expect(findSize("xxl platform rollout")).toBe("xxl"); }); diff --git a/tests/constants.test.js b/tests/constants.test.js index 83b4d9b..7699ef2 100644 --- a/tests/constants.test.js +++ b/tests/constants.test.js @@ -2,17 +2,15 @@ import { describe, expect, it } from "vitest"; import { GBAR_H, LEAD, SIZE_KEYS, SIZE_NAMES, SIZE_PTS } from "../src/data/constants.js"; describe("size constants", () => { - it("defines XS through XXL with increasing bar height and lead time", () => { - expect(SIZE_KEYS).toEqual(["xs", "s", "m", "l", "xl", "xxl"]); + it("defines S through XXL with the requested point weights", () => { + expect(SIZE_KEYS).toEqual(["s", "m", "l", "xl", "xxl"]); + expect(SIZE_PTS).toEqual({ s: 1, m: 2, l: 4, xl: 6, xxl: 8 }); for (const k of SIZE_KEYS) { expect(SIZE_NAMES[k]).toBeTruthy(); - expect(SIZE_PTS[k]).toBeGreaterThan(0); expect(GBAR_H[k]).toBeGreaterThan(0); expect(LEAD[k]).toBeGreaterThan(0); } - expect(GBAR_H.xs).toBeLessThan(GBAR_H.s); - expect(GBAR_H.xxl).toBeGreaterThan(GBAR_H.xl); - expect(LEAD.xs).toBeLessThanOrEqual(LEAD.s); - expect(LEAD.xxl).toBeGreaterThan(LEAD.xl); + expect(GBAR_H.s).toBeLessThan(GBAR_H.xxl); + expect(LEAD.s).toBeLessThanOrEqual(LEAD.xxl); }); }); diff --git a/tests/tree.test.js b/tests/tree.test.js index 5c4e754..26ee707 100644 --- a/tests/tree.test.js +++ b/tests/tree.test.js @@ -57,4 +57,10 @@ describe("tree", () => { expect(leaf.children).toEqual([]); expect(findPath(1, [root]).map((n) => n.title)).toEqual(["Root", "Leaf"]); }); + + it("migrates legacy xs size to s", () => { + const root = { id: 1, title: "Root", owner: "fd", size: "xs", children: [] }; + normalizeTaskTree([root]); + expect(root.size).toBe("s"); + }); }); From a357374e4fecae3dec17825003bc807a468f19b9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 19 Jun 2026 08:23:10 +0000 Subject: [PATCH 3/4] =?UTF-8?q?Apply=20unified=20S=E2=80=93XXL=20sizing=20?= =?UTF-8?q?to=20tasks=20and=20subtasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add normalizeSize, sizePts, and barHeight helpers for one sizing scale - Use the same bar height and point weights in Gantt for tasks and subtasks - Remove CSS overrides that forced subtask bars to a fixed small size - Show point values in subtask size selectors and normalize persisted sizes Co-authored-by: Tanops --- index.html | 4 ++-- src/app/main.js | 24 ++++++++++++------------ src/data/constants.js | 10 ++++++++++ src/lib/tree.js | 4 +++- tests/constants.test.js | 28 +++++++++++++++++++++++++++- 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index be94bc5..ccead89 100644 --- a/index.html +++ b/index.html @@ -335,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} diff --git a/src/app/main.js b/src/app/main.js index 1d06647..a56fe06 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -1,6 +1,6 @@ import { PEOPLE, TODAY, HARDWARE_VOCAB, CLIENTS, - SIZE_KEYS, 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"; @@ -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":""; @@ -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)=>`
@@ -426,8 +426,8 @@ function renderGantt(){ if(!n.due) return ""; const {s,e}=spanFor(n), done=isSub?n.done:taskDone(n); if(eR1G) 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; @@ -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(`
@@ -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('
'); @@ -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; @@ -966,10 +966,10 @@ function openDetail(id,opts){ mopts.push(``); }); // 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 ? `
Size
` + ${SIZE_KEYS.map(z=>``).join("")}
` : `
Size${_szPts} pts
`; document.getElementById("dBody").innerHTML=`
Owner${av(n.owner)}${mopts.join("")}
${path.length>=3?"":`
${path.length>1?"Subtasks":"Tasks — grip ⠿ to drag onto another project"}
`} ${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 - ? `` + ? `` : `${_cp} pts`; return `
diff --git a/src/data/constants.js b/src/data/constants.js index 27fd1de..e4f8081 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -41,6 +41,16 @@ 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 }, { l: "Week", h: 7, v: 7 }, diff --git a/src/lib/tree.js b/src/lib/tree.js index 9868c69..2884123 100644 --- a/src/lib/tree.js +++ b/src/lib/tree.js @@ -1,3 +1,5 @@ +import { normalizeSize } from "../data/constants.js"; + export function createTaskFactory() { let uid = 0; const T = (title, o, opts = {}) => ({ @@ -42,7 +44,7 @@ export function findPath(id, nodes, path = []) { export function normalizeTaskTree(nodes) { nodes.forEach((n) => { if (!Array.isArray(n.children)) n.children = []; - if (n.size === "xs") n.size = "s"; + if (n.size != null) n.size = normalizeSize(n.size); normalizeTaskTree(n.children); }); } diff --git a/tests/constants.test.js b/tests/constants.test.js index 7699ef2..539deb4 100644 --- a/tests/constants.test.js +++ b/tests/constants.test.js @@ -1,5 +1,14 @@ import { describe, expect, it } from "vitest"; -import { GBAR_H, LEAD, SIZE_KEYS, SIZE_NAMES, SIZE_PTS } from "../src/data/constants.js"; +import { + barHeight, + GBAR_H, + LEAD, + normalizeSize, + SIZE_KEYS, + SIZE_NAMES, + SIZE_PTS, + sizePts, +} from "../src/data/constants.js"; describe("size constants", () => { it("defines S through XXL with the requested point weights", () => { @@ -13,4 +22,21 @@ describe("size constants", () => { expect(GBAR_H.s).toBeLessThan(GBAR_H.xxl); expect(LEAD.s).toBeLessThanOrEqual(LEAD.xxl); }); + + it("normalizes legacy and missing sizes the same for tasks and subtasks", () => { + expect(normalizeSize("l")).toBe("l"); + expect(normalizeSize("xs")).toBe("s"); + expect(normalizeSize(null)).toBe("m"); + expect(normalizeSize(undefined)).toBe("m"); + }); + + it("uses the same point and bar-height scale for every item", () => { + for (const k of SIZE_KEYS) { + expect(sizePts(k)).toBe(SIZE_PTS[k]); + expect(barHeight(k)).toBe(GBAR_H[k]); + } + expect(sizePts(null)).toBe(2); + expect(barHeight("xs")).toBe(GBAR_H.s); + expect(barHeight("m")).toBe(barHeight(null)); + }); }); From 5ab8244c151506fb62774d17ba91cf90f3011c89 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 19 Jun 2026 10:48:11 +0000 Subject: [PATCH 4/4] Document PR #13 rebase onto main Co-authored-by: Sanket Sharma --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f11881..cc2bdf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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