create-pmos 0.124.0 → 0.126.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/nine-stages.js +81 -0
- package/payload/scripts/workflow-state.js +2 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-one-home-test.js +131 -0
- package/workspace/fixtures/initiatives-api/list.json +171 -3
- package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
- package/workspace/fixtures/initiatives-api/sheet.json +57 -1
- package/workspace/fixtures/one-home-baseline.json +83 -0
- package/workspace/guide/briefing-cards.md +40 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/layout.md +2 -0
- package/workspace/lib/engine/nine-stages.js +81 -0
- package/workspace/lib/engine/workflow-state.js +2 -0
- package/workspace/lib/facts-async.js +51 -0
- package/workspace/lib/item-steps.js +52 -0
- package/workspace/lib/jira-status.js +8 -3
- package/workspace/lib/jobs-to-be-done.js +52 -0
- package/workspace/lib/needs-overdue.js +41 -0
- package/workspace/lib/outbox-store.js +87 -0
- package/workspace/lib/pm-items.js +115 -0
- package/workspace/lib/route-modules.js +21 -0
- package/workspace/lib/routes-briefing.js +109 -0
- package/workspace/lib/routes-c3-init.js +53 -0
- package/workspace/lib/routes-c3-life.js +119 -0
- package/workspace/lib/routes-c3-strategy.js +169 -0
- package/workspace/lib/routes-c3-timeline.js +191 -0
- package/workspace/lib/routes-calendar-import.js +60 -0
- package/workspace/lib/routes-calendar-views.js +61 -0
- package/workspace/lib/routes-doc-health.js +79 -0
- package/workspace/lib/routes-doc-sheet.js +169 -0
- package/workspace/lib/routes-forecast.js +85 -0
- package/workspace/lib/routes-hooks.js +121 -0
- package/workspace/lib/routes-initiatives.js +3 -0
- package/workspace/lib/routes-intake.js +24 -0
- package/workspace/lib/routes-outbox.js +97 -0
- package/workspace/lib/routes-pm-items.js +216 -0
- package/workspace/lib/routes-records.js +61 -0
- package/workspace/lib/routes-releases.js +129 -0
- package/workspace/lib/routes-report-facts.js +84 -0
- package/workspace/lib/routes-report-families.js +32 -0
- package/workspace/lib/routes-roadmap-ops.js +141 -0
- package/workspace/lib/routes-runs.js +119 -0
- package/workspace/lib/routes-signals.js +50 -0
- package/workspace/lib/run-answers.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +26 -0
- package/workspace/public/pm/briefing-deck.js +413 -0
- package/workspace/public/pm/briefing-model.js +211 -0
- package/workspace/public/pm/c3-breakdown.js +240 -0
- package/workspace/public/pm/c3-brief.js +278 -0
- package/workspace/public/pm/c3-cal-strategy.js +528 -0
- package/workspace/public/pm/c3-case.js +185 -0
- package/workspace/public/pm/c3-daypart.js +176 -0
- package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
- package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
- package/workspace/public/pm/c3-demo-cal2.js +66 -0
- package/workspace/public/pm/c3-demo-cal3.js +173 -0
- package/workspace/public/pm/c3-demo-data.js +560 -0
- package/workspace/public/pm/c3-demo-init.js +373 -0
- package/workspace/public/pm/c3-demo-life.js +77 -0
- package/workspace/public/pm/c3-demo-misc2.js +46 -0
- package/workspace/public/pm/c3-demo-plan2.js +30 -0
- package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
- package/workspace/public/pm/c3-demo-rep2.js +92 -0
- package/workspace/public/pm/c3-demo-term.js +153 -0
- package/workspace/public/pm/c3-demo-time.js +49 -0
- package/workspace/public/pm/c3-demo-work-ix.js +80 -0
- package/workspace/public/pm/c3-demo-work2.js +13 -0
- package/workspace/public/pm/c3-demo.js +113 -0
- package/workspace/public/pm/c3-doc-sheet.js +314 -0
- package/workspace/public/pm/c3-dock.js +180 -0
- package/workspace/public/pm/c3-docs.js +290 -0
- package/workspace/public/pm/c3-expand.js +96 -0
- package/workspace/public/pm/c3-init-extras.js +170 -0
- package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
- package/workspace/public/pm/c3-misc-panels.js +89 -0
- package/workspace/public/pm/c3-plan-panels.js +163 -0
- package/workspace/public/pm/c3-plan.js +192 -0
- package/workspace/public/pm/c3-roadmap.js +249 -0
- package/workspace/public/pm/c3-sheet.js +107 -0
- package/workspace/public/pm/c3-shell.js +674 -0
- package/workspace/public/pm/c3-signals.js +196 -0
- package/workspace/public/pm/c3-strategy-panels.js +45 -0
- package/workspace/public/pm/c3-swipe.js +230 -0
- package/workspace/public/pm/c3-terminal-data.js +26 -0
- package/workspace/public/pm/c3-terminal-edge.js +91 -0
- package/workspace/public/pm/c3-terminal-icons.js +41 -0
- package/workspace/public/pm/c3-terminal-results.js +141 -0
- package/workspace/public/pm/c3-terminal.js +52 -0
- package/workspace/public/pm/c3-tickets.js +163 -0
- package/workspace/public/pm/c3-timeline.js +201 -0
- package/workspace/public/pm/c3-toast.js +16 -0
- package/workspace/public/pm/calendar-views.js +1014 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1547 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- package/workspace/public/pm/dest-reports.js +47 -1
- package/workspace/public/pm/dest-today.js +55 -1
- package/workspace/public/pm/doc-health-panel.js +110 -0
- package/workspace/public/pm/doc-rail.js +9 -3
- package/workspace/public/pm/lifecycle-lens-model.js +228 -0
- package/workspace/public/pm/lifecycle-lens.js +261 -0
- package/workspace/public/pm/new-initiative-sheet.js +116 -21
- package/workspace/public/pm/outbox-panel.js +125 -0
- package/workspace/public/pm/pm-work-view.js +895 -0
- package/workspace/public/pm/releases-view.js +248 -0
- package/workspace/public/pm/report-families-view.js +503 -0
- package/workspace/public/pm/run-card.js +244 -0
- package/workspace/public/pm/sessions-panel.js +149 -0
- package/workspace/public/pm/shell-bell.js +162 -0
- package/workspace/public/pm/shell-model.js +83 -0
- package/workspace/public/pm/shell-phone.js +124 -0
- package/workspace/public/pm/shell-readaloud.js +53 -0
- package/workspace/public/pm/shell-recording.js +110 -0
- package/workspace/public/pm/shell-ticker.js +90 -0
- package/workspace/public/pm/slide-confirm.js +163 -0
- package/workspace/public/pm/timeline-forecast-view.js +164 -0
- package/workspace/public/pm/today-day.js +8 -2
- package/workspace/public/pm/tokens.css +1 -0
- package/workspace/server.js +5 -2
- package/workspace/test-files.json +21 -3
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/* pm/c3-timeline.js — the drawing of the Initiatives Timeline of /c3.html, ported from the console3 mockup (console3.html, patch21, lines 4014 to 4042):
|
|
2
|
+
* scrubHtml (the track of checks and Replay), ganttTix (the Gantt), timeRight (What changed), driftSVG and creepSVG (forecast and scope at each check),
|
|
3
|
+
* fcMatrix (the forecast of every initiative at every check) and calSyncCard (the dates that reach the Calendar).
|
|
4
|
+
*
|
|
5
|
+
* INPUT is one payload of GET /api/c3-timeline (workspace/lib/routes-c3-timeline.js): `frames`, one whole plan per check. Every function here takes a
|
|
6
|
+
* frame (or the frames) and returns DOM; nothing is fetched and nothing is written. A replay is a loop over frames, so the Gantt is redrawn from the
|
|
7
|
+
* frame of each check: bars move, forecast and slip change, items appear as they were added. The mockup folds ticket histories in the browser (tixAt,
|
|
8
|
+
* planOf); here the server has already folded the events and planned them with plan.js, so the same drawing reads the result.
|
|
9
|
+
* DIFFERENCES FROM THE MOCKUP, kept honest: the mockup's tickets carry a typed "critical" flag and a progress number; here the critical path is derived
|
|
10
|
+
* (slack 0 in plan.js) and progress comes from the item's state. Origin (Jira, Local, Figma, Calendar, Plan) is in each row's tooltip and in What
|
|
11
|
+
* changed; the mockup draws no origin chip inside the Gantt either (its chips are in the Breakdown rows).
|
|
12
|
+
*/
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
import { c3ic } from './pm-work-view.js';
|
|
15
|
+
|
|
16
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
17
|
+
const DAY = 86400000;
|
|
18
|
+
export const dnum = (s) => Math.round(Date.parse(String(s).slice(0, 10) + 'T00:00:00Z') / DAY);
|
|
19
|
+
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
20
|
+
export const dlab = (n) => { const d = new Date(n * DAY); return d.getUTCDate() + ' ' + MON[d.getUTCMonth()]; };
|
|
21
|
+
export const dlabS = (s) => dlab(dnum(s));
|
|
22
|
+
export const short = (t, n) => { t = String(t == null ? '' : t); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
23
|
+
const STATUS = { done: ['Done', 'var(--success)'], 'on time': ['On time', 'var(--success)'], 'at risk': ['At risk', 'var(--warning)'], delayed: ['Delayed', 'var(--destructive)'], overdue: ['Overdue', 'var(--destructive)'] };
|
|
24
|
+
const stat = (s) => STATUS[s] || STATUS['on time'];
|
|
25
|
+
|
|
26
|
+
function svg(tag, attrs, ...kids) {
|
|
27
|
+
const n = document.createElementNS(NS, tag);
|
|
28
|
+
for (const k of Object.keys(attrs || {})) if (attrs[k] != null) n.setAttribute(k, attrs[k]);
|
|
29
|
+
for (const c of kids) if (c) n.appendChild(c);
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
32
|
+
const text = (x, y, s, attrs) => { const t = svg('text', { x, y, ...(attrs || {}) }); t.textContent = s; return t; };
|
|
33
|
+
const title = (s) => { const t = svg('title', {}); t.textContent = s; return t; };
|
|
34
|
+
|
|
35
|
+
/** Who changed it, as the mockup's orig(): You, Agent, or the teammate. */
|
|
36
|
+
export function whoOf(by) {
|
|
37
|
+
const b = String(by || '');
|
|
38
|
+
if (/^(agent|claude-code)/i.test(b)) return { l: 'Agent', c: 'var(--opp, var(--accent))', i: 'spark' };
|
|
39
|
+
if (/^(pm:|human:wawan)/i.test(b) || b === 'Wawan' || b === 'You') return { l: 'You', c: 'var(--acc)', i: 'user' };
|
|
40
|
+
return { l: b.replace(/^(pm|human|agent):/, '') || 'Someone', c: 'var(--muted-foreground)', i: 'user' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A key a person would say out loud: the Jira key or PM-n, else the title alone. */
|
|
44
|
+
const rowName = (t) => (t.key ? t.key + ' ' + short(t.title, 17) : short(t.title || t.id, 22));
|
|
45
|
+
|
|
46
|
+
/** The mockup's ganttTix. frame: one entry of `frames`; prev: the frame before it (the dashed "where it was before"); hl: ids touched at this check. */
|
|
47
|
+
export function ganttSvg(frame, prev, hl, today) {
|
|
48
|
+
const items = frame.items.slice().sort((a, b) => dnum(a.start) - dnum(b.start) || (a.id < b.id ? -1 : 1));
|
|
49
|
+
const crit = new Set(frame.critical || []);
|
|
50
|
+
const ghost = new Map((prev ? prev.items : []).map((t) => [t.id, t.forecast]));
|
|
51
|
+
const idx = {}; items.forEach((t, i) => { idx[t.id] = i; });
|
|
52
|
+
const days = [];
|
|
53
|
+
for (const t of items) days.push(dnum(t.start), dnum(t.due), dnum(t.forecast.end));
|
|
54
|
+
const a = Math.min(...days) - 1; const b = Math.max(...days) + 2;
|
|
55
|
+
const W = 720; const LW = 176; const SW = 76; const CW = W - LW - SW; const N = b - a + 1; const dw = CW / N; const rh = 30; const top = 26; const H = top + items.length * rh + 10;
|
|
56
|
+
const X = (n) => LW + (n - a) * dw; const Xc = (n) => X(n) + dw / 2; const cy = (i) => top + i * rh + rh / 2;
|
|
57
|
+
const s = svg('svg', { class: 'pg', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Ticket timeline with dependencies and the critical path', 'data-tl-gantt': '', 'data-tl-check': frame.check });
|
|
58
|
+
for (let n = a; n <= b; n++) if (new Date(n * DAY).getUTCDay() === 1) { s.appendChild(svg('line', { x1: X(n), x2: X(n), y1: 18, y2: H - 4, stroke: 'var(--border)' })); s.appendChild(text(X(n) + 3, 14, dlab(n))); }
|
|
59
|
+
const td = dnum(today);
|
|
60
|
+
if (td >= a && td <= b) s.appendChild(svg('rect', { x: X(td), y: 20, width: dw, height: H - 24, fill: 'var(--accw)', rx: 6 }));
|
|
61
|
+
if (frame.promised) { const px = Xc(dnum(frame.promised)); s.appendChild(svg('line', { x1: px, x2: px, y1: 20, y2: H - 4, stroke: 'var(--success)', 'stroke-dasharray': '4 4', 'stroke-width': 2, 'data-tl-promised': frame.promised })); s.appendChild(text(px + 4, H - 8, 'promised', { fill: 'var(--success)' })); }
|
|
62
|
+
items.forEach((t, i) => (t.after || []).forEach((dk) => {
|
|
63
|
+
if (idx[dk] == null) return;
|
|
64
|
+
const pd = items[idx[dk]].forecast; const pt = t.forecast;
|
|
65
|
+
const x1 = X(dnum(pd.end) + 1); const y1 = cy(idx[dk]); const x2 = Math.max(X(dnum(pt.start)), x1 + 2); const y2 = cy(i); const xm = x1 + 6; const c = crit.has(t.id) && crit.has(dk);
|
|
66
|
+
s.appendChild(svg('path', { d: `M${x1} ${y1} H${xm} V${y2} H${x2 - 1}`, fill: 'none', stroke: c ? 'var(--destructive)' : 'var(--ring)', 'stroke-width': c ? 2 : 1.2, 'data-tl-dep': dk + '>' + t.id }));
|
|
67
|
+
s.appendChild(svg('path', { d: `M${x2} ${y2} l-5 -3 v6z`, fill: c ? 'var(--destructive)' : 'var(--ring)' }));
|
|
68
|
+
}));
|
|
69
|
+
items.forEach((t, i) => {
|
|
70
|
+
const p = t.forecast; const st = stat(p.status); const y = cy(i); const done = t.state === 'done'; const xs = X(dnum(t.start)); const xe = X(dnum(t.due) + 1); const g = ghost.get(t.id);
|
|
71
|
+
if (hl && hl.has(t.id)) s.appendChild(svg('rect', { x: 0, y: y - 13, width: W, height: 26, rx: 8, fill: 'var(--accw)', 'data-tl-hl': t.id }));
|
|
72
|
+
if (g && (g.end !== p.end || g.start !== p.start)) s.appendChild(svg('rect', { x: X(dnum(g.start)), y: y - 10, width: Math.max(4, X(dnum(g.end) + 1) - X(dnum(g.start))), height: 4, rx: 2, fill: 'none', stroke: 'var(--muted-foreground)', 'stroke-dasharray': '3 2', 'data-tl-ghost': t.id }));
|
|
73
|
+
const grp = svg('g', { opacity: done ? '.6' : '1', 'data-tl-row': t.id, 'data-tl-origin': t.origin || null, 'data-critical': crit.has(t.id) ? 'true' : null });
|
|
74
|
+
grp.appendChild(title(`${t.title || t.id} (${t.src || t.origin || 'plan'}${t.phase ? ', ' + t.phase : ''}${t.owner ? ', ' + t.owner : ''}), forecast ${p.start} to ${p.end}: ${st[0]}${p.slip_days > 0 ? ', ' + p.slip_days + ' days past the plan' : ''}`));
|
|
75
|
+
grp.appendChild(text(0, y + 4, (crit.has(t.id) ? '● ' : '') + rowName(t), { class: 't' + (crit.has(t.id) ? ' crit' : '') }));
|
|
76
|
+
if (t.ms) grp.appendChild(svg('path', { d: `M${Xc(dnum(t.due))} ${y - 9} l8 9 -8 9 -8 -9z`, fill: 'var(--card)', stroke: st[1], 'stroke-width': 2 }));
|
|
77
|
+
else {
|
|
78
|
+
grp.appendChild(svg('rect', { x: xs, y: y - 5, width: Math.max(4, xe - xs), height: 10, rx: 5, fill: st[1], 'fill-opacity': '.2' }));
|
|
79
|
+
const frac = done ? 1 : Math.max(0, Math.min(1, t.progress || 0));
|
|
80
|
+
if (frac > 0) grp.appendChild(svg('rect', { x: xs, y: y - 5, width: Math.max(done ? 4 : 0, (xe - xs) * frac), height: 10, rx: 5, fill: st[1] }));
|
|
81
|
+
}
|
|
82
|
+
if (!done && (p.end > t.due || p.start > t.start)) {
|
|
83
|
+
grp.appendChild(svg('rect', { x: X(dnum(p.start)), y: y + 6, width: Math.max(4, X(dnum(p.end) + 1) - X(dnum(p.start))), height: 4, rx: 2, fill: st[1], 'fill-opacity': '.7', 'data-tl-forecast': t.id }));
|
|
84
|
+
if (p.end > t.due) grp.appendChild(svg('circle', { cx: Xc(dnum(p.end)), cy: y, r: 5, fill: 'var(--destructive)', stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
85
|
+
}
|
|
86
|
+
grp.appendChild(text(W - SW + 6, y + 4, done ? 'Done' : p.slip_days > 0 ? '+' + p.slip_days + ' d' : st[0], { style: 'fill:' + st[1], 'data-tl-status': t.id }));
|
|
87
|
+
s.appendChild(grp);
|
|
88
|
+
});
|
|
89
|
+
return s;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The mockup's scrubHtml: the header (As of …, Replay) and the track of checks. */
|
|
93
|
+
export function scrub(frames, n, h) {
|
|
94
|
+
const last = frames.length - 1;
|
|
95
|
+
const f = frames[n];
|
|
96
|
+
const dateLab = dlabS(f.date);
|
|
97
|
+
const track = el('div', { class: 'sntrack', role: 'group', 'aria-label': 'Timeline history', style: `grid-template-columns:repeat(${frames.length},minmax(0,1fr));--n:${frames.length}`, 'data-n': frames.length },
|
|
98
|
+
frames.map((fr, i) => el('button', { type: 'button', class: 'sn', 'aria-pressed': String(i === n), 'data-past': String(i <= n), 'data-tl-snap': i, onClick: () => h.snap(i) },
|
|
99
|
+
el('i'), el('b', null, dlabS(fr.date)), el('span', null, fr.label))));
|
|
100
|
+
const play = el('button', { type: 'button', class: 'ib sm', title: 'Replay how the plan changed', 'aria-label': 'Replay how the plan changed', 'data-tl-replay': '', onClick: () => h.replay() }, c3ic('next', 'sm'));
|
|
101
|
+
return el('div', null, el('div', { class: 'snh' }, el('span', { class: 'dim', style: 'font-size:var(--fs-2)', 'data-tl-asof': '' }, n === last ? 'Today' : 'As of ' + dateLab), play), track);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The Timeline history card: header, the track, the Gantt, the legend. */
|
|
105
|
+
export function historyCard(frames, n, h, today) {
|
|
106
|
+
const frame = frames[n]; const prev = n > 0 ? frames[n - 1] : null;
|
|
107
|
+
const hl = new Set((frame.events || []).map((e) => e.id));
|
|
108
|
+
return el('section', { class: 'card', 'data-tl-history': '' }, el('h3', null, c3ic('cal'), 'Timeline history ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'scrub or replay how the plan changed')),
|
|
109
|
+
scrub(frames, n, h), frame.items.length ? ganttSvg(frame, prev, hl, today) : el('div', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing was in the plan at this check.'),
|
|
110
|
+
el('div', { class: 'pgl' }, el('span', null, '◆ milestone'), el('span', { style: 'color:var(--destructive)' }, '● critical path'), el('span', { style: 'color:var(--success)' }, '┆ promised'), el('span', null, 'thin bar = forecast'), el('span', null, 'dashed = where it was before')));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const wordsTx = (e) => (e.op === 'add' ? 'added' : e.op === 'remove' ? 'removed' : e.tx || 'edited');
|
|
114
|
+
/** The mockup's timeRight, first card: what changed at this check, who, and why. */
|
|
115
|
+
export function whatChangedCard(frames, n) {
|
|
116
|
+
const f = frames[n]; const p = n > 0 ? frames[n - 1] : null;
|
|
117
|
+
const head = n === 0 ? `Baseline: ${f.items.length} item${f.items.length === 1 ? '' : 's'}, ${f.points} points${f.promised ? ', promised ' + dlabS(f.promised) : ''}`
|
|
118
|
+
: `Since ${dlabS(p.date)}: ${f.points - p.points > 0 ? '+' + (f.points - p.points) + ' points' : f.points - p.points < 0 ? (f.points - p.points) + ' points' : 'no scope change'}${f.forecast_end && p.forecast_end && f.forecast_end !== p.forecast_end ? ', forecast ' + dlabS(p.forecast_end) + ' to ' + dlabS(f.forecast_end) : ', forecast unchanged'}`;
|
|
119
|
+
const ev = (f.events || []).slice(0, 12);
|
|
120
|
+
const more = (f.events || []).length - ev.length;
|
|
121
|
+
return el('section', { class: 'card', 'data-tl-changed': '' }, el('h3', null, c3ic('clock'), 'What changed ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, dlabS(f.date))),
|
|
122
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)', 'data-tl-head': '' }, head),
|
|
123
|
+
el('ul', { class: 'hx' }, ev.length ? ev.map((e) => { const o = whoOf(e.by);
|
|
124
|
+
return el('li', { 'data-tl-change': e.id }, el('span', { class: 'chip2', style: 'color:' + o.c }, c3ic(o.i, 'sm'), o.l),
|
|
125
|
+
el('span', null, (e.title || e.id) + ': ' + wordsTx(e), e.why ? el('small', null, e.why) : (e.origin ? el('small', null, 'No reason recorded · ' + e.origin) : null)), el('span', { class: 'w' }, e.key || '')); })
|
|
126
|
+
: [el('li', null, el('span'), el('span', null, 'No change at this check.'), el('span'))], more > 0 ? el('li', null, el('span'), el('span', { class: 'dim' }, '+' + more + ' more'), el('span')) : null));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The mockup's driftSVG: the forecast date at each check against the promise. */
|
|
130
|
+
export function driftSvg(frames, n) {
|
|
131
|
+
const W = 560; const H = 160; const x0 = 58; const x1 = W - 20; const y0 = 16; const y1 = H - 30;
|
|
132
|
+
const F2 = frames.map((f) => (f.forecast_end ? { fe: dnum(f.forecast_end), e: f.promised ? dnum(f.promised) : null } : null));
|
|
133
|
+
const have = F2.filter(Boolean);
|
|
134
|
+
const first = have[0];
|
|
135
|
+
const pr = first ? (first.e != null ? first.e : first.fe) : 0;
|
|
136
|
+
const vals = have.map((x) => x.fe);
|
|
137
|
+
const mn = Math.min(pr, ...vals) - 2; const mx = Math.max(pr, ...vals) + 2; const span = Math.max(1, mx - mn);
|
|
138
|
+
const X = (i) => x0 + i * (x1 - x0) / Math.max(1, frames.length - 1); const Y = (v) => y1 - (v - mn) / span * (y1 - y0);
|
|
139
|
+
const s = svg('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Forecast date at each check against the promise', 'data-tl-drift': '' });
|
|
140
|
+
[pr, Math.round((mn + mx) / 2), mx - 1].forEach((v) => { s.appendChild(svg('line', { x1: x0, x2: x1, y1: Y(v), y2: Y(v), stroke: 'var(--border)' })); s.appendChild(text(x0 - 5, Y(v) + 4, dlab(v), { 'text-anchor': 'end' })); });
|
|
141
|
+
if (first && first.e != null) s.appendChild(svg('line', { x1: x0, x2: x1, y1: Y(pr), y2: Y(pr), stroke: 'var(--success)', 'stroke-dasharray': '5 4', 'stroke-width': 2 }));
|
|
142
|
+
const pts = []; F2.forEach((x, i) => { if (x) pts.push(X(i).toFixed(1) + ',' + Y(x.fe).toFixed(1)); });
|
|
143
|
+
s.appendChild(svg('polyline', { points: pts.join(' '), fill: 'none', stroke: 'var(--destructive)', 'stroke-width': 2.5, 'stroke-linejoin': 'round' }));
|
|
144
|
+
F2.forEach((x, i) => { if (!x) return; s.appendChild(svg('circle', { cx: X(i), cy: Y(x.fe), r: i === n ? 6 : 4, fill: x.fe > pr ? 'var(--destructive)' : 'var(--success)', stroke: 'var(--card)', 'stroke-width': 2 })); s.appendChild(text(X(i), H - 12, dlabS(frames[i].date), { 'text-anchor': 'middle' })); });
|
|
145
|
+
if (first && first.e != null) s.appendChild(text(x1, Y(pr) - 6, 'promised ' + dlab(pr), { 'text-anchor': 'end', fill: 'var(--success)' }));
|
|
146
|
+
return s;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The mockup's creepSVG: scope in points at each check against the baseline. */
|
|
150
|
+
export function creepSvg(frames, n) {
|
|
151
|
+
const W = 560; const H = 170; const x0 = 36; const x1 = W - 20; const y0 = 16; const y1 = H - 32;
|
|
152
|
+
const pp = frames.map((f) => f.points); const b0 = pp[0];
|
|
153
|
+
const mx = Math.max(...pp) + 6; const mn = Math.max(0, Math.min(...pp) - 6); const span = Math.max(1, mx - mn);
|
|
154
|
+
const X = (i) => x0 + i * (x1 - x0) / Math.max(1, frames.length - 1); const Y = (v) => y1 - (v - mn) / span * (y1 - y0);
|
|
155
|
+
const s = svg('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Scope in points against the baseline of ' + b0 + ' points', 'data-tl-creep': '' });
|
|
156
|
+
const pts = []; pp.forEach((v, i) => { if (i) pts.push(X(i).toFixed(1) + ',' + Y(pp[i - 1]).toFixed(1)); pts.push(X(i).toFixed(1) + ',' + Y(v).toFixed(1)); });
|
|
157
|
+
const base = []; for (let i = pp.length - 1; i >= 0; i--) base.push(X(i).toFixed(1) + ',' + Y(b0).toFixed(1));
|
|
158
|
+
s.appendChild(svg('polygon', { points: pts.join(' ') + ' ' + base.join(' '), fill: 'var(--destructive)', 'fill-opacity': '.12' }));
|
|
159
|
+
s.appendChild(svg('line', { x1: x0, x2: x1, y1: Y(b0), y2: Y(b0), stroke: 'var(--ring)', 'stroke-dasharray': '5 4', 'stroke-width': 2 }));
|
|
160
|
+
s.appendChild(svg('polyline', { points: pts.join(' '), fill: 'none', stroke: 'var(--destructive)', 'stroke-width': 2.5, 'stroke-linejoin': 'round' }));
|
|
161
|
+
pp.forEach((v, i) => {
|
|
162
|
+
s.appendChild(svg('circle', { cx: X(i), cy: Y(v), r: i === n ? 6 : 4, fill: 'var(--destructive)', stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
163
|
+
s.appendChild(text(X(i), H - 16, dlabS(frames[i].date), { 'text-anchor': 'middle' }));
|
|
164
|
+
if (i && v > pp[i - 1]) {
|
|
165
|
+
const add = (frames[i].events || []).find((e) => e.op === 'add');
|
|
166
|
+
s.appendChild(text(X(i), Y(v) - 10, '+' + (v - pp[i - 1]) + ' ' + short(add ? add.title : '', 16), { 'text-anchor': i === pp.length - 1 ? 'end' : 'middle' }));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
s.appendChild(text(x1, Y(b0) + 14, 'baseline ' + b0 + ' points', { 'text-anchor': 'end' }));
|
|
170
|
+
return s;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The mockup's calSyncCard: milestones and late dates, the ones that reach the Calendar. */
|
|
174
|
+
export function calSyncCard(frame) {
|
|
175
|
+
const list = frame.items.filter((t) => t.state !== 'done' && (t.ms || t.forecast.status !== 'on time')).sort((a, b) => (a.forecast.end < b.forecast.end ? -1 : a.forecast.end > b.forecast.end ? 1 : 0)).slice(0, 6);
|
|
176
|
+
return el('section', { class: 'card', 'data-tl-cal': '' }, el('h3', null, c3ic('cal'), 'On your Calendar ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'dates that reach it')),
|
|
177
|
+
el('ul', { class: 'chgl' }, list.length ? list.map((t) => { const bad = t.forecast.status === 'delayed' || t.forecast.status === 'overdue';
|
|
178
|
+
return el('li', { class: bad ? 'bad' : 'ok' }, c3ic(bad ? 'clock' : 'check', 'sm'), el('span', null, (t.title || t.id) + ' · ' + dlabS(t.forecast.end) + (bad ? ' (plan ' + dlabS(t.due) + ')' : ''))); })
|
|
179
|
+
: [el('li', null, c3ic('check', 'sm'), el('span', null, 'Nothing dated.'))]),
|
|
180
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Milestones and late dates appear in Calendar week and month. Meetings carry their initiative back here.'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The mockup's fcMatrix: the forecast of every initiative at every check; a cell replays that check. */
|
|
184
|
+
export function matrixCard(data, titles, onCell) {
|
|
185
|
+
const checks = data.checks || [];
|
|
186
|
+
const head = el('tr', null, el('th'), checks.map((c) => el('th', { class: 'mono', style: 'font-size:var(--fs-1)' }, dlabS(c.date))));
|
|
187
|
+
const rows = (data.rows || []).map((r) => el('tr', null, el('th', { style: 'text-align:left' }, titles[r.id] || r.id), r.cells.map((c, n) => {
|
|
188
|
+
if (!c || !c.forecast_end) return el('td', null, '-');
|
|
189
|
+
const bad = c.promised && c.forecast_end > c.promised;
|
|
190
|
+
return el('td', null, el('button', { type: 'button', 'data-a': 'isnapgo', 'data-k': r.id + '|' + n, 'data-tl-cell': r.id + '|' + n, style: `color:${bad ? 'var(--destructive)' : 'var(--success)'};background:${bad ? 'color-mix(in oklab,var(--destructive) 10%,transparent)' : 'transparent'}`, onClick: () => onCell(r.id, n) }, dlabS(c.forecast_end)));
|
|
191
|
+
})));
|
|
192
|
+
return el('section', { class: 'card', 'data-tl-matrix': '' }, el('h3', null, c3ic('clock'), 'Forecast at each check ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'tap a cell to replay it')),
|
|
193
|
+
el('div', { style: 'overflow-x:auto' }, el('table', { class: 'fcm' }, el('tbody', null, head, rows))), el('div', { class: 'pgl' }, el('span', { style: 'color:var(--success)' }, 'on the promised date'), el('span', { style: 'color:var(--destructive)' }, 'after it')));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The Calendar card for the All scope: the commitments of c3-plan-panels.js (rows with a due date and a status) drawn as one frame, so the same card reads them. */
|
|
197
|
+
const FSTAT = { ok: 'on time', risk: 'at risk', late: 'delayed', over: 'overdue', done: 'on time', park: 'on time' };
|
|
198
|
+
export function calSyncFromCommits(rows) {
|
|
199
|
+
const items = (rows || []).filter((r) => r.st !== 'done' && r.st !== 'park').map((r) => ({ id: r.id, title: r.n, state: 'todo', ms: true, due: r.due, forecast: { end: r.fc || r.due, status: FSTAT[r.st] || 'on time' } }));
|
|
200
|
+
return calSyncCard({ items });
|
|
201
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* pm/c3-toast.js — the one toast every console3 view uses (console3.html toast(), line 1166): a dark pill at the bottom, 4.2 s, with an
|
|
2
|
+
* Undo button when the action can be taken back. One at a time. Two call shapes: toast('text', undoFn) like the mockup, or
|
|
3
|
+
* toast({ text, actionLabel, onAction, timeout, tone }) for a view that names its own button. The CSS is `.c3 .toast` in console3.css. */
|
|
4
|
+
import { el } from './dom.js';
|
|
5
|
+
import { ensureC3Css } from './pm-work-view.js';
|
|
6
|
+
|
|
7
|
+
let host = null; let timer = null;
|
|
8
|
+
export function toast(arg, undo) {
|
|
9
|
+
const o = arg && typeof arg === 'object' ? arg : { text: arg, onAction: undo };
|
|
10
|
+
ensureC3Css();
|
|
11
|
+
if (!host || !host.isConnected) { host = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'toast' }); document.body.appendChild(host); }
|
|
12
|
+
const t = el('div', { class: 'toast' + (o.tone === 'error' ? ' err' : ''), role: 'status', 'aria-live': 'polite', 'data-c3-toast': '' }, el('span', null, o.text));
|
|
13
|
+
if (o.onAction) t.append(el('button', { type: 'button', 'data-c3-toast-action': '', onclick: () => { clearTimeout(timer); t.remove(); o.onAction(); } }, o.actionLabel || 'Undo'));
|
|
14
|
+
host.replaceChildren(t);
|
|
15
|
+
clearTimeout(timer); timer = setTimeout(() => t.remove(), o.timeout || 4200);
|
|
16
|
+
}
|