create-pmos 0.128.0 → 0.130.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 +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-one-home-test.js +4 -1
- package/workspace/guide/briefing.md +60 -0
- package/workspace/guide/calendar-popup.md +46 -0
- package/workspace/guide/index.md +3 -0
- package/workspace/guide/layout.md +1 -0
- package/workspace/guide/proactive-engine.md +2 -2
- package/workspace/lib/day-run.js +4 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-brief-page.js +187 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-brief-data.js +11 -0
- package/workspace/public/pm/c3-brief-needs.js +145 -0
- package/workspace/public/pm/c3-brief-pulse.js +70 -0
- package/workspace/public/pm/c3-brief-today.js +44 -0
- package/workspace/public/pm/c3-calpop.js +103 -0
- package/workspace/public/pm/c3-demo-brief-page.js +54 -0
- package/workspace/public/pm/c3-demo.js +1 -1
- package/workspace/public/pm/c3-shell.js +26 -148
- package/workspace/public/pm/calendar-views.js +1 -0
- package/workspace/public/pm/console3.css +43 -0
- package/workspace/server.js +30 -9
- package/workspace/test-files.json +5 -1
- package/workspace/test-lists/calpop.json +4 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* pm/c3-brief-pulse.js — the Briefing's "Portfolio pulse" (PRD briefing-brief R5, R6).
|
|
2
|
+
* One status bar and a row per initiative (the Calendar's own gantt rows, GET /api/calendar-views quarter, c3-plan.js readQuarter; the demo's pulse rows
|
|
3
|
+
* carry their own colour and reason). A row opens in place with the case component the Needs you rows use (c3-expand + c3-case): why it is where it
|
|
4
|
+
* is, promised against forecast. A press on a row does NOT change the tab. "Open initiative" is an explicit link inside the opened row and is the only
|
|
5
|
+
* thing here that does. Rows are references (data-ref-row): nothing in them changes a record. */
|
|
6
|
+
import { el } from './dom.js';
|
|
7
|
+
import { c3ic } from './pm-work-view.js';
|
|
8
|
+
import { makeXp, attachExpand } from './c3-expand.js';
|
|
9
|
+
import { caseFile } from './c3-brief.js';
|
|
10
|
+
import { readQuarter } from './c3-plan.js';
|
|
11
|
+
import { row, signal, ensureComponentsCss } from './primitives.js';
|
|
12
|
+
|
|
13
|
+
/* tone: ok is neutral, warn is the warning token, bad the destructive one (R28-12) */
|
|
14
|
+
const PST = { 'on time': ['ok', 'On time'], 'at risk': ['warn', 'At risk'], delayed: ['bad', 'Delayed'], overdue: ['bad', 'Overdue'], 'no plan': ['', 'No plan'] };
|
|
15
|
+
const RAG = { red: ['bad', 'Off track'], amber: ['warn', 'At risk'], green: ['ok', 'On track'], park: ['', 'Parked'] };
|
|
16
|
+
const SIG = { bad: 'destructive', warn: 'warning' };
|
|
17
|
+
const end = (o) => (o && (o.end || o.start)) || null;
|
|
18
|
+
|
|
19
|
+
/** One gantt row as the pulse draws it: { id, title, cls, word, color, why, evidence }. Pure. */
|
|
20
|
+
export function pulseRow(g) {
|
|
21
|
+
if (g.pulse && RAG[g.pulse.c]) {
|
|
22
|
+
const [cls, word] = RAG[g.pulse.c];
|
|
23
|
+
const ev = [];
|
|
24
|
+
if (g.promised) ev.push(['target', 'Promised ' + g.promised, 'Plan', cls === 'bad' ? 'bad' : cls === 'warn' ? 'warn' : '']);
|
|
25
|
+
return { id: g.id, title: g.title || g.id, cls, word, why: g.pulse.why, evidence: ev };
|
|
26
|
+
}
|
|
27
|
+
const k = PST[g.status] ? g.status : 'no plan'; const f = end(g.forecast); const p = g.promised || null;
|
|
28
|
+
const why = p ? 'Forecast ' + (f || 'not set') + ' vs promised ' + p : 'No promised date' + (f ? '; forecast ' + f : '');
|
|
29
|
+
const ev = [];
|
|
30
|
+
if (p) ev.push(['target', 'Promised ' + p, 'Plan']);
|
|
31
|
+
if (f) ev.push(['clock', 'Forecast ' + f, 'Plan', PST[k][0] === 'bad' ? 'bad' : PST[k][0] === 'warn' ? 'warn' : '']);
|
|
32
|
+
if (g.slip_days > 0) ev.push(['alert', 'Slipping by ' + g.slip_days + ' d', 'Plan', 'warn']);
|
|
33
|
+
return { id: g.id, title: g.title || g.id, cls: PST[k][0], word: PST[k][1] + (g.slip_days > 0 ? ' +' + g.slip_days + ' d' : ''), why, evidence: ev };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function pulseCase(r) {
|
|
37
|
+
return { id: 'pulse:' + r.id, tc: r.cls === 'bad' ? 'var(--destructive)' : r.cls === 'warn' ? 'var(--warning)' : 'var(--muted-foreground)', sit: r.why, chg: r.evidence, opts: [], go: ['goinit', r.id, 'Open initiative'], cta: '' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function pulse(S) {
|
|
41
|
+
ensureComponentsCss();
|
|
42
|
+
const body = el('div', null, el('p', { class: 'dim mono c3s2' }, 'Reading the initiatives.'));
|
|
43
|
+
const box = el('section', { class: 'card pulsecard', 'data-c3pg': 'pulse' },
|
|
44
|
+
el('div', { class: 'pulsehead' },
|
|
45
|
+
el('h3', null, c3ic('init', 'sm'), 'Portfolio pulse'), el('span', { class: 'mono dim c3s2' }, 'promised vs forecast')), body);
|
|
46
|
+
const note = (t, ic) => el('div', { class: 'fold' }, c3ic(ic || 'moon', 'sm'), t);
|
|
47
|
+
const draw = (rows, err) => {
|
|
48
|
+
if (err) { body.replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status' }, 'The initiatives could not be read: ' + err)); return; }
|
|
49
|
+
if (!rows.length) { body.replaceChildren(note('No initiative has a promised date yet, so there is nothing to compare with the forecast.')); return; }
|
|
50
|
+
const noPlan = rows.filter((g) => !g.pulse && g.status === 'no plan').length;
|
|
51
|
+
rows = rows.filter((g) => g.pulse || g.status !== 'no plan');
|
|
52
|
+
if (!rows.length) { body.replaceChildren(note('No initiative has a promised date yet, so there is nothing to compare with the forecast.'), ...(noPlan ? [el('p', { class: 'dim mono c3s2' }, noPlan + ' initiatives have no plan date.')] : [])); return; }
|
|
53
|
+
const pr = rows.map(pulseRow);
|
|
54
|
+
const by = new Map(); pr.forEach((r) => { const k = r.word.replace(/ \+.*/, ''); const o = by.get(k) || { n: 0, cls: r.cls }; o.n += 1; by.set(k, o); });
|
|
55
|
+
const bar = el('div', { class: 'pbs', role: 'group', 'aria-label': 'Status of initiatives' },
|
|
56
|
+
...[...by.entries()].map(([k, v]) => { const seg = el('span', { class: 'pb-seg pb-' + (v.cls || 'mute'), title: v.n + ' ' + k.toLowerCase(), 'aria-label': v.n + ' ' + k.toLowerCase() }); seg.style.setProperty('--fr-grow', String(v.n)); return seg; }));
|
|
57
|
+
const rowEl = (r) => {
|
|
58
|
+
const xp = makeXp(caseFile(pulseCase(r)));
|
|
59
|
+
let ex = null;
|
|
60
|
+
const head = row({ title: r.title, meta: [r.why], signals: [signal({ text: r.word, tone: SIG[r.cls] || 'muted' })], openLabel: r.title + ': ' + r.word, onOpen: () => ex.toggle(), openAttrs: { 'data-c3pg-prow': r.id } });
|
|
61
|
+
ex = attachExpand(head, xp, { key: 'pulse-' + r.id });
|
|
62
|
+
return el('div', { class: 'pwrap', 'data-ref-row': '', 'data-pulse-row': r.id }, head, xp);
|
|
63
|
+
};
|
|
64
|
+
body.replaceChildren(...[bar, el('div', null, ...pr.slice(0, 8).map(rowEl)), pr.length > 8 ? el('div', { class: 'dim mono c3s2' }, '+' + (pr.length - 8) + ' more in the Calendar tab') : null, noPlan ? el('div', { class: 'dim mono c3s2' }, noPlan + ' more have no plan date.') : null].filter(Boolean));
|
|
65
|
+
};
|
|
66
|
+
readQuarter().then((j) => {
|
|
67
|
+
if (!j || j.ok === false) draw([], (j && j.error) || 'unreadable'); else { S.today = j.today || ''; draw(j.gantt || []); }
|
|
68
|
+
}).catch((e) => draw([], String((e && e.message) || e)));
|
|
69
|
+
return box;
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* pm/c3-brief-today.js — the Briefing's "Today": the agenda of today's meetings (PRD briefing-brief R2).
|
|
2
|
+
* One row per meeting, in time order, from GET /api/brief-page: its time and length, the people, the initiative, what PMOS holds as PREPARED
|
|
3
|
+
* (documents, notes) and what the PM must do BEFORE the meeting (own items that name it). A meeting with nothing held says "Nothing prepared
|
|
4
|
+
* yet". A meeting that is over is dimmed; the one under way is marked Now, the next one Next.
|
|
5
|
+
* Rows are REFERENCES (data-ref-row): they carry no control that changes a record. The record's actions are in Needs you and on its own page. */
|
|
6
|
+
import { el } from './dom.js';
|
|
7
|
+
import { c3ic } from './pm-work-view.js';
|
|
8
|
+
import { onBriefPage } from './c3-brief-data.js';
|
|
9
|
+
|
|
10
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
11
|
+
const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p2(d.getMinutes()); };
|
|
12
|
+
|
|
13
|
+
export function meetingRow(m) {
|
|
14
|
+
const marks = { now: 'Now', next: 'Next' };
|
|
15
|
+
const prep = m.prepared && m.prepared.length
|
|
16
|
+
? el('ul', { class: 'tl', role: 'list', 'aria-label': 'Prepared' }, ...m.prepared.map((x) => el('li', { 'data-prep': x.kind }, c3ic(x.kind === 'prep' ? 'spark' : 'doc', 'sm'), x.text)))
|
|
17
|
+
: el('p', { class: 'dim c3s2', 'data-prep': 'none' }, c3ic('moon', 'sm'), 'Nothing prepared yet');
|
|
18
|
+
const bef = m.before && m.before.length
|
|
19
|
+
? el('ul', { class: 'tl', role: 'list', 'aria-label': 'Before the meeting' }, ...m.before.map((x) => el('li', { 'data-before': x.id }, c3ic('todo', 'sm'), x.title, x.due ? el('span', { class: 'mono dim' }, ' · due ' + x.due) : null)))
|
|
20
|
+
: el('p', { class: 'dim c3s2', 'data-before': 'none' }, 'Nothing to do before it.');
|
|
21
|
+
return el('li', { class: 'trow ' + m.state, 'data-ref-row': '', 'data-today-row': m.id, 'data-state': m.state },
|
|
22
|
+
el('div', { class: 'tm mono' }, hm(m.start), el('div', { class: 'dim c3s2' }, m.minutes ? m.minutes + ' min' : 'no end')),
|
|
23
|
+
el('div', { class: 'tb' },
|
|
24
|
+
el('div', { class: 'chips2' }, el('b', { class: 'tt' }, m.title), marks[m.state] ? el('span', { class: 'st ' + (m.state === 'now' ? 'ok' : ''), 'data-mark': m.state }, marks[m.state]) : null),
|
|
25
|
+
el('div', { class: 'chips2 dim c3s2' },
|
|
26
|
+
m.people && m.people.length ? el('span', { class: 'chip2', 'data-people': '' }, c3ic('user', 'sm'), m.people.join(', ')) : null,
|
|
27
|
+
m.initiative ? el('span', { class: 'chip2', 'data-init': '' }, c3ic('init', 'sm'), m.initiative) : null),
|
|
28
|
+
el('div', { class: 'tcols' }, el('div', null, el('div', { class: 'lab mono' }, 'Prepared'), prep), el('div', null, el('div', { class: 'lab mono' }, 'Before the meeting'), bef))));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function todaySection() {
|
|
32
|
+
const list = el('ul', { class: 'list', role: 'list' });
|
|
33
|
+
const count = el('span', { class: 'bdg g' }, '0');
|
|
34
|
+
const box = el('section', { class: 'card sect', 'data-c3pg': 'today', 'aria-label': 'Today' },
|
|
35
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('cal', 'sm'), 'Today ', count)), list);
|
|
36
|
+
list.replaceChildren(el('li', { class: 'fold', 'data-today-state': 'loading' }, c3ic('clock', 'sm'), 'Reading today\'s meetings.'));
|
|
37
|
+
onBriefPage((j) => {
|
|
38
|
+
if (!j || j.ok === false) { count.textContent = '0'; list.replaceChildren(el('li', { class: 'fold', role: 'status', 'data-today-state': 'error' }, c3ic('alert', 'sm'), 'Today\'s meetings could not be read: ' + ((j && j.error) || 'unreadable'))); return; }
|
|
39
|
+
const t = j.today || [];
|
|
40
|
+
count.textContent = String(t.length);
|
|
41
|
+
list.replaceChildren(...(t.length ? t.map(meetingRow) : [el('li', { class: 'fold', 'data-today-state': 'empty' }, c3ic('moon', 'sm'), 'No meetings today in the calendar PMOS holds.')]));
|
|
42
|
+
});
|
|
43
|
+
return box;
|
|
44
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* pm/c3-calpop.js — the calendar as a popup over any page of the console (briefing-brief PRD R7, R9).
|
|
2
|
+
*
|
|
3
|
+
* mountCalPopupTrigger(toolsHost) adds the calendar button to the top bar's tools, and binds the key C (once)
|
|
4
|
+
* openCalendarPopup(opener?) opens the full calendar (five zooms, no ribbon) in a dialog over the page
|
|
5
|
+
* closeCalendarPopup(back?) closes it; `back` (default true) returns the focus to where it was
|
|
6
|
+
* lastZoom() the zoom last used, "day" until the PM changes it
|
|
7
|
+
*
|
|
8
|
+
* The popup mounts calendar-views.js mountCalendarViews on a host of its own, so it never shares state with the Calendar tab
|
|
9
|
+
* (mountCalendarViews keys its state on the host element). Closing calls disposeCalendarViews on that host, which removes
|
|
10
|
+
* the view's own key handler. The dialog is data-shell="calpop", not "sheet", so the view's + and - keys keep working inside it;
|
|
11
|
+
* the Calendar tab's instance steps aside while the popup is open (calendar-views.js onKey).
|
|
12
|
+
* Not open on load. Escape, a press on the scrim and the close button close it. Tab stays inside. On a phone it is full screen.
|
|
13
|
+
* The zoom is kept in module state; localStorage only inside try/catch, as a convenience.
|
|
14
|
+
*/
|
|
15
|
+
import { el } from './dom.js';
|
|
16
|
+
import { ensureC3Css, c3ic, c3ib, c3Phone } from './pm-work-view.js';
|
|
17
|
+
import { mountCalendarViews, disposeCalendarViews } from './calendar-views.js';
|
|
18
|
+
|
|
19
|
+
const ZOOMS = ['day', 'week', 'month', 'quarter', 'year'];
|
|
20
|
+
const KEY = 'pmos.c3.calpop.zoom';
|
|
21
|
+
const store = {
|
|
22
|
+
get() { try { return localStorage.getItem(KEY); } catch { return null; } },
|
|
23
|
+
set(v) { try { localStorage.setItem(KEY, v); } catch { /* no storage */ } },
|
|
24
|
+
};
|
|
25
|
+
let zoom = ZOOMS.includes(store.get()) ? store.get() : 'day';
|
|
26
|
+
let cur = null; // { wrap, host, opener, off, obs, stopPhone }
|
|
27
|
+
let keyBound = false;
|
|
28
|
+
|
|
29
|
+
export const lastZoom = () => zoom;
|
|
30
|
+
export const isCalendarPopupOpen = () => !!cur;
|
|
31
|
+
|
|
32
|
+
/** The zoom the view is on now: its pressed Day/Week/... button. */
|
|
33
|
+
function readZoom(host) {
|
|
34
|
+
const b = host.querySelector('[data-cv-view][aria-pressed="true"],[data-cv-view][aria-selected="true"],[data-cv-view][aria-current="true"]');
|
|
35
|
+
const v = b && b.getAttribute('data-cv-view');
|
|
36
|
+
return ZOOMS.includes(v) ? v : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
40
|
+
|
|
41
|
+
export function closeCalendarPopup(back = true) {
|
|
42
|
+
if (!cur) return;
|
|
43
|
+
const c = cur; cur = null;
|
|
44
|
+
const z = readZoom(c.host); if (z) { zoom = z; store.set(z); }
|
|
45
|
+
document.removeEventListener('keydown', c.onKey, true);
|
|
46
|
+
if (c.obs) c.obs.disconnect();
|
|
47
|
+
if (c.stopPhone) c.stopPhone();
|
|
48
|
+
try { disposeCalendarViews(c.host); } catch { /* already gone */ }
|
|
49
|
+
c.wrap.remove();
|
|
50
|
+
if (back && c.opener && c.opener.isConnected && c.opener.focus) { try { c.opener.focus({ preventScroll: true }); } catch { /* not focusable any more */ } }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function openCalendarPopup(opener) {
|
|
54
|
+
if (cur) return;
|
|
55
|
+
ensureC3Css();
|
|
56
|
+
const from = opener || (document.activeElement && document.activeElement !== document.body ? document.activeElement : document.querySelector('[data-c3pg="calendar"]'));
|
|
57
|
+
const close = () => closeCalendarPopup(true);
|
|
58
|
+
const host = el('div', { class: 'calpop-host', 'data-c3pg': 'calpop-host' });
|
|
59
|
+
const closeBtn = c3ib('Close', 'x', { title: 'Close (Esc)', 'data-c3pg': 'calpop-close', onclick: close }, 'sm');
|
|
60
|
+
const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic('cal'), el('b', null, 'Calendar')), el('span', { class: 'chips2' }, closeBtn));
|
|
61
|
+
const dlg = el('div', { class: 'sheet bs calpop', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Calendar', tabindex: '-1', 'data-c3pg': 'calpop' },
|
|
62
|
+
el('div', { class: 'grab' }), head, el('div', { class: 'sb' }, host));
|
|
63
|
+
const wrap = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'calpop' }, el('div', { class: 'scrim', onclick: close }), dlg);
|
|
64
|
+
const stopPhone = c3Phone(wrap);
|
|
65
|
+
const onKey = (e) => {
|
|
66
|
+
if (e.key === 'Escape') { e.stopPropagation(); e.preventDefault(); close(); return; }
|
|
67
|
+
if (e.key !== 'Tab') return;
|
|
68
|
+
const items = [...dlg.querySelectorAll(FOCUSABLE)].filter((n) => n.offsetWidth > 0 || n.offsetHeight > 0 || n === document.activeElement);
|
|
69
|
+
if (!items.length) { e.preventDefault(); dlg.focus(); return; }
|
|
70
|
+
const at = items.indexOf(document.activeElement);
|
|
71
|
+
if (at < 0 || (e.shiftKey && at === 0) || (!e.shiftKey && at === items.length - 1)) { e.preventDefault(); items[e.shiftKey ? items.length - 1 : 0].focus(); }
|
|
72
|
+
};
|
|
73
|
+
document.addEventListener('keydown', onKey, true);
|
|
74
|
+
document.body.appendChild(wrap);
|
|
75
|
+
cur = { wrap, host, opener: from, onKey, stopPhone, obs: null };
|
|
76
|
+
mountCalendarViews(host, { view: zoom, scope: undefined });
|
|
77
|
+
/* remember the zoom as it changes, so a reload of the page also keeps it */
|
|
78
|
+
cur.obs = new MutationObserver(() => { const z = readZoom(host); if (z) { zoom = z; store.set(z); } });
|
|
79
|
+
cur.obs.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ['aria-pressed'] });
|
|
80
|
+
dlg.focus({ preventScroll: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const typing = (n) => !!(n && n.matches && n.matches('input,textarea,select,[contenteditable],[contenteditable="true"]'));
|
|
84
|
+
const otherSheet = () => !!document.querySelector('[data-shell="sheet"],[data-shell="preview"],[data-shell="palette"]');
|
|
85
|
+
|
|
86
|
+
function onC(e) {
|
|
87
|
+
if (e.key !== 'c' && e.key !== 'C') return;
|
|
88
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.defaultPrevented || e.repeat) return;
|
|
89
|
+
if (cur) return; // open already: Escape closes it
|
|
90
|
+
if (typing(document.activeElement) || typing(e.target) || otherSheet()) return;
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
openCalendarPopup();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The calendar button, last before the search button; and the key C. Call once, with the top bar's tools element. */
|
|
96
|
+
export function mountCalPopupTrigger(toolsHost) {
|
|
97
|
+
if (!keyBound) { keyBound = true; document.addEventListener('keydown', onC, true); }
|
|
98
|
+
if (!toolsHost || toolsHost.querySelector('[data-c3pg="calendar"]')) return null;
|
|
99
|
+
const btn = c3ib('Calendar (C)', 'cal', { 'aria-haspopup': 'dialog', 'data-c3pg': 'calendar', onclick: (e) => openCalendarPopup(e.currentTarget) });
|
|
100
|
+
const search = toolsHost.querySelector('[data-c3pg="search"]');
|
|
101
|
+
if (search) toolsHost.insertBefore(btn, search); else toolsHost.append(btn);
|
|
102
|
+
return btn;
|
|
103
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* pm/c3-demo-brief-page.js — DEMO ONLY (/c3.html?demo=1): the Briefing's Today agenda and the own-item rows of Needs you, in the real shape of
|
|
2
|
+
* GET /api/brief-page (lib/routes-brief-page.js), for the demo day (Thu 8 Oct 2026, 08:12), with the demo's own people and initiatives.
|
|
3
|
+
* Today is the five meetings of the demo calendar (c3-demo-data.js MEET). Needs you gets two items that are overdue, one due today, and two outside-PMOS items
|
|
4
|
+
* (one with evidence: a meeting with Dara is booked today). Decisions are the demo briefing's own needs (c3-demo-data.js NEEDS), not repeated here.
|
|
5
|
+
* Writes: the pm-items routes the rows call (done, notyet, remove, reopen, undo) are answered in memory, and every one is kept in window.__c3demoWrites
|
|
6
|
+
* ({ path, body }) so a test can see what the page asked to write. Real mode never loads this file. */
|
|
7
|
+
const iso = (h, m) => new Date(2026, 9, 8, h, m).toISOString();
|
|
8
|
+
const TODAY = [
|
|
9
|
+
{ id: 'm1', title: 'Standup', start: iso(9, 30), end: iso(10, 0), people: ['Dara', 'Lee', 'Sam'], initiative: 'Checkout v2', prepared: [], before: [] },
|
|
10
|
+
{ id: 'm2', title: 'Checkout sync', start: iso(10, 30), end: iso(11, 15), people: ['Priya', 'Dara'], initiative: 'Checkout v2',
|
|
11
|
+
prepared: [{ kind: 'prep', text: 'Prep note: sandbox keys are 2 days late; Jira says 24 Oct, the PRD says 31 Oct.' }, { kind: 'last-note', text: 'Last notes: Checkout sync (2026-10-05)' }],
|
|
12
|
+
before: [{ id: 'i2', title: 'Confirm the 1 Oct numbers before the 10:30 update', due: '2026-10-08' }] },
|
|
13
|
+
{ id: 'm3', title: 'Customer interview', start: iso(13, 0), end: iso(14, 0), people: ['Marta'], initiative: 'Returns experience',
|
|
14
|
+
prepared: [{ kind: 'prep', text: 'Interview guide draft is in Review.' }], before: [{ id: 'i6', title: 'Review the 13:00 interview guide', due: '2026-10-08' }] },
|
|
15
|
+
{ id: 'm4', title: 'Sprint planning', start: iso(14, 0), end: iso(15, 0), people: ['Dara', 'Priya', 'Sam'], initiative: 'Checkout v2', prepared: [],
|
|
16
|
+
before: [{ id: 'i1', title: 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', due: '2026-10-08' }] },
|
|
17
|
+
{ id: 'm5', title: 'Leadership update', start: iso(16, 30), end: iso(17, 0), people: ['Priya', 'Marta'], initiative: null,
|
|
18
|
+
prepared: [{ kind: 'last-note', text: 'Last notes: Leadership update (2026-10-01)' }], before: [] },
|
|
19
|
+
];
|
|
20
|
+
const NEEDS = [
|
|
21
|
+
{ key: 'item:bp-od1', kind: 'overdue', id: 'bp-od1', headline: 'Send Priya the revised Checkout v2 date', due: '2026-10-06', days_late: 2, priority: 'P1', person: 'Priya', offline: false, evidence: null },
|
|
22
|
+
{ key: 'item:bp-od2', kind: 'overdue', id: 'bp-od2', headline: 'Ask Lee to book the design vendor call', due: '2026-10-07', days_late: 1, priority: 'P2', person: 'Lee', offline: true, evidence: null },
|
|
23
|
+
{ key: 'item:bp-dt1', kind: 'due-today', id: 'bp-dt1', headline: 'Send Lee the pricing-page feedback', due: '2026-10-08', days_late: 0, priority: 'P2', person: null, offline: false, evidence: null },
|
|
24
|
+
{ key: 'item:bp-of1', kind: 'offline', id: 'bp-of1', headline: 'Talk to Dara about the flow 4 estimate', due: null, days_late: 0, priority: 'P2', person: 'Dara', offline: true,
|
|
25
|
+
evidence: { text: 'a meeting with Dara is booked Thu 14:00', event_id: 'm4' } },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
let gone = {};
|
|
29
|
+
export function installBriefPage() {
|
|
30
|
+
if (window.__c3demoBriefPage) return; window.__c3demoBriefPage = true;
|
|
31
|
+
window.__c3demoWrites = [];
|
|
32
|
+
const prev = window.fetch.bind(window);
|
|
33
|
+
const json = (b) => new Response(JSON.stringify(b), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
34
|
+
window.fetch = (input, init) => {
|
|
35
|
+
try {
|
|
36
|
+
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
|
37
|
+
const u = new URL(raw, location.origin);
|
|
38
|
+
const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
39
|
+
if (u.origin === location.origin) {
|
|
40
|
+
if (method === 'GET' && u.pathname === '/api/brief-page') return Promise.resolve(json({ ok: true, date: '2026-10-08', today: TODAY.map((m) => ({ ...m, minutes: (Date.parse(m.end) - Date.parse(m.start)) / 60000, state: Date.parse(m.end) <= Date.now() ? 'past' : 'later' })).map((m, i, a) => ({ ...m, state: m.state === 'past' ? 'past' : (a.findIndex((x) => x.state !== 'past' && Date.parse(x.start) > Date.now()) === i ? 'next' : 'later') })), needs: NEEDS.filter((n) => !gone[n.id]) }));
|
|
41
|
+
const m = /^\/api\/pm-items\/(done|notyet|remove|reopen|undo)$/.exec(u.pathname);
|
|
42
|
+
if (method !== 'GET' && m) {
|
|
43
|
+
let b = {}; try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; }
|
|
44
|
+
if (NEEDS.some((n) => n.id === b.id)) {
|
|
45
|
+
window.__c3demoWrites.push({ path: u.pathname, body: b });
|
|
46
|
+
if (m[1] === 'reopen' || m[1] === 'undo') delete gone[b.id]; else gone[b.id] = m[1];
|
|
47
|
+
return Promise.resolve(json({ ok: true }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch { /* an unreadable request goes on as it is */ }
|
|
52
|
+
return prev(input, init);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -111,4 +111,4 @@ export function demoPill() {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export const DEMO_WS = D.WS_ID;
|
|
114
|
-
export function installDemo() { freezeClock(); install(); installRep3(); installInit(); import('./c3-demo-rep2.js').then((m) => m.installRep2()); window.__c3demo = true; window.__c3demoReady = import('./c3-demo-brief-ix.js').then((m) => m.installBriefIx()).then(() => import('./c3-demo-misc2.js')).then((m) => m.installMisc2()); } // Briefing case files, people and a simulated agent (c3-shell.js waits for __c3demoReady)
|
|
114
|
+
export function installDemo() { freezeClock(); install(); installRep3(); installInit(); import('./c3-demo-rep2.js').then((m) => m.installRep2()); window.__c3demo = true; window.__c3demoReady = import('./c3-demo-brief-ix.js').then((m) => m.installBriefIx()).then(() => import('./c3-demo-misc2.js')).then((m) => m.installMisc2()).then(() => import('./c3-demo-brief-page.js')).then((m) => m.installBriefPage()); } // Briefing case files, people and a simulated agent (c3-shell.js waits for __c3demoReady)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/* pm/c3-shell.js — the console3 SHELL as its own page (/c3.html). The mockup (console3.html) is the UI: the header (title and date,
|
|
2
2
|
* the icon buttons, the bell), the tab bar (Briefing, Calendar, Work, Initiatives, Reports) and the three-column Briefing. This
|
|
3
3
|
* file draws only the frame and composes the real views into it, each through its own mount function:
|
|
4
|
-
* Briefing
|
|
5
|
-
* middle mountCalendarViews (day) + a portfolio pulse drawn from the Calendar's own gantt rows
|
|
4
|
+
* Briefing one column, the brief: the deck (mountBriefing), Today (c3-brief-today.js), Needs you (c3-brief-needs.js), Portfolio pulse (c3-brief-pulse.js)
|
|
6
5
|
* right the dock (c3-dock.js): the repository, branch and session tree, the selected session's detail, "Open the session" to /
|
|
7
6
|
* Every tab but Briefing also carries the mockup's "Plan: Decide what gates the day" lens row and deck (c3-plan.js planNode).
|
|
8
7
|
* Calendar mountCalendarViews Work mountPmWork
|
|
@@ -17,6 +16,10 @@ import { pmGet } from './http.js';
|
|
|
17
16
|
import { ensureC3Css, c3ic, c3Phone, mountPmWork } from './pm-work-view.js';
|
|
18
17
|
import { mountBriefing, showCard, flipBack } from './briefing-deck.js';
|
|
19
18
|
import { mountCalendarViews } from './calendar-views.js';
|
|
19
|
+
import { todaySection } from './c3-brief-today.js';
|
|
20
|
+
import { needsSection } from './c3-brief-needs.js';
|
|
21
|
+
import { pulse } from './c3-brief-pulse.js';
|
|
22
|
+
import { loadBriefPage } from './c3-brief-data.js';
|
|
20
23
|
import { mountLifecycle } from './lifecycle-lens.js';
|
|
21
24
|
import { mountForecast } from './timeline-forecast-view.js';
|
|
22
25
|
import { mountReleases } from './releases-view.js';
|
|
@@ -24,11 +27,11 @@ import { mountReportFamilies } from './report-families-view.js';
|
|
|
24
27
|
import { mountReports } from './c3-reports.js';
|
|
25
28
|
import { mountOutbox } from './outbox-panel.js';
|
|
26
29
|
import { createDock } from './c3-dock.js';
|
|
27
|
-
import {
|
|
28
|
-
import { makeXp, attachExpand } from './c3-expand.js';
|
|
30
|
+
import { reviewSection, handledRow, trustCard, footLine, rowApi, BX, toast, setNeedsTitle, hooks as briefHooks } from './c3-brief.js';
|
|
29
31
|
import { stopCard } from './c3-misc-panels.js';
|
|
30
32
|
import { openSheet } from './c3-sheet.js';
|
|
31
|
-
import {
|
|
33
|
+
import { mountCalPopupTrigger } from './c3-calpop.js';
|
|
34
|
+
import { planNode, setPlanScope, dueCard, crossLinks, newInitLine, portfolioCard } from './c3-plan.js';
|
|
32
35
|
import { initiativesViewCtx } from './registry.js';
|
|
33
36
|
import { mount as mountRoadmap } from './c3-roadmap.js';
|
|
34
37
|
import { mount as mountBreakdown } from './c3-breakdown.js';
|
|
@@ -40,7 +43,7 @@ import { mountDaypart } from './c3-daypart.js';
|
|
|
40
43
|
import { mountTicker } from './shell-ticker.js';
|
|
41
44
|
import { mountBell, openBellList } from './shell-bell.js';
|
|
42
45
|
import { onBriefing, loadBriefing } from './shell-ticker.js';
|
|
43
|
-
import {
|
|
46
|
+
import { registry, filterPalette, rowStep } from './briefing-model.js';
|
|
44
47
|
|
|
45
48
|
/* In demo mode each tab's deck asks for its own cards (the mockup draws a different deck per tab); real mode sends no scope. */
|
|
46
49
|
const sc = (name) => (DEMO ? name : undefined);
|
|
@@ -99,6 +102,7 @@ function header() {
|
|
|
99
102
|
S.bellHost = el('span', { class: 'c3pg-bell' });
|
|
100
103
|
S.tools.append(S.bellHost);
|
|
101
104
|
S.tools.append(ibtn('Search (Ctrl K)', 'search', { 'data-c3pg': 'search', 'aria-haspopup': 'dialog', onclick: () => openPalette() }));
|
|
105
|
+
mountCalPopupTrigger(S.tools); // the calendar button (before Search) and the key C (c3-calpop.js)
|
|
102
106
|
S.moreBtn = ibtn('More', 'more', { 'aria-haspopup': 'menu', 'aria-expanded': 'false', 'data-c3pg': 'more', onclick: toggleMore });
|
|
103
107
|
S.tools.append(el('span', { class: 'c3pg-menuwrap' }, S.moreBtn));
|
|
104
108
|
return el('div', { class: 'c3top' }, el('div', null, S.ttl, S.sub), S.tools);
|
|
@@ -240,130 +244,12 @@ function moveInd() {
|
|
|
240
244
|
S.ind.style.width = b.offsetWidth + 'px'; S.ind.style.transform = 'translateX(' + b.offsetLeft + 'px)';
|
|
241
245
|
}
|
|
242
246
|
|
|
243
|
-
/* ── the Briefing tab:
|
|
244
|
-
|
|
245
|
-
*
|
|
246
|
-
* only the needs with a hard deadline stay. */
|
|
247
|
-
function needsList() {
|
|
248
|
-
const count = el('span', { class: 'bdg g' }, '0');
|
|
249
|
-
const quick = el('span', { class: 'chips2 dim mono', style: 'font-size:var(--fs-2)' });
|
|
250
|
-
const list = el('ul', { class: 'list', role: 'list' });
|
|
251
|
-
const extra = el('div');
|
|
252
|
-
const alertHost = el('div');
|
|
253
|
-
const box = el('section', { class: 'card sect', 'data-c3pg': 'needs', 'aria-label': 'Needs you' },
|
|
254
|
-
el('div', { class: 'sh2' }, el('h2', null, c3ic('inbox', 'sm'), 'Needs you ', count)), alertHost, list, extra);
|
|
255
|
-
let sig = ''; let last = null;
|
|
256
|
-
const recount = () => {
|
|
257
|
-
const n = box.querySelectorAll('.nrow[data-needs-row]').length;
|
|
258
|
-
count.textContent = String(n);
|
|
259
|
-
if (!n && !list.querySelector('.fold')) list.replaceChildren(el('li', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing needs you.'));
|
|
260
|
-
};
|
|
261
|
-
const ctx = { changed: recount };
|
|
262
|
-
const fill = (b) => {
|
|
263
|
-
last = b;
|
|
264
|
-
const inc = !!S.incident;
|
|
265
|
-
const needs = b && Array.isArray(b.needs) && b.needs.length ? b.needs.filter((n) => !BX.gone[n.key] && (!inc || n.hard)) : null;
|
|
266
|
-
const own = !needs && b && Array.isArray(b.own_needs) ? b.own_needs.filter((n) => n && !BX.gone[n.key]) : []; // the PM's own overdue and due-today items: overdue first, above the engine's cards
|
|
267
|
-
const key = needs ? needs.map((n) => n.key + n.headline) : own.map((n) => n.key + n.headline).concat(((b && b.cards) || []).map((c) => (c.key || '') + c.headline));
|
|
268
|
-
if (needs && S.pinNeed) { const at = needs.findIndex((n) => n.key === S.pinNeed); if (at > 0) needs.unshift(...needs.splice(at, 1)); } // a need the banner or the bell named rides to the top of the list: shown, not folded
|
|
269
|
-
const g = JSON.stringify([key, inc, !!(b && b.kept_out), S.pinNeed || '']);
|
|
270
|
-
if (g === sig) return; sig = g;
|
|
271
|
-
alertHost.replaceChildren(...(inc ? [el('div', { class: 'incident', role: 'status', 'data-c3pg': 'incident' }, c3ic('alert', 'lg'), el('div', null, el('b', null, 'Card payments failing since 07:40'), el('div', { class: 'dim', style: 'font-size:var(--fs-3)' }, '12% of checkouts. Sam on call. Only deadlines shown.')))] : []));
|
|
272
|
-
if (needs) {
|
|
273
|
-
if (!quick.isConnected) box.querySelector('.sh2').append(quick);
|
|
274
|
-
quick.textContent = ''; quick.append(c3ic('bolt', 'sm'), needs.filter((n) => n.quick).length + ' under 2 min');
|
|
275
|
-
list.replaceChildren(...needs.slice(0, 5).map((n, i) => needRow(n, i, ctx))); // the mockup shows five and folds the rest "below the line"
|
|
276
|
-
extra.replaceChildren(...(inc ? [] : belowLine(needs.slice(5), (n, i) => needRow(n, i + 5, ctx), b.kept_out)));
|
|
277
|
-
recount();
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const cards = ((b && b.cards) || []).map(cardView);
|
|
281
|
-
quick.remove();
|
|
282
|
-
const ownRows = (inc ? own.filter((n) => n.overdue) : own).map((n, i) => needRow(n, i, ctx));
|
|
283
|
-
const cardRows = cards.map((v, i) => {
|
|
284
|
-
const T = TAG_STYLE[v.tag] || TAG_STYLE['Just in'];
|
|
285
|
-
return needRow({ key: v.key || String(i), headline: v.headline, kind: v.tag, icon: T.icon, color: T.color, time: v.deadline && v.deadline.text ? v.deadline.text : null, src: v.tag, view: v }, i + ownRows.length, ctx);
|
|
286
|
-
});
|
|
287
|
-
const all = ownRows.concat(cardRows);
|
|
288
|
-
list.replaceChildren(...(all.length ? all : [el('li', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing needs you.')]));
|
|
289
|
-
extra.replaceChildren();
|
|
290
|
-
count.textContent = String(all.length);
|
|
291
|
-
};
|
|
292
|
-
S.refreshNeeds = () => { sig = ''; if (last) fill(last); };
|
|
293
|
-
S.needsFill = fill;
|
|
294
|
-
onBriefing((b) => { S.brief = b; fill(b); });
|
|
295
|
-
loadBriefing();
|
|
296
|
-
return box;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/* portfolio pulse: the Calendar's own gantt rows (GET /api/calendar-views, quarter) as one status bar and a row per initiative */
|
|
300
|
-
const PST = { 'on time': ['ok', 'On time', 'var(--success)'], 'at risk': ['warn', 'At risk', 'var(--warning)'], delayed: ['bad', 'Delayed', 'var(--destructive)'], overdue: ['bad', 'Overdue', 'var(--destructive)'], 'no plan': ['', 'No plan', 'var(--muted-foreground)'] };
|
|
301
|
-
const PST_CLS = { 'on time': 'pst-ok', 'at risk': 'pst-warn', delayed: 'pst-bad', overdue: 'pst-bad', 'no plan': 'pst-none' };
|
|
302
|
-
|
|
303
|
-
/* A pulse row opens in place, like a Needs you row: the same c3-expand panel and the same case component. It never changes the tab. "Open initiative" inside the case is the only way out. */
|
|
304
|
-
const PULSE_OPEN = new Set();
|
|
305
|
-
function pulseCase(g) {
|
|
306
|
-
const f = (g.forecast && (g.forecast.end || g.forecast.start)) || null; const p = g.promised || null;
|
|
307
|
-
const ev = [];
|
|
308
|
-
if (p) ev.push(['clock', 'Promised ' + p, '', 'ok']);
|
|
309
|
-
if (f) ev.push(['clock', 'Forecast ' + f, '', g.slip_days > 0 ? 'warn' : 'ok']);
|
|
310
|
-
if (g.slip_days > 0) ev.push(['alert', 'Slips ' + g.slip_days + ' d past the promise.', '', 'warn']);
|
|
311
|
-
const why = (g.pulse && g.pulse.why) || (p ? 'Forecast ' + (f || 'not set') + ' vs promised ' + p + '.' : 'No promised date' + (f ? '; forecast ' + f : '') + '.');
|
|
312
|
-
return caseFile({ id: 'pulse:' + g.id, sit: why, chg: ev, go: ['goinit', g.id, 'Open initiative'] });
|
|
313
|
-
}
|
|
314
|
-
function inlineRows(list, rows, watch) {
|
|
315
|
-
const byId = new Map(rows.map((g) => [String(g.id), g]));
|
|
316
|
-
const enhance = () => list.querySelectorAll('[data-c3pg-prow]').forEach((btn) => {
|
|
317
|
-
if (btn.hasAttribute('data-expand')) return;
|
|
318
|
-
const g = byId.get(btn.getAttribute('data-c3pg-prow')); if (!g) return;
|
|
319
|
-
const id = String(g.id); const xp = makeXp(); let built = false;
|
|
320
|
-
(btn.closest('.prow') || btn).after(xp); btn.setAttribute('data-a', 'pulsexp'); btn.setAttribute('aria-label', 'Show details: ' + (g.title || g.id));
|
|
321
|
-
attachExpand(btn, xp, { key: 'pulse-' + id, initial: PULSE_OPEN.has(id), onToggle: (on) => {
|
|
322
|
-
if (on) { PULSE_OPEN.add(id); if (!built) { built = true; xp.firstChild.append(pulseCase(g)); } } else PULSE_OPEN.delete(id);
|
|
323
|
-
} });
|
|
324
|
-
});
|
|
325
|
-
enhance();
|
|
326
|
-
if (watch) new MutationObserver(enhance).observe(list, { childList: true });
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function pulse() {
|
|
330
|
-
const body = el('div', null, el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'Reading the initiatives.'));
|
|
331
|
-
const box = el('section', { class: 'card', 'data-c3pg': 'pulse', style: 'gap:8px' },
|
|
332
|
-
el('div', { class: 'chips2', style: 'justify-content:space-between' },
|
|
333
|
-
el('h3', null, c3ic('init', 'sm'), 'Portfolio pulse'), el('span', { class: 'mono dim', style: 'font-size:var(--fs-1)' }, 'promised vs forecast')), body);
|
|
334
|
-
const draw = (rows, err) => {
|
|
335
|
-
if (err) { body.replaceChildren(el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status' }, 'The initiatives could not be read: ' + err)); return; }
|
|
336
|
-
if (!rows.length) { body.replaceChildren(el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No initiative has a promised date yet, so there is nothing to compare with the forecast.')); return; }
|
|
337
|
-
if (rows.some((g) => g.pulse)) { const parts = mockPulse(rows, S.today, null); body.replaceChildren(...parts); inlineRows(parts[1], rows, true); return; } // demo: the mockup's pulse (its own colours, reasons and mini timelines)
|
|
338
|
-
const noPlan = rows.filter((g) => g.status === 'no plan').length;
|
|
339
|
-
rows = rows.filter((g) => g.status !== 'no plan');
|
|
340
|
-
if (!rows.length) { body.replaceChildren(el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No initiative has a promised date yet, so there is nothing to compare with the forecast.'), noPlan ? el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, noPlan + ' initiatives have no plan date.') : null); return; }
|
|
341
|
-
const by = {};
|
|
342
|
-
rows.forEach((g) => { const k = PST[g.status] ? g.status : 'no plan'; by[k] = (by[k] || 0) + 1; });
|
|
343
|
-
const bar = el('div', { class: 'pbs', role: 'group', 'aria-label': 'Status of initiatives' },
|
|
344
|
-
...Object.keys(by).map((k) => el('span', { style: 'flex:' + by[k] + ';height:10px;border-radius:5px;background:' + PST[k][2], title: by[k] + ' ' + PST[k][1].toLowerCase(), 'aria-label': by[k] + ' ' + PST[k][1].toLowerCase() })));
|
|
345
|
-
const end = (o) => (o && (o.end || o.start)) || null;
|
|
346
|
-
const rowEl = (g) => {
|
|
347
|
-
const k = PST[g.status] ? g.status : 'no plan'; const f = end(g.forecast); const p = g.promised || null;
|
|
348
|
-
const why = p ? 'Forecast ' + (f || 'not set') + ' vs promised ' + p : 'No promised date' + (f ? '; forecast ' + f : '');
|
|
349
|
-
const label = 'Show details: ' + (g.title || g.id);
|
|
350
|
-
return row({ title: g.title || g.id, meta: [why], lead: [el('span', { class: 'dotc ' + PST[k][0] })],
|
|
351
|
-
trail: [el('span', { class: 'mono pst ' + PST_CLS[k] }, PST[k][1] + (g.slip_days > 0 ? ' +' + g.slip_days + ' d' : ''))],
|
|
352
|
-
onOpen: () => {}, openLabel: label, compact: true, attrs: { class: 'prow' },
|
|
353
|
-
openAttrs: { 'data-a': 'pulsexp', 'data-c3pg-prow': g.id, 'aria-label': label } });
|
|
354
|
-
};
|
|
355
|
-
const list = el('div', null, ...rows.slice(0, 8).map(rowEl)); inlineRows(list, rows, false);
|
|
356
|
-
body.replaceChildren(bar, list, rows.length > 8 ? el('div', { class: 'dim mono pnote' }, '+' + (rows.length - 8) + ' more in the Calendar tab') : null, noPlan ? el('div', { class: 'dim mono pnote' }, noPlan + ' more have no plan date.') : null);
|
|
357
|
-
};
|
|
358
|
-
readQuarter().then((j) => {
|
|
359
|
-
if (!j || j.ok === false) draw([], (j && j.error) || 'unreadable'); else { S.today = j.today || ''; draw(j.gantt || []); }
|
|
360
|
-
}).catch((e) => draw([], String((e && e.message) || e)));
|
|
361
|
-
return box;
|
|
362
|
-
}
|
|
363
|
-
|
|
247
|
+
/* ── the Briefing tab: one column, the brief ──
|
|
248
|
+
* In order: the headline deck, Today (c3-brief-today.js), Needs you (c3-brief-needs.js) and Portfolio pulse (c3-brief-pulse.js). The ribbon and the embedded calendar are not here:
|
|
249
|
+
* the calendar is the Calendar tab. The rows below Needs you (drafts, handled, what to stop) and the data-trust card are the briefing's own, when it carries them (the demo's). */
|
|
364
250
|
function briefTab(pane) {
|
|
365
|
-
const
|
|
366
|
-
const deckHost = el('div', { 'data-c3pg': 'deck-host' });
|
|
251
|
+
const col = el('div', { class: 'bcol l' });
|
|
252
|
+
const deckHost = el('div', { 'data-c3pg': 'deck-host' });
|
|
367
253
|
const rvHost = el('div', { 'data-c3pg': 'review-host' }); const hdHost = el('div', { 'data-c3pg': 'handled-host' }); const trHost = el('div', { 'data-c3pg': 'trust-host' }); const ftHost = el('div'); const stHost = el('div', { 'data-c3pg': 'stop-host' }); S.stopHost = stHost; S.needsBox = null;
|
|
368
254
|
S.deckHost = deckHost;
|
|
369
255
|
let sig = '';
|
|
@@ -374,24 +260,16 @@ function briefTab(pane) {
|
|
|
374
260
|
stHost.replaceChildren(...[stopCard(b && b.stop)].filter(Boolean)); trHost.replaceChildren(...(b && b.trust ? [trustCard(b.trust)] : [])); ftHost.replaceChildren(...(b && b.foot ? [footLine(b.foot)] : []));
|
|
375
261
|
if (S.why) whyMark();
|
|
376
262
|
});
|
|
377
|
-
S.needsBox =
|
|
378
|
-
|
|
379
|
-
pane.append(el('div', { class: 'brief' }, el('div', { class: 'bcols' },
|
|
263
|
+
S.needsBox = needsSection(S);
|
|
264
|
+
col.append(deckHost, todaySection(), S.needsBox, rvHost, hdHost, stHost, pulse(S), trHost);
|
|
265
|
+
pane.append(el('div', { class: 'brief one' }, el('div', { class: 'bcols' }, col), ftHost));
|
|
380
266
|
mountBriefing(deckHost, { noTicker: true, onLens: lensChanged });
|
|
381
|
-
|
|
267
|
+
loadBriefPage();
|
|
382
268
|
}
|
|
383
269
|
|
|
384
|
-
/* The day-part switch
|
|
385
|
-
function ribbonGo(label) {
|
|
386
|
-
const b = [...S.panes.brief.querySelectorAll('[data-cv="ribbon"] .seg button')].find((x) => x.textContent.trim() === label);
|
|
387
|
-
if (b) b.click();
|
|
388
|
-
}
|
|
270
|
+
/* The day-part switch: Reflect shows "Carries over" and "What I could stop" (the mockup's vBrief, line 1470). */
|
|
389
271
|
function lensChanged(l) {
|
|
390
|
-
const rf = l === 'Reflect'; if (S.stopHost) S.stopHost.hidden = !rf; setNeedsTitle(S.needsBox, rf);
|
|
391
|
-
const cur = S.panes && S.panes.brief ? S.panes.brief.querySelector('[data-cv="ribbon"] .seg [aria-pressed="true"]') : null;
|
|
392
|
-
const on = cur ? cur.textContent.trim() : '';
|
|
393
|
-
if (l === 'Reflect' && on === 'Today') { S.rAuto = true; ribbonGo('Week'); }
|
|
394
|
-
else if (l !== 'Reflect' && S.rAuto) { S.rAuto = false; ribbonGo('Today'); }
|
|
272
|
+
const rf = l === 'Reflect'; if (S.stopHost) S.stopHost.hidden = !rf; setNeedsTitle(S.needsBox, rf);
|
|
395
273
|
}
|
|
396
274
|
|
|
397
275
|
/* ── global: where a notification goes, the person sheet, the palette and the keys ── */
|
|
@@ -502,11 +380,6 @@ function onKey(e) {
|
|
|
502
380
|
if (rows[ni]) rows[ni].focus();
|
|
503
381
|
return;
|
|
504
382
|
}
|
|
505
|
-
if ('+=-_'.includes(e.key) && e.key.length === 1) {
|
|
506
|
-
const b = S.panes.brief.querySelector('[data-cv="ribbon"] [aria-label^="Zoom ' + (e.key === '+' || e.key === '=' ? 'in' : 'out') + '"]');
|
|
507
|
-
e.stopPropagation(); if (b && !b.disabled) b.click();
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
383
|
if (cur && e.key.length === 1 && 'enar'.includes(e.key)) {
|
|
511
384
|
e.stopPropagation(); e.preventDefault();
|
|
512
385
|
const api = rowApi.get(cur); const draft = cur.classList.contains('drow');
|
|
@@ -633,6 +506,11 @@ function showTab(id, push) {
|
|
|
633
506
|
async function workCount() {
|
|
634
507
|
const j = await pmGet('/api/pm-items').catch(() => null);
|
|
635
508
|
if (!j || !j.ok || !j.lists) return;
|
|
509
|
+
if (j.counts && typeof j.counts.inbox === 'number') { // the Work page's own counts: what is to process (inbox) plus what is on today
|
|
510
|
+
const w = j.counts.inbox + (Number(j.counts.today) || 0);
|
|
511
|
+
S.workBdg.textContent = String(w); S.workBdg.hidden = w === 0;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
636
514
|
const n = (j.lists.triage || []).length + (S.demo ? 0 : (j.lists.today || []).length); // the mockup's Work badge counts what is to triage
|
|
637
515
|
S.workBdg.textContent = String(n); S.workBdg.hidden = n === 0;
|
|
638
516
|
}
|
|
@@ -967,6 +967,7 @@ function wire(host, r) {
|
|
|
967
967
|
if (!root.isConnected || !root.offsetParent) return; // another tab is showing
|
|
968
968
|
const t = e.target; if (t && t.closest && t.closest('input,select,textarea,[contenteditable="true"]')) return;
|
|
969
969
|
if (document.querySelector('[data-shell="sheet"]')) return; // a sheet is open
|
|
970
|
+
if (!root.closest('[data-shell="calpop"]') && document.querySelector('[data-shell="calpop"]')) return; // the calendar popup is open: it takes the keys, not the tab behind it
|
|
970
971
|
if ((e.key === '+' || e.key === '=') && zoomBy(-1)) e.preventDefault();
|
|
971
972
|
else if ((e.key === '-' || e.key === '_') && zoomBy(1)) e.preventDefault();
|
|
972
973
|
else if (rib) return;
|
|
@@ -1101,6 +1101,13 @@
|
|
|
1101
1101
|
.c3 .sheet:focus{outline:none}
|
|
1102
1102
|
.c3 .sheet.bs.dragging{transition:none}
|
|
1103
1103
|
.c3 .sheet .sh2 .ib{flex:none}
|
|
1104
|
+
/* the calendar popup (c3-calpop.js): the sheet, centred and wide on a desktop, the whole screen on a phone */
|
|
1105
|
+
.c3 .sheet.calpop{left:12px;right:12px;margin-inline:auto;width:min(1100px,calc(100% - 24px))}
|
|
1106
|
+
.c3 .sheet.calpop .sb{padding:var(--sp-3);min-width:0}
|
|
1107
|
+
.c3 .calpop-host{min-width:0;max-width:100%}
|
|
1108
|
+
.c3.ph .sheet.calpop{top:0;left:0;right:0;bottom:0;width:auto;max-height:none;border-radius:0;margin:0}
|
|
1109
|
+
.c3.ph .sheet.bs.calpop{max-height:none;animation:none}
|
|
1110
|
+
.c3 .sheet.calpop .ib,.c3 .sheet.calpop .seg button,.c3 .sheet.calpop .tdy{min-height:44px;min-width:44px}
|
|
1104
1111
|
@media (prefers-reduced-motion:reduce){.c3 .sheet,.c3 .scrim,.c3.ph .sheet.bs{animation:none}}
|
|
1105
1112
|
/* a slide that cannot be used: the action goes outside and no outbox is connected. The reason sits under it (c3-case.js). */
|
|
1106
1113
|
.c3 .slt.off{opacity:.55;--tc:var(--muted-foreground)}
|
|
@@ -1484,6 +1491,42 @@
|
|
|
1484
1491
|
.c3 .psess:active{transform:scale(.93)}
|
|
1485
1492
|
@media (max-width:760px){.c3 .psess{min-height:44px}}
|
|
1486
1493
|
.c3 .psess .c{font:var(--fs-1) var(--mono)}
|
|
1494
|
+
/* Briefing as the brief (initiative briefing-brief): one column; Today agenda; own-item rows of Needs you; inline Portfolio pulse */
|
|
1495
|
+
.c3 .brief.one{max-width:900px}
|
|
1496
|
+
.c3 .brief.one .bcols{grid-template-columns:minmax(0,1fr)}
|
|
1497
|
+
.c3 .brief.one .bcol{display:grid;gap:16px;min-width:0}
|
|
1498
|
+
.c3 .c3s2{font-size:var(--fs-2)}
|
|
1499
|
+
.c3 .c3s3{font-size:var(--fs-3)}
|
|
1500
|
+
.c3 .ki.t-bad{color:var(--destructive)}
|
|
1501
|
+
.c3 .ki.t-warn{color:var(--warning)}
|
|
1502
|
+
.c3 .ki.t-ink{color:var(--foreground)}
|
|
1503
|
+
.c3 .trow{display:grid;grid-template-columns:72px minmax(0,1fr);gap:10px;padding:10px 6px;border-bottom:1px solid var(--border);border-radius:8px}
|
|
1504
|
+
.c3 .trow.past{opacity:.55}
|
|
1505
|
+
.c3 .trow.now,.c3 .trow.next{background:var(--muted)}
|
|
1506
|
+
.c3 .trow .tm{font:500 var(--fs-3) var(--mono)}
|
|
1507
|
+
.c3 .trow .tb{display:grid;gap:6px;min-width:0}
|
|
1508
|
+
.c3 .trow .tt{font-size:var(--fs-3)}
|
|
1509
|
+
.c3 .trow .tcols{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
|
|
1510
|
+
.c3 .trow .lab{font-size:var(--fs-1);letter-spacing:.06em;text-transform:uppercase;color:var(--muted-foreground)}
|
|
1511
|
+
.c3 .trow ul{margin:2px 0 0;padding:0;list-style:none;display:grid;gap:4px;font-size:var(--fs-2)}
|
|
1512
|
+
.c3 .trow p{margin:2px 0 0}
|
|
1513
|
+
.c3 .ip{display:grid;gap:8px;padding:8px 0 4px}
|
|
1514
|
+
.c3 .ip .looks{display:flex;flex-wrap:wrap;gap:8px;align-items:center;font-size:var(--fs-2);padding:8px;border-radius:8px;background:var(--muted)}
|
|
1515
|
+
.c3 .ip .ioutcome{width:100%;min-height:36px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--foreground);font:inherit;font-size:var(--fs-3)}
|
|
1516
|
+
.c3 .ip .iacts{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
|
1517
|
+
.c3 .ip .ny{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
|
1518
|
+
.c3 .pulsecard{gap:8px}
|
|
1519
|
+
.c3 .pulsehead{display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
1520
|
+
.c3 .pbs .pb-seg{flex:var(--fr-grow,1);height:10px;border-radius:6px;background:var(--border)}
|
|
1521
|
+
.c3 .pbs .pb-warn{background:var(--warning)}
|
|
1522
|
+
.c3 .pbs .pb-bad{background:var(--destructive)}
|
|
1523
|
+
.c3 .pbs .pb-ok{background:var(--muted-foreground)}
|
|
1524
|
+
.c3 .pwrap{border-radius:8px}
|
|
1525
|
+
@container (max-width:520px){
|
|
1526
|
+
.c3 .trow{grid-template-columns:minmax(0,1fr)}
|
|
1527
|
+
.c3 .trow .tcols{grid-template-columns:minmax(0,1fr)}
|
|
1528
|
+
}
|
|
1529
|
+
.c3.ph .ip .ioutcome{min-height:44px}
|
|
1487
1530
|
|
|
1488
1531
|
/* Time of day (console3 quick fixes R5). data-daypart is set on the page root from the clock: morning, midday, afternoon, evening or night.
|
|
1489
1532
|
* Each part sets two tokens, both very faint: --dp-tint (a wash over the page background, alpha 6% or less) and --dp-warm (the colour of the
|