create-pmos 0.59.2 → 0.60.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/scripts/delivery-index.js +85 -7
- package/payload/scripts/workflow-state.js +3 -3
- package/payload/state/discoveries.md +1 -1
- package/workspace/REMOTE.md +1 -1
- package/workspace/fixtures/initiatives-api/sheet.json +29 -7
- package/workspace/guide/index.md +3 -3
- package/workspace/guide/initiatives-screen.md +25 -1
- package/workspace/guide/layout.md +7 -9
- package/workspace/guide/proactive-engine.md +1 -0
- package/workspace/guide/today.md +23 -1
- package/workspace/lib/calendar-memory.js +257 -0
- package/workspace/lib/console-state.js +1 -1
- package/workspace/lib/doc-category.js +3 -1
- package/workspace/lib/engine/delivery-index.js +85 -7
- package/workspace/lib/engine/workflow-state.js +3 -3
- package/workspace/lib/initiative-files.js +59 -1
- package/workspace/lib/initiative-rows.js +14 -2
- package/workspace/lib/mobile-nav.js +1 -1
- package/workspace/lib/plan-tree.js +44 -5
- package/workspace/lib/proactive-cli.js +16 -1
- package/workspace/lib/routes-initiatives.js +165 -9
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/index.html +1 -1
- package/workspace/public/pm/components.css +70 -0
- package/workspace/public/pm/dest-initiatives.js +7 -1
- package/workspace/public/pm/dest-settings.js +1 -0
- package/workspace/public/pm/frame-copy.js +14 -2
- package/workspace/public/pm/initiative-sheet.js +42 -10
- package/workspace/public/pm/initiatives-api.js +15 -0
- package/workspace/public/pm/initiatives-copy.js +13 -0
- package/workspace/public/pm/initiatives-ui.js +14 -0
- package/workspace/public/pm/mobile.js +4 -5
- package/workspace/public/pm/registry.js +1 -1
- package/workspace/public/pm/tab-term.js +6 -50
- package/workspace/public/pm/today-agenda.js +305 -0
- package/workspace/public/pm/today-copy.js +54 -2
- package/workspace/public/pm/today-day.js +23 -3
- package/workspace/public/pm/today-goals.js +7 -1
- package/workspace/server.js +71 -63
- package/workspace/skills/morning-brief.skill +6 -1
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/scan-consistency.json +5 -0
|
@@ -26,7 +26,7 @@ import { state, setState } from './state.js';
|
|
|
26
26
|
import { dayOf } from './breakdown-copy.js';
|
|
27
27
|
import { COPY, shortDate } from './initiatives-copy.js';
|
|
28
28
|
import { getSheet, getExport, place, placed, leaveNow, setLifecycle } from './initiatives-api.js';
|
|
29
|
-
import { healthSignal, say, confirmSheet, checkBySheet, horizonLabel } from './initiatives-ui.js';
|
|
29
|
+
import { healthSignal, say, confirmSheet, checkBySheet, horizonLabel, rescanRepo } from './initiatives-ui.js';
|
|
30
30
|
|
|
31
31
|
/** The pre-Run-2 detail pane: the fallback, and the home of the legacy gate buttons that "Open gate" reaches. */
|
|
32
32
|
export function legacyOpen(repo, slug) {
|
|
@@ -72,10 +72,12 @@ export function openSheet(repo, slug) {
|
|
|
72
72
|
const waitingHost = el('div', { class: 'in-sheet__waiting-host' });
|
|
73
73
|
const tabsHost = el('div', { class: 'in-sheet__tabs' });
|
|
74
74
|
const panel = el('div', { class: 'in-sheet__panel', 'data-in-panel': '' });
|
|
75
|
+
// The Breakdown pane mounts into its own host, so the breakdown read from the spec can sit above it (scan-consistency R4).
|
|
76
|
+
const bdHost = el('div', { class: 'in-sheet__bd', 'data-in-bd': '' });
|
|
75
77
|
const s = sheet({
|
|
76
78
|
mode: 'auto', title: slug, sub: '', body: [crumb, stepperHost, waitingHost, tabsHost, panel],
|
|
77
79
|
attrs: { class: 'in-sheet', 'data-in-sheet': 'initiative', 'data-in-id': slug },
|
|
78
|
-
onClose: () => { closed = true; unmountBreakdown(
|
|
80
|
+
onClose: () => { closed = true; unmountBreakdown(bdHost); unmountJira(panel); if (current === s) current = null; },
|
|
79
81
|
});
|
|
80
82
|
current = s;
|
|
81
83
|
|
|
@@ -117,6 +119,7 @@ export function openSheet(repo, slug) {
|
|
|
117
119
|
items.push({ sep: true });
|
|
118
120
|
items.push({ label: COPY.sheet.copyCli, onSelect: () => copyCli() });
|
|
119
121
|
items.push({ label: COPY.sheet.exportBreakdown, onSelect: () => exportBreakdown() });
|
|
122
|
+
items.push({ label: COPY.rescan, onSelect: async () => { const a = await rescanRepo(ws); if (a.ok) reload(); } });
|
|
120
123
|
menu({ anchor, items, ariaLabel: COPY.sheet.menuMore(I.title) });
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -182,7 +185,24 @@ export function openSheet(repo, slug) {
|
|
|
182
185
|
const steps = data.stepper || [];
|
|
183
186
|
stepperHost.replaceChildren(el('ol', { class: 'in-stepper', 'aria-label': COPY.sheet.stages, 'data-in-stepper': '' }, steps.map((st) => el('li', {
|
|
184
187
|
class: 'in-stepper__step', 'data-state': st.state, 'data-step': st.id, 'aria-current': st.state === 'current' ? 'step' : null,
|
|
185
|
-
|
|
188
|
+
title: st.state === 'skipped' && st.owed ? COPY.sheet.skippedStep(st.owed) : null,
|
|
189
|
+
}, st.state === 'done' ? icon('check') : st.state === 'skipped' ? icon('circle-alert') : null, el('span', null, st.label)))));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The breakdown read from the spec when nothing is planned (read-only; GET /api/initiatives/<id> `derived`). */
|
|
193
|
+
function derivedSection() {
|
|
194
|
+
const d = data.derived;
|
|
195
|
+
if (!d || !Array.isArray(d.items) || !d.items.length) return null;
|
|
196
|
+
const state = COPY.sheet.derivedStates[d.state] || d.state;
|
|
197
|
+
return el('section', { class: 'in-derived', 'data-in-derived': d.state },
|
|
198
|
+
el('h3', { class: 'in-derived__title' }, COPY.sheet.derivedTitle(d.source)),
|
|
199
|
+
el('p', { class: 'in-note' }, COPY.sheet.derivedNote(d.basis)),
|
|
200
|
+
el('div', { class: 'in-sheet__rows', role: 'list' }, d.items.map((it) => row({
|
|
201
|
+
title: (it.ref ? it.ref + ' · ' : '') + it.title, meta: [state], done: d.state === 'done',
|
|
202
|
+
openLabel: COPY.aria.open(it.title),
|
|
203
|
+
onOpen: () => { const src = String(d.source || '').split(',')[0].trim(); if (src) { s.close(); openDoc(slug, src, src.split('/').pop()); } },
|
|
204
|
+
attrs: { 'data-in-derived-item': it.id, 'data-state': d.state },
|
|
205
|
+
}))));
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
function drawWaiting() {
|
|
@@ -224,7 +244,7 @@ export function openSheet(repo, slug) {
|
|
|
224
244
|
items: [{ id: 'breakdown', label: COPY.sheet.breakdown }, { id: 'files', label: COPY.sheet.files }, { id: 'history', label: COPY.sheet.history }, { id: 'jira', label: COPY.sheet.jira }],
|
|
225
245
|
onChange: (id) => {
|
|
226
246
|
if (id === tab) return;
|
|
227
|
-
if (tab === 'breakdown') unmountBreakdown(
|
|
247
|
+
if (tab === 'breakdown') unmountBreakdown(bdHost);
|
|
228
248
|
if (tab === 'jira') unmountJira(panel);
|
|
229
249
|
tab = id;
|
|
230
250
|
drawTabs(); drawPanel();
|
|
@@ -236,15 +256,27 @@ export function openSheet(repo, slug) {
|
|
|
236
256
|
|
|
237
257
|
function drawPanel() {
|
|
238
258
|
if (!data) return;
|
|
239
|
-
if (tab === 'breakdown') {
|
|
259
|
+
if (tab === 'breakdown') {
|
|
260
|
+
const derived = derivedSection();
|
|
261
|
+
panel.replaceChildren(...(derived ? [derived, bdHost] : [bdHost]));
|
|
262
|
+
mountBreakdown(bdHost, { id: tabId, ws, slug }, { embedded: true });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
240
265
|
if (tab === 'jira') { mountJira(panel, { ws, slug }); return; }
|
|
241
266
|
if (tab === 'files') {
|
|
242
267
|
const files = data.files || [];
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
268
|
+
// Grouped by where the tie came from (scan-consistency R3): the record's own paths, the documents the library's
|
|
269
|
+
// index links to it, and the files git changed for it. A `note` row is a fact about the list, not a file.
|
|
270
|
+
const order = ['record', 'knowledge-graph', 'git'];
|
|
271
|
+
const groups = order.map((src) => [src, files.filter((f) => (f.source || 'record') === src)]).filter(([, list]) => list.length);
|
|
272
|
+
const fileRow = (f) => (f.kind === 'note' ? el('p', { class: 'in-note', 'data-in-file-note': '' }, f.label) : row({
|
|
273
|
+
title: f.label, meta: [f.path, f.commits ? COPY.sheet.commits(f.commits) : null, f.shared ? COPY.sheet.shared : null], openLabel: COPY.aria.open(f.label),
|
|
274
|
+
onOpen: () => { s.close(); openDoc(slug, f.path, f.label, null, f.source === 'git' ? { readonly: true } : undefined); },
|
|
275
|
+
attrs: { 'data-in-file': f.kind, 'data-in-file-source': f.source || 'record' }, openAttrs: { 'data-in-file-open': f.kind },
|
|
276
|
+
}));
|
|
277
|
+
panel.replaceChildren(files.length ? el('div', { class: 'in-files' }, groups.map(([src, list]) => el('section', { class: 'in-files__group', 'data-in-files': src },
|
|
278
|
+
el('h3', { class: 'in-files__title' }, COPY.sheet.fileSource[src] || src),
|
|
279
|
+
el('div', { class: 'in-sheet__rows', role: 'list' }, list.map(fileRow))))) : empty({ text: COPY.sheet.noFiles }));
|
|
248
280
|
return;
|
|
249
281
|
}
|
|
250
282
|
const hist = data.history || [];
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* GET /api/initiatives/<id>/export?ws= the breakdown as text
|
|
11
11
|
* POST /api/placements { ws, events: [placed…] } every move, reorder and check-by
|
|
12
12
|
* POST /api/initiatives { ws, title, lane, anchor, intent?, from? } the New-initiative form and a triage card (one body, no events)
|
|
13
|
+
* POST /api/initiatives/rescan { ws } the Re-scan press: read the repository again now (nothing is written)
|
|
13
14
|
*
|
|
14
15
|
* Every GET of /api/initiatives answers 202 { ok: false, pending: true } until the repository's first derivation lands (2C,
|
|
15
16
|
* about 260 ms a record); a read waits and asks again, so a caller sees the answer or a real error, never the 202.
|
|
@@ -47,6 +48,20 @@ export const adopt = (ws, proposal) => pmPost('/api/placements', { ws, events: [
|
|
|
47
48
|
export const drop = (ws, proposal) => pmPost('/api/placements', { ws, events: [{ ev: 'dropped', proposal }] });
|
|
48
49
|
|
|
49
50
|
/** A new initiative: from the form, or from a triaged capture (`from` is the capture's id). It lands in Later. */
|
|
51
|
+
/** Read the repository again now. Answers when the new derivation has landed (the records part of the sig stops reading
|
|
52
|
+
* `~stale`), or { ok: true, slow: true } when it is still running after about three minutes. */
|
|
53
|
+
export async function rescan(ws) {
|
|
54
|
+
const a = await pmPost('/api/initiatives/rescan', { ws });
|
|
55
|
+
if (!a || !a.ok) return a || { ok: false, error: 'no answer' };
|
|
56
|
+
for (let k = 0; k < 120; k++) {
|
|
57
|
+
await sleep(1500);
|
|
58
|
+
const r = await pmGet('/api/initiatives?ws=' + enc(ws) + '&view=roadmap');
|
|
59
|
+
const rec = r && r.sig ? String(r.sig.records || '') : '';
|
|
60
|
+
if (r && r.ok && rec && !rec.endsWith('~stale')) return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, slow: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
export const createInitiative = (ws, fields) => pmPost('/api/initiatives', { ws, ...fields });
|
|
51
66
|
|
|
52
67
|
export const setLifecycle = (ws, id, state, reason) => pmPost('/api/lifecycle', { ws, initiative_id: id, state, reason: reason || '' });
|
|
@@ -28,6 +28,12 @@ export const COPY = {
|
|
|
28
28
|
roadmap: 'Roadmap',
|
|
29
29
|
list: 'List',
|
|
30
30
|
newInitiative: 'New initiative',
|
|
31
|
+
rescan: 'Re-scan the repository',
|
|
32
|
+
rescanning: 'Re-scanning…',
|
|
33
|
+
rescanHint: 'Read stage, files and breakdown again now',
|
|
34
|
+
rescanDone: (t) => `Read again at ${t}`,
|
|
35
|
+
rescanFailed: (why) => `Could not re-scan: ${why}`,
|
|
36
|
+
rescanSlow: 'The re-scan is still running. The view updates when it ends.',
|
|
31
37
|
reading: 'Reading…',
|
|
32
38
|
readFailed: (why) => `Could not read the initiatives: ${why}`,
|
|
33
39
|
noRepo: 'Add a repository to see its initiatives.',
|
|
@@ -127,6 +133,13 @@ export const COPY = {
|
|
|
127
133
|
openGate: 'Open gate',
|
|
128
134
|
handToAgent: 'Hand to agent',
|
|
129
135
|
noFiles: 'No files yet.',
|
|
136
|
+
fileSource: { record: 'Named by the record', 'knowledge-graph': 'Linked documents', git: 'Changed for it in git' },
|
|
137
|
+
commits: (n) => (n === 1 ? '1 commit' : `${n} commits`),
|
|
138
|
+
shared: 'shared',
|
|
139
|
+
skippedStep: (noun) => `Skipped: ${noun} is still open`,
|
|
140
|
+
derivedTitle: (src) => `From the spec · ${src}`,
|
|
141
|
+
derivedNote: (basis) => `Read from the spec, because the Breakdown has no items yet. Each item shows the state of the whole initiative: ${basis}.`,
|
|
142
|
+
derivedStates: { todo: 'Todo', review: 'Built', done: 'Done' },
|
|
130
143
|
noHistory: 'No history yet.',
|
|
131
144
|
reading: 'Reading…',
|
|
132
145
|
readFailed: (why) => `Could not read the initiative: ${why}`,
|
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* confirmSheet({…}) a sheet that asks for one press (a PM-only act): an optional choice, a reason, Cancel, Confirm
|
|
7
7
|
* checkBySheet({…}) a sheet with a date and Save or Clear
|
|
8
8
|
* say(out, ok) a toast for the answer of a write: the success text, or the server's refusal
|
|
9
|
+
* rescanRepo(ws) read the repository again now, with a toast at the start and at the end (scan-consistency R6)
|
|
9
10
|
* No native dialog is used anywhere here. Every control is a primitive (primitives.js).
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { el } from './dom.js';
|
|
13
14
|
import { button, field, signal, sheet, tabs, toast } from './primitives.js';
|
|
14
15
|
import { COPY } from './initiatives-copy.js';
|
|
16
|
+
import { rescan } from './initiatives-api.js';
|
|
15
17
|
|
|
16
18
|
export const horizonLabel = (h) => COPY.horizons[h] || h;
|
|
17
19
|
export const progressText = (row) => (row && row.progress && row.progress.label) || '';
|
|
@@ -24,6 +26,18 @@ export function healthSignal(health) {
|
|
|
24
26
|
return signal({ text: top.text || COPY.healthUnknown, tone, title: top.source || null, attrs: { 'data-health': health.level } });
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/** Re-scan: read the repository again now. A toast says it started and another says how it ended. Answers the rescan
|
|
30
|
+
* answer; the caller reloads what it shows when `ok`. */
|
|
31
|
+
export async function rescanRepo(ws) {
|
|
32
|
+
toast({ text: COPY.rescanning, timeout: 4000 });
|
|
33
|
+
const a = await rescan(ws);
|
|
34
|
+
const t = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
35
|
+
if (!a || !a.ok) toast({ text: COPY.rescanFailed((a && a.error) || 'no answer'), tone: 'error', timeout: 8000 });
|
|
36
|
+
else toast({ text: a.slow ? COPY.rescanSlow : COPY.rescanDone(t), timeout: 6000 });
|
|
37
|
+
try { if (a && a.ok) window.dispatchEvent(new CustomEvent('pmos:initiatives-changed')); } catch { /* no window */ }
|
|
38
|
+
return a || { ok: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
/** A toast for the answer of a write. A refusal shows the server's own words. */
|
|
28
42
|
export function say(out, okText) {
|
|
29
43
|
if (out && out.ok) { if (okText) toast({ text: okText, timeout: 4000 }); return true; }
|
|
@@ -505,11 +505,10 @@ export function mountMobile(context) {
|
|
|
505
505
|
if (theme) theme.addEventListener('click', toggleTheme);
|
|
506
506
|
document.addEventListener('click', onChromeClick);
|
|
507
507
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (window.visualViewport) window.visualViewport.addEventListener('resize', refit);
|
|
508
|
+
if (window.visualViewport) {
|
|
509
|
+
window.visualViewport.addEventListener('resize', refit);
|
|
510
|
+
window.visualViewport.addEventListener('scroll', refit);
|
|
511
|
+
}
|
|
513
512
|
window.addEventListener('orientationchange', () => setTimeout(refit, 250));
|
|
514
513
|
|
|
515
514
|
// Crossing the breakpoint (rotate a tablet, drag a window narrow) has to re-decide whether the
|
|
@@ -44,7 +44,7 @@ import { pmGet } from './http.js';
|
|
|
44
44
|
import { empty, ensureComponentsCss } from './primitives.js';
|
|
45
45
|
|
|
46
46
|
export const DESTINATIONS = Object.freeze([
|
|
47
|
-
Object.freeze({ id: 'today', label: '
|
|
47
|
+
Object.freeze({ id: 'today', label: 'Agenda', key: 'g t', module: 'dest-today.js', run: 3 }),
|
|
48
48
|
Object.freeze({ id: 'initiatives', label: 'Initiatives', key: 'g i', module: 'dest-initiatives.js', run: 2 }),
|
|
49
49
|
Object.freeze({ id: 'board', label: 'Board', key: 'g b', module: 'dest-board.js', run: 4 }),
|
|
50
50
|
Object.freeze({ id: 'reports', label: 'Reports', key: 'g r', module: 'dest-reports.js', run: 5 }),
|
|
@@ -678,25 +678,7 @@ function syncSize(rec) {
|
|
|
678
678
|
if (!rec.sessionId || rec.attachedId !== rec.sessionId) return;
|
|
679
679
|
const cols = rec.term.cols, rows = rec.term.rows;
|
|
680
680
|
if (!cols || !rows) return;
|
|
681
|
-
if (rec.sentCols === cols && rec.sentRows === rows)
|
|
682
|
-
/* The FIRST statement on an attach goes out at once: until it does, the shell is drawing into the
|
|
683
|
-
* 80×24 it was spawned with. A CHANGE after that waits for the grid to settle. A phone's soft
|
|
684
|
-
* keyboard slides in over about a quarter of a second and the pane is re-fitted on every frame of
|
|
685
|
-
* it; each `resize` the PTY gets makes a full-screen agent CLI repaint everything it shows, so
|
|
686
|
-
* stating every intermediate grid buried the session under a dozen redraws of screens that were
|
|
687
|
-
* gone before they arrived. The local fit is not delayed — only what the shell is told. */
|
|
688
|
-
if (rec.sentCols == null) { sendSize(rec); return; }
|
|
689
|
-
clearTimeout(rec.sizeTimer);
|
|
690
|
-
rec.sizeTimer = setTimeout(() => { rec.sizeTimer = null; sendSize(rec); }, RESIZE_SETTLE_MS);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/** How long the grid must hold still before a change is stated to the PTY. See syncSize. */
|
|
694
|
-
const RESIZE_SETTLE_MS = 120;
|
|
695
|
-
|
|
696
|
-
function sendSize(rec) {
|
|
697
|
-
if (!rec.term || !rec.sessionId || rec.attachedId !== rec.sessionId) return;
|
|
698
|
-
const cols = rec.term.cols, rows = rec.term.rows;
|
|
699
|
-
if (!cols || !rows || (rec.sentCols === cols && rec.sentRows === rows)) return;
|
|
681
|
+
if (rec.sentCols === cols && rec.sentRows === rows) return;
|
|
700
682
|
if (send({ t: 'resize', id: rec.sessionId, cols, rows })) { rec.sentCols = cols; rec.sentRows = rows; }
|
|
701
683
|
}
|
|
702
684
|
|
|
@@ -712,40 +694,19 @@ function note(rec, line, force) {
|
|
|
712
694
|
* The single entry point. Coalesced into one animation frame so the six triggers below cost one
|
|
713
695
|
* measure per frame rather than six. A pane that is not in the document, or has no box yet, is
|
|
714
696
|
* skipped — fitting a zero-sized element is how a terminal ends up 1×1 and stays there.
|
|
715
|
-
*
|
|
716
|
-
* A pane whose box has not moved since it was last fitted is NOT re-measured. Every state change
|
|
717
|
-
* in the app lands here (the `subscribe` below), and a fit is a getComputedStyle round-trip on the
|
|
718
|
-
* terminal's parent per pane — on a phone, that was a forced style pass on every render for a grid
|
|
719
|
-
* that could not have changed. The grid depends on the box and on the cell size; the box is the key
|
|
720
|
-
* here, and the one event that moves the cell size without moving the box (the web font arriving)
|
|
721
|
-
* passes `force`.
|
|
722
697
|
*/
|
|
723
698
|
let refitQueued = false;
|
|
724
|
-
let refitForce = false;
|
|
725
699
|
|
|
726
|
-
export function refitVisible(
|
|
727
|
-
if (force === true) refitForce = true;
|
|
700
|
+
export function refitVisible() {
|
|
728
701
|
if (refitQueued) return;
|
|
729
702
|
refitQueued = true;
|
|
730
703
|
const run = () => {
|
|
731
704
|
refitQueued = false;
|
|
732
|
-
const forced = refitForce;
|
|
733
|
-
refitForce = false;
|
|
734
705
|
for (const rec of panes.values()) {
|
|
735
706
|
if (rec.kind !== 'real' || !rec.fit || !rec.host) continue;
|
|
736
707
|
if (!rec.host.isConnected) continue;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (!forced && rec.fitW === w && rec.fitH === h) { syncSize(rec); continue; }
|
|
740
|
-
try {
|
|
741
|
-
// Measured first, so a pane whose renderer cannot measure yet is retried next time rather
|
|
742
|
-
// than remembered as fitted at a grid it never reached.
|
|
743
|
-
const dims = rec.fit.proposeDimensions();
|
|
744
|
-
if (dims && dims.cols > 0 && dims.rows > 0) {
|
|
745
|
-
if (dims.cols !== rec.term.cols || dims.rows !== rec.term.rows) rec.fit.fit();
|
|
746
|
-
rec.fitW = w; rec.fitH = h;
|
|
747
|
-
}
|
|
748
|
-
} catch { /* a terminal mid-teardown */ }
|
|
708
|
+
if (!rec.host.offsetWidth || !rec.host.offsetHeight) continue;
|
|
709
|
+
try { rec.fit.fit(); } catch { /* a terminal mid-teardown */ }
|
|
749
710
|
// …and state the resulting grid to the PTY. `fit()` only fires xterm's `onResize` when the
|
|
750
711
|
// grid MOVES, so a pane that was measured before it had a session would otherwise never tell
|
|
751
712
|
// the shell how wide it is. See the `attached` handler.
|
|
@@ -763,8 +724,7 @@ if (typeof window !== 'undefined') {
|
|
|
763
724
|
subscribe(() => refitVisible());
|
|
764
725
|
window.addEventListener('resize', () => refitVisible());
|
|
765
726
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) refitVisible(); });
|
|
766
|
-
|
|
767
|
-
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => refitVisible(true)).catch(() => {});
|
|
727
|
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => refitVisible()).catch(() => {});
|
|
768
728
|
}
|
|
769
729
|
|
|
770
730
|
/* ── fixture pane ──────────────────────────────────────────────────────────────────────────── */
|
|
@@ -971,7 +931,6 @@ export function disposeTerm(tab) {
|
|
|
971
931
|
* pane for the id and detaches from the orphan instead. */
|
|
972
932
|
if (rec.kind !== 'real') return;
|
|
973
933
|
if (rec.ro) { try { rec.ro.disconnect(); } catch { /* ignore */ } }
|
|
974
|
-
if (rec.sizeTimer) { clearTimeout(rec.sizeTimer); rec.sizeTimer = null; }
|
|
975
934
|
if (rec.sessionId) {
|
|
976
935
|
send({ t: 'detach', id: rec.sessionId });
|
|
977
936
|
SOCK.attached.delete(rec.sessionId);
|
|
@@ -1099,9 +1058,6 @@ export function termProbe() {
|
|
|
1099
1058
|
*/
|
|
1100
1059
|
export function __handleForTest(m) { return handle(m); }
|
|
1101
1060
|
|
|
1102
|
-
|
|
1103
|
-
* that module fell back to dispatching a synthetic window `resize`, which every resize listener on
|
|
1104
|
-
* the page answered — not only this one. */
|
|
1105
|
-
if (typeof window !== 'undefined') window.pmTerm = { refitVisible, fit: () => refitVisible(), termProbe, panes, requestSpawn };
|
|
1061
|
+
if (typeof window !== 'undefined') window.pmTerm = { refitVisible, termProbe, panes, requestSpawn };
|
|
1106
1062
|
|
|
1107
1063
|
export default mountTerm;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/* pm/today-agenda.js — the Agenda month in the left column of Today (initiative today-goals, PRD G25 to G27).
|
|
2
|
+
*
|
|
3
|
+
* agendaPanel(ctx, h) → the panel: the month on top, the agenda of the picked day or week under it; null when the
|
|
4
|
+
* server has no agenda route (an older workspace), so the day's calendar list shows instead
|
|
5
|
+
* meetingDetail(ctx, id) → the right column for a remembered meeting: what it means, before it, the last time
|
|
6
|
+
* memoryBlock(id) → the series part of that detail, for a meeting of today drawn from the brief
|
|
7
|
+
* litSeries(sel) → the series key of the selected meeting, so the month marks the days it happens
|
|
8
|
+
* refreshAgenda() · agendaSig() · onAgenda(fn)
|
|
9
|
+
*
|
|
10
|
+
* One read feeds it: GET /api/proactive/agenda?month=YYYY-MM. The server derives every answer (how full, the key
|
|
11
|
+
* meeting, readiness, no notes); this file only draws them. A day cell is a button: it picks that day. A week number
|
|
12
|
+
* picks the week. The lens switch says which answer the cells show; the agenda under the month always shows them all.
|
|
13
|
+
* Nothing here writes: the calendar memory changes only through the morning-brief session's brief.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { el } from './dom.js';
|
|
17
|
+
import { button, iconButton, badge, tabs } from './primitives.js';
|
|
18
|
+
import { pmGet } from './http.js';
|
|
19
|
+
import { COPY } from './today-copy.js';
|
|
20
|
+
|
|
21
|
+
const A = COPY.day.ag;
|
|
22
|
+
const DAY0 = 9 * 60;
|
|
23
|
+
const SPAN = 9 * 60;
|
|
24
|
+
|
|
25
|
+
const cache = new Map(); // month -> { data, error, old }
|
|
26
|
+
const ag = { month: null, pick: null, lens: 'overview', showRoutine: false };
|
|
27
|
+
let redraw = () => {};
|
|
28
|
+
let inflight = new Set();
|
|
29
|
+
|
|
30
|
+
export function onAgenda(fn) { redraw = fn; }
|
|
31
|
+
|
|
32
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
33
|
+
const ymOf = (date) => date.slice(0, 7);
|
|
34
|
+
const shiftMonth = (ym, n) => { const [y, m] = ym.split('-').map(Number); const d = new Date(y, m - 1 + n, 1); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`; };
|
|
35
|
+
const dateOf = (s) => { const [y, m, d] = s.split('-').map(Number); return new Date(y, m - 1, d); };
|
|
36
|
+
const label = (s, opts) => { try { return dateOf(s).toLocaleDateString(undefined, opts); } catch { return s; } };
|
|
37
|
+
const dayLabel = (s) => label(s, { weekday: 'short', day: 'numeric', month: 'short' });
|
|
38
|
+
/** ISO 8601 week number of a YYYY-MM-DD date. */
|
|
39
|
+
function isoWeek(s) {
|
|
40
|
+
const d = dateOf(s);
|
|
41
|
+
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
42
|
+
const wd = t.getUTCDay() || 7;
|
|
43
|
+
t.setUTCDate(t.getUTCDate() + 4 - wd);
|
|
44
|
+
const y0 = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
|
|
45
|
+
return Math.ceil(((t - y0) / 86400000 + 1) / 7);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadMonth(month) {
|
|
49
|
+
const key = month || 'now';
|
|
50
|
+
if (inflight.has(key)) return;
|
|
51
|
+
inflight.add(key);
|
|
52
|
+
const out = await pmGet('/api/proactive/agenda' + (month ? '?month=' + encodeURIComponent(month) : ''));
|
|
53
|
+
inflight.delete(key);
|
|
54
|
+
const m = out && out.ok ? out.month : month;
|
|
55
|
+
const prev = cache.get(m);
|
|
56
|
+
const next = out && out.ok ? { data: out, error: null, old: false }
|
|
57
|
+
: { data: prev ? prev.data : null, error: (out && out.error) || A.unread, old: !!(out && out.status === 404) };
|
|
58
|
+
if (!ag.month && out && out.ok) ag.month = out.month;
|
|
59
|
+
if (!month && !(out && out.ok)) cache.set('now', next);
|
|
60
|
+
if (m) cache.set(m, next);
|
|
61
|
+
// `now` moves every read: compare it to the minute, so a read that changes nothing draws nothing
|
|
62
|
+
if (!prev || sigOf(prev) !== sigOf(next)) redraw();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sigOf = (c) => JSON.stringify({ ...c, data: c.data ? { ...c.data, now: String(c.data.now).slice(0, 16) } : null });
|
|
66
|
+
|
|
67
|
+
/** Read the shown month again (the day read calls this, so the month follows the brief and the clock). */
|
|
68
|
+
export function refreshAgenda() { loadMonth(ag.month); }
|
|
69
|
+
|
|
70
|
+
function current() {
|
|
71
|
+
const c = (ag.month && cache.get(ag.month)) || cache.get('now') || null;
|
|
72
|
+
if (!c) { loadMonth(ag.month); return null; }
|
|
73
|
+
return c;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function agendaSig() {
|
|
77
|
+
const c = ag.month ? cache.get(ag.month) : cache.get('now');
|
|
78
|
+
return JSON.stringify([ag, c ? [c.error, c.data && c.data.now ? String(c.data.now).slice(0, 16) : null, c.data && Object.keys(c.data.days || {}).length, c.data && c.data.memory] : null])
|
|
79
|
+
+ (c && c.data ? JSON.stringify(c.data.days).length : 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── finding a meeting across the months read ─────────────────────────────────────────────────────────────────── */
|
|
83
|
+
|
|
84
|
+
function findEvent(id) {
|
|
85
|
+
for (const c of cache.values()) {
|
|
86
|
+
if (!c.data) continue;
|
|
87
|
+
for (const d of Object.values(c.data.days)) {
|
|
88
|
+
const e = d.events.find((x) => x.id === id);
|
|
89
|
+
if (e) return { e, day: d, data: c.data };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The series key of the selected meeting (a remembered one, or today's from the brief), or null. */
|
|
96
|
+
export function litSeries(sel) {
|
|
97
|
+
if (!sel || (sel.type !== 'meeting' && sel.type !== 'event')) return null;
|
|
98
|
+
const f = findEvent(sel.id);
|
|
99
|
+
return f ? { key: f.e.key, title: f.e.title } : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ── the month ────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
103
|
+
|
|
104
|
+
function strip(day, lens) {
|
|
105
|
+
const s = el('div', { class: 'td-ag__strip', 'aria-hidden': 'true' });
|
|
106
|
+
const put = (from, mins, cls) => {
|
|
107
|
+
const a = Math.max(0, from - DAY0); const b = Math.min(SPAN, from - DAY0 + mins);
|
|
108
|
+
if (b <= a) return;
|
|
109
|
+
const n = el('span', { class: cls });
|
|
110
|
+
n.style.setProperty('--td-ag-l', (a / SPAN * 100).toFixed(2) + '%');
|
|
111
|
+
n.style.setProperty('--td-ag-w', ((b - a) / SPAN * 100).toFixed(2) + '%');
|
|
112
|
+
s.append(n);
|
|
113
|
+
};
|
|
114
|
+
if (lens === 'free') for (const [a, b] of day.gaps) put(a, b - a, 'td-ag__blk td-ag__blk--free');
|
|
115
|
+
else for (const e of day.events) { const [h, m] = e.time.split(':').map(Number); put(h * 60 + m, e.minutes, 'td-ag__blk' + (e.imp ? ' td-ag__blk--imp' : '')); }
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readyBadge(day, withWord) {
|
|
120
|
+
if (!day.need) return null;
|
|
121
|
+
return el('span', { class: 'td-ag__ready', 'data-ag-ready': day.ready || 'ok', title: A.readyWord[day.ready || 'ok'] },
|
|
122
|
+
A.readyBadge(day.done, day.need), withWord ? ' ' + A.readyWord[day.ready || 'ok'] : null);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function keyLine(day) {
|
|
126
|
+
if (!day.key) return null;
|
|
127
|
+
const e = day.events.find((x) => x.id === day.key);
|
|
128
|
+
return e ? el('span', { class: 'td-ag__key' }, A.star + ' ' + e.title) : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cell(data, d, lit) {
|
|
132
|
+
const day = data.days[d];
|
|
133
|
+
const lens = ag.lens;
|
|
134
|
+
const kids = [el('span', { class: 'td-ag__num' }, String(Number(d.slice(8))))];
|
|
135
|
+
if (day.events.length && (lens === 'overview' || lens === 'full' || lens === 'free')) kids.push(strip(day, lens));
|
|
136
|
+
if (lens === 'overview') {
|
|
137
|
+
kids.push(keyLine(day));
|
|
138
|
+
if (day.ready === 'risk' || day.ready === 'warn' || (day.ready === 'ok' && !day.past)) kids.push(readyBadge(day));
|
|
139
|
+
if (day.past && day.no_notes) kids.push(el('span', { class: 'td-ag__nonotes' }, A.noNotes(day.no_notes)));
|
|
140
|
+
} else if (lens === 'full') {
|
|
141
|
+
if (day.busy) kids.push(el('span', { class: 'td-ag__meta' }, A.busy(day.busy)));
|
|
142
|
+
} else if (lens === 'key') {
|
|
143
|
+
kids.push(keyLine(day));
|
|
144
|
+
} else if (lens === 'ready') {
|
|
145
|
+
kids.push(readyBadge(day, false));
|
|
146
|
+
if (day.past && day.no_notes) kids.push(el('span', { class: 'td-ag__nonotes' }, A.noNotes(day.no_notes)));
|
|
147
|
+
} else if (lens === 'free' && !day.weekend) {
|
|
148
|
+
kids.push(el('span', { class: 'td-ag__meta' }, day.gaps.length ? A.freeBlocks(day.gaps.length) : A.noFree));
|
|
149
|
+
}
|
|
150
|
+
const picked = ag.pick && ag.pick.type === 'day' && ag.pick.date === d;
|
|
151
|
+
return button({ label: el('span', { class: 'td-ag__in' }, ...kids.filter(Boolean)), variant: 'ghost', onClick: () => pickDay(d),
|
|
152
|
+
attrs: { class: 'td-ag__cell', 'data-ag-day': d, 'data-out': day.in_month ? null : 'true', 'data-past': day.past ? 'true' : null, 'data-today': day.today ? 'true' : null,
|
|
153
|
+
'data-weekend': day.weekend ? 'true' : null, 'data-lit': lit && lit.has(d) ? 'true' : null, 'aria-pressed': picked ? 'true' : 'false', title: A.dayHint(dayLabel(d), day.key ? (day.events.find((e) => e.id === day.key) || {}).title : null) } });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pickDay(d) { ag.pick = { type: 'day', date: d }; ag.showRoutine = false; redraw(); }
|
|
157
|
+
function pickWeek(i) { ag.pick = { type: 'week', i }; ag.showRoutine = false; redraw(); }
|
|
158
|
+
function goMonth(n, data) {
|
|
159
|
+
ag.month = n === 0 ? ymOf(data.today) : shiftMonth(ag.month || data.month, n);
|
|
160
|
+
ag.pick = null;
|
|
161
|
+
if (!cache.has(ag.month)) loadMonth(ag.month);
|
|
162
|
+
redraw();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function month(data, lit) {
|
|
166
|
+
const head = el('div', { class: 'td-ag__mhead' },
|
|
167
|
+
iconButton({ icon: 'chevron-left', ariaLabel: A.prev, size: 'sm', attrs: { 'data-ag': 'prev' }, onClick: () => goMonth(-1, data) }),
|
|
168
|
+
el('b', { class: 'td-ag__mlabel', 'data-ag': 'month' }, label(data.month + '-01', { month: 'long', year: 'numeric' })),
|
|
169
|
+
iconButton({ icon: 'chevron-right', ariaLabel: A.later, size: 'sm', attrs: { 'data-ag': 'later' }, onClick: () => goMonth(1, data) }),
|
|
170
|
+
ymOf(data.today) !== data.month ? button({ label: A.thisMonth, size: 'sm', variant: 'ghost', attrs: { 'data-ag': 'this-month' }, onClick: () => goMonth(0, data) }) : null);
|
|
171
|
+
const lens = tabs({ variant: 'segmented', role: 'radiogroup', ariaLabel: A.title, value: ag.lens, attrs: { 'data-ag': 'lens', class: 'td-ag__lens' },
|
|
172
|
+
items: Object.keys(A.lens).map((k) => ({ id: k, label: A.lens[k] })), onChange: (v) => { ag.lens = v; redraw(); } });
|
|
173
|
+
const wd = el('div', { class: 'td-ag__wk td-ag__wk--head', 'aria-hidden': 'true' }, el('span', null, ''),
|
|
174
|
+
...data.weeks[0].map((d) => el('span', { class: 'td-ag__wd' }, label(d, { weekday: 'short' }))));
|
|
175
|
+
const rows = data.weeks.map((w, i) => {
|
|
176
|
+
const n = isoWeek(w[0]);
|
|
177
|
+
const picked = ag.pick && ag.pick.type === 'week' && ag.pick.i === i;
|
|
178
|
+
return el('div', { class: 'td-ag__wk', role: 'group', 'aria-label': A.weekOf(n) },
|
|
179
|
+
button({ label: A.weekNo(n), variant: 'ghost', size: 'sm', onClick: () => pickWeek(i), attrs: { class: 'td-ag__wno', 'data-ag-week': String(n), 'aria-pressed': picked ? 'true' : 'false', title: A.weekHint(n) } }),
|
|
180
|
+
...w.map((d) => cell(data, d, lit)));
|
|
181
|
+
});
|
|
182
|
+
return [head, lens, el('p', { class: 'td-day__note', 'data-ag': 'lens-help' }, A.lensHelp[ag.lens]),
|
|
183
|
+
el('div', { class: 'td-ag__grid', 'data-ag': 'grid', 'data-lens': ag.lens }, wd, ...rows)];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ── the agenda under the month ───────────────────────────────────────────────────────────────────────────────── */
|
|
187
|
+
|
|
188
|
+
function tiles(day) {
|
|
189
|
+
const key = day.key ? day.events.find((e) => e.id === day.key) : null;
|
|
190
|
+
const t = (k, v, attrs) => el('div', { class: 'td-ag__tile', ...(attrs || {}) }, el('span', { class: 'td-ag__tk' }, A.tiles[k]), el('b', null, v));
|
|
191
|
+
return el('div', { class: 'td-ag__tiles', 'data-ag': 'tiles' },
|
|
192
|
+
t('busy', day.busy ? A.busy(day.busy) : A.noMeetings),
|
|
193
|
+
t('key', key ? key.title : A.noKey),
|
|
194
|
+
t('ready', day.need ? A.readyBadge(day.done, day.need) + ' · ' + A.readyWord[day.ready || 'ok'] : A.noPrep, day.need ? { 'data-ag-ready': day.ready || 'ok' } : null));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function eventRow(h, e) {
|
|
198
|
+
const type = e.src === 'brief' ? 'event' : 'meeting';
|
|
199
|
+
const chips = el('span', { class: 'td-day__chips' },
|
|
200
|
+
e.imp ? badge({ text: A.star, tone: 'outline', attrs: { title: A.means } }) : null,
|
|
201
|
+
e.prep ? el('span', { class: 'td-ag__ready', 'data-ag-ready': e.ready || 'ok' }, A.readyBadge(e.prep_done || 0, e.prep.length)) : null,
|
|
202
|
+
e.ended && !e.notes && (e.imp || e.kind === 'one') ? el('span', { class: 'td-ag__nonotes' }, A.noNotes(1)) : null);
|
|
203
|
+
return h.rowBtn(type, e.id, { lead: e.time, title: e.title, meta: A.kindWord[e.kind] + (e.attendees ? ' · ' + A.people(e.attendees) : ''), trail: chips },
|
|
204
|
+
{ 'data-past': e.ended ? 'true' : null, 'data-ag-event': e.id });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function rowsOf(h, evs) {
|
|
208
|
+
const main = evs.filter((e) => !e.routine);
|
|
209
|
+
const rout = evs.filter((e) => e.routine);
|
|
210
|
+
return [
|
|
211
|
+
main.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'agenda' }, ...main.map((e) => eventRow(h, e))) : null,
|
|
212
|
+
rout.length ? button({ label: ag.showRoutine ? A.hideRoutine : A.routine(rout.length), variant: 'ghost', size: 'sm',
|
|
213
|
+
attrs: { 'data-ag': 'routine', 'aria-expanded': ag.showRoutine ? 'true' : 'false' }, onClick: () => { ag.showRoutine = !ag.showRoutine; redraw(); } }) : null,
|
|
214
|
+
rout.length && ag.showRoutine ? el('ul', { class: 'td-day__rows', 'data-day-list': 'routine' }, ...rout.map((e) => eventRow(h, e))) : null,
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function agendaOf(data, h) {
|
|
219
|
+
const pick = ag.pick || (data.days[data.today] ? { type: 'day', date: data.today } : { type: 'day', date: data.month + '-01' });
|
|
220
|
+
if (pick.type === 'week') {
|
|
221
|
+
const w = data.weeks[pick.i] || data.weeks[0];
|
|
222
|
+
const all = w.flatMap((d) => data.days[d].events);
|
|
223
|
+
const days = w.filter((d) => data.days[d].events.some((e) => !e.routine));
|
|
224
|
+
const rout = all.filter((e) => e.routine);
|
|
225
|
+
return el('div', { class: 'td-ag__agenda', 'data-ag': 'agenda', 'data-ag-pick': 'week:' + isoWeek(w[0]) },
|
|
226
|
+
el('h3', { class: 'td-ag__ah' }, A.weekOf(isoWeek(w[0])), el('span', { class: 'td-day__dim' }, ' · ' + dayLabel(w[0]) + ' – ' + dayLabel(w[6]))),
|
|
227
|
+
days.length ? days.map((d) => el('div', { class: 'td-ag__wday' }, el('h4', null, dayLabel(d), data.days[d].busy ? el('span', { class: 'td-day__dim' }, ' · ' + A.busy(data.days[d].busy)) : null, readyBadge(data.days[d])),
|
|
228
|
+
el('ul', { class: 'td-day__rows' }, ...data.days[d].events.filter((e) => !e.routine).map((e) => eventRow(h, e))))) : el('p', { class: 'td-day__note' }, A.noMeetings),
|
|
229
|
+
rout.length ? button({ label: ag.showRoutine ? A.hideRoutine : A.routine(rout.length), variant: 'ghost', size: 'sm',
|
|
230
|
+
attrs: { 'data-ag': 'routine', 'aria-expanded': ag.showRoutine ? 'true' : 'false' }, onClick: () => { ag.showRoutine = !ag.showRoutine; redraw(); } }) : null,
|
|
231
|
+
rout.length && ag.showRoutine ? el('ul', { class: 'td-day__rows', 'data-day-list': 'routine' }, ...rout.map((e) => eventRow(h, e))) : null);
|
|
232
|
+
}
|
|
233
|
+
const day = data.days[pick.date];
|
|
234
|
+
if (!day) return null;
|
|
235
|
+
// today keeps the day's calendar from the brief (the line at the current time, the goal ties, the prep chips)
|
|
236
|
+
const todayList = day.today && h.todayRows ? h.todayRows() : null;
|
|
237
|
+
return el('div', { class: 'td-ag__agenda', 'data-ag': 'agenda', 'data-ag-pick': 'day:' + pick.date },
|
|
238
|
+
el('h3', { class: 'td-ag__ah' }, day.today ? A.today + ' · ' + dayLabel(pick.date) : dayLabel(pick.date)),
|
|
239
|
+
tiles(day),
|
|
240
|
+
todayList || (day.events.length ? rowsOf(h, day.events) : el('p', { class: 'td-day__note' }, A.noMeetings)));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The panel. `h` lends the day section's helpers: { panel, rowBtn(type, id, parts, mods), todayRows(), sel }. */
|
|
244
|
+
export function agendaPanel(ctx, h) {
|
|
245
|
+
const c = current();
|
|
246
|
+
if (!c) return h.panel('agenda', null, el('p', { class: 'td-day__note' }, A.loading));
|
|
247
|
+
if (!c.data) return c.old ? null : h.panel('agenda', null, el('p', { class: 'td-day__note', role: 'status' }, c.error || A.unread));
|
|
248
|
+
const data = c.data;
|
|
249
|
+
const lit = litSeries(h.sel);
|
|
250
|
+
const litDays = lit && data.series[lit.key] ? new Set(data.series[lit.key].dates) : null;
|
|
251
|
+
const empty = !data.memory || !data.memory.events;
|
|
252
|
+
return h.panel('agenda', null,
|
|
253
|
+
...month(data, litDays),
|
|
254
|
+
litDays ? el('p', { class: 'td-day__note', 'data-ag': 'lit' }, A.lit(lit.title, [...litDays].filter((d) => data.days[d]).length)) : null,
|
|
255
|
+
empty ? el('p', { class: 'td-day__note', 'data-ag': 'empty' }, A.empty) : null,
|
|
256
|
+
agendaOf(data, h));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ── the right column ─────────────────────────────────────────────────────────────────────────────────────────── */
|
|
260
|
+
|
|
261
|
+
function whyLines(e) {
|
|
262
|
+
const out = [];
|
|
263
|
+
if (e.role === 'present') out.push(A.why.present);
|
|
264
|
+
if (e.role === 'decide') out.push(A.why.decide);
|
|
265
|
+
if (e.external) out.push(A.why.external);
|
|
266
|
+
if (e.exec) out.push(A.why.exec);
|
|
267
|
+
if (e.attendees && e.attendees >= 8) out.push(A.why.many(e.attendees));
|
|
268
|
+
if (e.routine) out.push(A.why.routine);
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The series of a meeting: how often it met, what is to come, and the last time, folded. Null when unknown. */
|
|
273
|
+
export function memoryBlock(id) {
|
|
274
|
+
const f = findEvent(id);
|
|
275
|
+
if (!f) return null;
|
|
276
|
+
const s = f.data.series[f.e.key];
|
|
277
|
+
if (!s) return null;
|
|
278
|
+
const last = s.last && s.last.date !== f.day.date ? s.last : null;
|
|
279
|
+
return el('div', { class: 'td-day__notes', 'data-ag': 'memory' },
|
|
280
|
+
el('h4', null, A.means),
|
|
281
|
+
el('p', null, A.kindWord[f.e.kind] + '. ' + (s.count > 1 ? A.series(s.past, s.coming) : A.once)),
|
|
282
|
+
...whyLines(f.e).map((t) => el('p', null, t)),
|
|
283
|
+
last ? el('details', { class: 'td-ag__last', 'data-ag': 'last' }, el('summary', null, A.lastTime(dayLabel(last.date))),
|
|
284
|
+
last.notes ? el('p', null, last.notes) : el('p', { class: 'td-day__note' }, A.noLast),
|
|
285
|
+
last.open && last.open.length ? el('ul', { class: 'td-day__chk' }, ...last.open.map((x) => el('li', null, x))) : null) : null);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function meetingDetail(ctx, id, h) {
|
|
289
|
+
const f = findEvent(id);
|
|
290
|
+
if (!f) return null;
|
|
291
|
+
const e = f.e;
|
|
292
|
+
const end = (() => { const d = new Date(e.end); return `${pad(d.getHours())}:${pad(d.getMinutes())}`; })();
|
|
293
|
+
return [
|
|
294
|
+
h.dhead(A.kindWord[e.kind], e.title),
|
|
295
|
+
el('p', { class: 'td-day__dim td-day__mono' }, `${dayLabel(f.day.date)} · ${e.time}–${end}${e.attendees ? ' · ' + A.people(e.attendees) : ''}`),
|
|
296
|
+
memoryBlock(id),
|
|
297
|
+
e.prep ? el('div', { class: 'td-day__phase', 'data-ag': 'before' }, el('h4', null, A.before),
|
|
298
|
+
el('p', null, A.prepLine(e.prep_done || 0, e.prep.length)),
|
|
299
|
+
el('ul', { class: 'td-day__chk' }, ...e.prep.map((t, i) => el('li', { class: i < (e.prep_done || 0) ? 'td-day__ok' : '' }, t))),
|
|
300
|
+
el('p', { class: e.ready === 'risk' ? 'td-day__warn' : 'td-day__note', 'data-ag-ready': e.ready || 'ok' }, A.readyNote[e.ready || 'ok'])) : null,
|
|
301
|
+
e.ended ? el('div', { class: 'td-day__phase', 'data-ag': 'notes' }, el('h4', null, A.afterNotes),
|
|
302
|
+
e.notes ? el('p', null, e.notes) : el('p', { class: 'td-day__note' }, A.noNotesYet),
|
|
303
|
+
e.open && e.open.length ? [el('h4', null, A.openLines), el('ul', { class: 'td-day__chk' }, ...e.open.map((x) => el('li', null, x)))] : null) : null,
|
|
304
|
+
];
|
|
305
|
+
}
|