create-pmos 0.186.0 → 0.188.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/workflow-state.js +2 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-ib-triage-backlog-test.js +20 -13
- package/workspace/c3-ini-life-test.js +167 -0
- package/workspace/c3-initiatives-test.js +6 -6
- package/workspace/c3-phone-test.js +67 -16
- package/workspace/c3-quiet-test.js +2 -1
- package/workspace/guide/initiative-boards.md +37 -2
- package/workspace/guide/my-work.md +1 -0
- package/workspace/guide/phone-companion.md +19 -4
- package/workspace/lib/day-run.js +4 -3
- package/workspace/lib/engine/boards.js +73 -18
- package/workspace/lib/engine/workflow-state.js +2 -1
- package/workspace/lib/initiative-rows.js +2 -2
- package/workspace/lib/my-work.js +2 -1
- package/workspace/lib/routes-boards.js +119 -6
- package/workspace/lib/routes-meeting-focus.js +19 -7
- package/workspace/lib/vendor/scripts/workflow-state.js +2 -1
- package/workspace/lib/work-assigned.js +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-boards.js +82 -17
- package/workspace/public/pm/c3-demo-focus.js +39 -1
- package/workspace/public/pm/c3-focus.css +53 -0
- package/workspace/public/pm/c3-focus.js +188 -44
- package/workspace/public/pm/c3-ib-initiatives.js +127 -60
- package/workspace/public/pm/c3-ib-items.js +2 -2
- package/workspace/public/pm/c3-ini-life.css +32 -0
- package/workspace/public/pm/c3-ini-life.js +379 -0
- package/workspace/public/pm/c3-ini-ui.js +12 -7
- package/workspace/public/pm/c3-initiatives.js +14 -4
- package/workspace/public/pm/c3-phone.css +3 -1
- package/workspace/public/pm/c3-phone.js +28 -8
- package/workspace/public/pm/console3.css +3 -0
- package/workspace/public/pm/pm-work-view.js +11 -5
- package/workspace/public/pm/run-card.js +12 -3
- package/workspace/public/pm/sessions-panel.js +2 -2
- package/workspace/server.js +25 -3
- package/workspace/test-lists/c3-ini-life.json +9 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* pm/c3-focus.js — the Meeting focus page of console3 (initiative c3-meeting-focus; the mockup the PM approved on 2026-10-12, version 7).
|
|
2
|
-
* openFocus({ event, date, pop, back, meet, mtitle }) closeFocus()
|
|
2
|
+
* openFocus({ event, date, pop, back, phone, note, meet, mtitle }) closeFocus()
|
|
3
3
|
* One calm full-screen page over console3, inside #c3-root: it covers the header, the tabs and the dock (they stay in the DOM, set inert) and Leave
|
|
4
4
|
* focus gives them back. Recording is not touched by leaving. It reads GET /api/meeting-focus every 3 seconds while it is open:
|
|
5
5
|
* header title, time, people, initiative; the clock (starts in, or elapsed and left); REC with Start recording or End meeting (the note dock);
|
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
* the captured items are not drawn, and there is no Leave focus. With back (the phone companion opens it so) the head and the foot have
|
|
16
16
|
* "Back to your day" in place of Open focus in PMOS, which closes the page as Leave focus does. ?focus=now first asks GET /api/meeting-focus/now (meet = the Meet code, mtitle = the tab's title) which
|
|
17
17
|
* meeting the call is; when none fits, the page says so and lists today's meetings to pick from. Fold shows the head only and tells the frame's parent with one
|
|
18
|
-
* message, { pmosFocus: 'fold' | 'open' }, that carries no data.
|
|
18
|
+
* message, { pmosFocus: 'fold' | 'open' }, that carries no data.
|
|
19
|
+
* Phone mode (phone, initiative c3-phone-companion, mockup version 5): the phone companion opens this page when a recording starts. It is the full
|
|
20
|
+
* page in one column: a head with "Back to your day" (recording goes on), the title, the clock and the bar of the talk; the now line; chips that open
|
|
21
|
+
* sheets (Topics, Captured, Note, and Your slides or Material for the stage); What's being said; the conversation; and a foot with Ask PMOS quietly
|
|
22
|
+
* and Stop. Nothing on it leads to the desktop pages. A recording that is not on the calendar (a talk in the corridor) opens with note and no
|
|
23
|
+
* event: GET /api/meeting-focus?note= answers it with no topics, no brief and no stage. The address is not changed. */
|
|
19
24
|
import { el } from './dom.js';
|
|
20
25
|
import { pmGet, pmPost } from './http.js';
|
|
21
26
|
import { ensureC3Css } from './pm-work-view.js';
|
|
@@ -77,27 +82,37 @@ export function closeFocus() {
|
|
|
77
82
|
const host = document.getElementById('c3-root');
|
|
78
83
|
if (host) host.classList.remove('fc-open');
|
|
79
84
|
const grid = host && host.querySelector('.c3grid'); if (grid) grid.inert = false;
|
|
85
|
+
const comp = host && host.querySelector('[data-c3ph]'); if (comp) comp.inert = false;
|
|
86
|
+
if (f.phone) { const st = f.phone && document.querySelector('[data-c3ph-focus-bar]'); if (st) st.focus({ preventScroll: true }); return; }
|
|
80
87
|
try { const u = new URL(location.href); if (u.searchParams.has('focus')) { u.searchParams.delete('focus'); u.searchParams.delete('fdate'); history.replaceState(history.state, '', u); } } catch { /* no history */ }
|
|
81
88
|
if (f.opener && document.contains(f.opener) && typeof f.opener.focus === 'function') { try { f.opener.focus({ preventScroll: true }); } catch { /* not focusable any more */ } }
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
export function openFocus({ event, date, pop, back, meet, mtitle } = {}) {
|
|
91
|
+
export function openFocus({ event, date, pop, back, phone, note, meet, mtitle } = {}) {
|
|
85
92
|
closeFocus();
|
|
86
93
|
const ev = event || {};
|
|
87
94
|
const want = String(ev.id || '');
|
|
95
|
+
if (phone) pop = false; // the phone has the whole page, in one column
|
|
88
96
|
const resolve = !!pop && want === 'now'; // the extension names no meeting: which one this call is comes first
|
|
89
97
|
const id = resolve ? '' : want;
|
|
90
98
|
const day = String(date || String(ev.at || ev.start || '').slice(0, 10) || '');
|
|
91
99
|
ensureCss();
|
|
92
100
|
const host = document.getElementById('c3-root') || document.body;
|
|
93
|
-
const root = el('div', { class: 'c3focus' + (pop ? ' fc-pop' : ''), role: 'region', 'aria-label': pop ? 'PMOS meeting helper' : 'Meeting focus', 'data-c3focus': '', ...(pop ? { 'data-fc-pop': '' } : {}) });
|
|
101
|
+
const root = el('div', { class: 'c3focus' + (pop ? ' fc-pop' : '') + (phone ? ' fc-phone' : ''), role: 'region', 'aria-label': pop ? 'PMOS meeting helper' : 'Meeting focus', 'data-c3focus': '', ...(pop ? { 'data-fc-pop': '' } : {}), ...(phone ? { 'data-fc-phone': '' } : {}) });
|
|
94
102
|
const r = {};
|
|
103
|
+
r.form = askForm();
|
|
95
104
|
const convo = el('section', { class: 'fc-cv', 'aria-label': 'The conversation, with PMOS' },
|
|
96
|
-
pop ? null : el('div', { class: 'fc-cv-h' }, el('b', null, 'Conversation'), el('span', { class: 'dim' }, 'PMOS replies only to you')),
|
|
105
|
+
pop || phone ? null : el('div', { class: 'fc-cv-h' }, el('b', null, 'Conversation'), el('span', { class: 'dim' }, 'PMOS replies only to you')),
|
|
97
106
|
r.tick = el('div', { class: 'fc-tk', 'aria-label': 'Last lines in the room', 'data-fc': 'ticker' }),
|
|
98
107
|
r.stream = el('ol', { class: 'fc-s2', role: 'list', 'data-fc': 'stream' }),
|
|
99
|
-
r.form
|
|
100
|
-
const page =
|
|
108
|
+
phone ? null : r.form);
|
|
109
|
+
const page = phone
|
|
110
|
+
? el('div', { class: 'fc-page' },
|
|
111
|
+
r.top = el('header', { class: 'fc-phd', 'data-fc': 'phone-head' }),
|
|
112
|
+
r.now = el('div', { class: 'fc-phnow', 'data-fc': 'now-line' }),
|
|
113
|
+
r.band = el('div', { class: 'fc-bandwrap' }), convo,
|
|
114
|
+
el('div', { class: 'fc-phft', 'data-fc': 'phone-foot' }, r.form, r.act = el('span', { class: 'fc-phact' })))
|
|
115
|
+
: pop
|
|
101
116
|
? el('div', { class: 'fc-page' },
|
|
102
117
|
r.top = el('header', { class: 'fc-ph', 'data-fc': 'pop-head' }),
|
|
103
118
|
el('div', { class: 'fc-popbody', 'data-fc': 'pop-body' },
|
|
@@ -114,14 +129,15 @@ export function openFocus({ event, date, pop, back, meet, mtitle } = {}) {
|
|
|
114
129
|
el('aside', { class: 'fc-w', 'aria-label': 'Conversation' }, convo)));
|
|
115
130
|
root.append(page);
|
|
116
131
|
F = { id, date: day, ev0: ev, root, r, sig: {}, data: null, offset: 0, ui: { open: new Set(), folded: new Set(), pulled: [], stage: 0, picker: false, newTopic: false, addTopic: false, heldOpen: false, pending: new Map(), failed: new Map(), pill: false },
|
|
117
|
-
pop: !!pop, back: !!back, meet: String(meet || ''), mtitle: String(mtitle || ''), choices: null,
|
|
132
|
+
pop: !!pop, back: !!back, phone: !!phone, noteId: id ? '' : String(note || ''), meet: String(meet || ''), mtitle: String(mtitle || ''), choices: null,
|
|
118
133
|
timers: {}, askTimers: new Map(), sheet: null, sheetOpen: false, status: 'reading', error: '',
|
|
119
134
|
opener: document.activeElement && document.activeElement !== document.body ? document.activeElement : null, seenNotes: 0 };
|
|
120
135
|
// a fixed box inside an element with container-type is placed against that element, not the screen: lift it while the page is open
|
|
121
136
|
host.classList.add('fc-open');
|
|
122
137
|
host.append(root);
|
|
123
138
|
const grid = host.querySelector && host.querySelector('.c3grid'); if (grid) grid.inert = true;
|
|
124
|
-
|
|
139
|
+
const comp = host.querySelector && host.querySelector('[data-c3ph]'); if (comp) comp.inert = true; // the phone companion under the page
|
|
140
|
+
if (!pop && !phone) { try { const u = new URL(location.href); u.searchParams.set('focus', id); if (day) u.searchParams.set('fdate', day); history.replaceState(history.state, '', u); } catch { /* no history */ } } // the compact page keeps its address: a reload asks again which meeting it is
|
|
125
141
|
draw();
|
|
126
142
|
F.timers.clock = setInterval(tick, 1000);
|
|
127
143
|
if (resolve) resolveNow(); else load();
|
|
@@ -162,8 +178,9 @@ async function load() {
|
|
|
162
178
|
const e = f.ev0;
|
|
163
179
|
const p = new URLSearchParams();
|
|
164
180
|
if (f.id) p.set('event', f.id);
|
|
165
|
-
if (f.
|
|
166
|
-
if (
|
|
181
|
+
else if (f.noteId) p.set('note', f.noteId); // a recording with no meeting
|
|
182
|
+
if (f.date && !f.noteId) p.set('date', f.date);
|
|
183
|
+
if (!f.id && !f.noteId && (e.at || e.start)) { p.set('at', e.at || e.start); if (e.title) p.set('title', e.title); }
|
|
167
184
|
const j = await pmGet('/api/meeting-focus?' + p.toString());
|
|
168
185
|
if (F !== f) return;
|
|
169
186
|
if (j && j.ok) {
|
|
@@ -199,6 +216,7 @@ function draw() {
|
|
|
199
216
|
const d = F.data;
|
|
200
217
|
F.root.dataset.fcState = d ? 'ok' : F.status;
|
|
201
218
|
if (F.pop) { drawPop(d); return; }
|
|
219
|
+
if (F.phone) { drawPhone(d); return; }
|
|
202
220
|
if (!d) {
|
|
203
221
|
const title = F.ev0.title || 'Meeting';
|
|
204
222
|
region('top', ['wait', F.status, F.error], () => [el('div', { class: 'fc-id' }, leaveBtn(), el('div', { class: 'fc-tt' }, el('h1', null, title),
|
|
@@ -249,6 +267,107 @@ function popMessage() {
|
|
|
249
267
|
if (F.status === 'reading') return [el('p', { class: 'dim mono fc-popmsg', role: 'status', 'data-fc': 'pop-msg' }, 'Reading the meeting.')];
|
|
250
268
|
return [el('p', { class: 'dim mono fc-popmsg', role: 'status', 'data-fc': 'pop-msg' }, F.status === 'missing' ? 'PMOS has no record of this meeting on the calendar, so there is nothing to show.' : F.error || 'The meeting could not be read.')];
|
|
251
269
|
}
|
|
270
|
+
/* ── phone mode: the full page in one column ────────────────────────────────────────────────────────────────── */
|
|
271
|
+
|
|
272
|
+
function drawPhone(d) {
|
|
273
|
+
if (!d) {
|
|
274
|
+
const title = F.ev0.title || (F.noteId ? 'Conversation' : 'Meeting');
|
|
275
|
+
region('top', ['wait', F.status, F.error], () => [el('div', { class: 'fc-phid' }, backBtn(), el('div', { class: 'fc-tt' }, el('h1', null, title),
|
|
276
|
+
el('div', { class: 'dim mono fc-sub', role: 'status' }, F.status === 'missing' ? (F.noteId ? 'PMOS could not find the notes of this recording.' : 'PMOS has no record of this meeting on the calendar, so there is nothing to show.') : F.status === 'error' ? F.error : 'Reading the meeting.')))]);
|
|
277
|
+
for (const k of ['now', 'band', 'tick', 'stream', 'act']) region(k, 'wait', () => []);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const ph = phaseOf();
|
|
281
|
+
drawPhTop(d, ph); drawPhNow(d, ph); drawBand(d); drawConversation(d); drawPhAct(d, ph);
|
|
282
|
+
tick();
|
|
283
|
+
}
|
|
284
|
+
const backBtn = () => ib('Back to your day', 'prev', { 'data-fc': 'leave', title: 'Back to your day. It keeps recording.' });
|
|
285
|
+
function drawPhTop(d, ph) {
|
|
286
|
+
const e = d.event; const rec = recState(d); const ini = (d.brief && d.brief.initiative) || null;
|
|
287
|
+
const sig = [e.title, e.at, e.end, e.attendees, ini, e.initiative, ph, rec, !!d.talk, d.topics.map((t) => t.id), d.runs, F.status === 'ok' && F.stale ? 1 : 0];
|
|
288
|
+
region('top', sig, () => {
|
|
289
|
+
const s = startMs(); const f = endMs();
|
|
290
|
+
const people = e.attendees || [];
|
|
291
|
+
const sub = d.talk ? [Number.isFinite(s) ? 'Started ' + hhmm(s) : null, 'not on the calendar']
|
|
292
|
+
: [Number.isFinite(s) ? hhmm(s) + (Number.isFinite(f) ? ' to ' + hhmm(f) : '') : null, ini ? ini.title : e.initiative, people.length ? people.length + (people.length === 1 ? ' person' : ' people') : null];
|
|
293
|
+
const clock = ph === 'before'
|
|
294
|
+
? [el('b', { class: 'mono', 'data-fc': 'big' }, clockText((s - nowMs()) / 1000)), el('span', { class: 'dim mono' }, 'starts in')]
|
|
295
|
+
: [el('span', { class: 'fc-recd', 'data-fc': 'rec', 'data-rec': rec, title: rec === 'recording' ? 'Recording' : rec === 'paused' ? 'Paused' : '' }, rec === 'recording' ? el('span', { class: 'fc-dot' }) : null,
|
|
296
|
+
el('b', { class: 'mono', 'data-fc': 'big' }, clockText((nowMs() - s) / 1000))),
|
|
297
|
+
Number.isFinite(f) ? el('span', { class: 'dim mono' }, el('span', { 'data-fc': 'left' }, clockText((f - nowMs()) / 1000)), ' left') : rec === 'paused' ? el('span', { class: 'dim mono' }, 'paused') : null];
|
|
298
|
+
return [el('div', { class: 'fc-phid' }, backBtn(),
|
|
299
|
+
el('div', { class: 'fc-tt' }, el('h1', null, e.title), el('div', { class: 'dim mono fc-sub' }, sub.filter(Boolean).join(' · '))),
|
|
300
|
+
el('div', { class: 'fc-phclk', role: 'timer', 'aria-live': 'off' }, ...clock)),
|
|
301
|
+
d.talk || !d.runs.length ? null : trackEl(d),
|
|
302
|
+
F.stale ? el('p', { class: 'dim mono fc-stale', role: 'status' }, 'The last read failed. Showing what PMOS had.') : null];
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function drawPhNow(d, ph) {
|
|
306
|
+
const cur = currentOf(d); const nc = d.topics.filter((t) => t.state === 'notyet').length; const cap = capCount(d);
|
|
307
|
+
const links = linksOf(d); const slides = links.some((l) => l.kind === 'slides');
|
|
308
|
+
region('now', [cur && cur.id, cur && cur.text, d.topics.length, nc, cap, d.note && d.note.id, ph, d.topics[0] && d.topics[0].text, !!d.talk, links.length, slides], () => [
|
|
309
|
+
d.talk ? null : cur
|
|
310
|
+
? el('div', { class: 'fc-nowt' }, el('span', { class: 'ki hot' }, icon('play', 'sm')), el('b', { 'data-fc': 'topic' }, cur.text), el('span', { class: 'mono dim', 'data-fc': 'onfor' }, ''))
|
|
311
|
+
: el('div', { class: 'fc-nowt dim', 'data-fc': 'first' }, d.topics.length ? [ph === 'before' ? 'First up: ' : 'No topic yet. First up: ', el('b', null, d.topics[0].text)] : 'No topics yet. PMOS adds topics as they come up.'),
|
|
312
|
+
el('div', { class: 'fc-phchips', role: 'group', 'aria-label': 'Open beside the talk' },
|
|
313
|
+
d.talk ? null : lk({ 'data-fc': 'topics' }, icon('todo', 'sm'), d.topics.length + ' topic' + (d.topics.length === 1 ? '' : 's') + (cur && nc ? ' · ' + nc + ' not yet' : '')),
|
|
314
|
+
lk({ 'data-fc': 'captured' }, icon('check', 'sm'), 'Captured ' + cap),
|
|
315
|
+
d.note ? lk({ 'data-fc': 'note' }, icon('notes', 'sm'), 'Note') : null,
|
|
316
|
+
links.length ? lk({ 'data-fc': 'material' }, icon('doc', 'sm'), slides ? 'Your slides' : 'Material') : null)]);
|
|
317
|
+
}
|
|
318
|
+
function drawPhAct(d, ph) {
|
|
319
|
+
const rec = recState(d);
|
|
320
|
+
region('act', [rec, ph], () => (rec === 'recording' || rec === 'paused' ? [pbtn('outline', 'Stop', 'stop', { 'data-fc': 'stop', title: 'Stop recording' })]
|
|
321
|
+
: rec !== 'ended' && ph !== 'after' && !d.talk ? [pbtn('primary', 'Record', 'play', { 'data-fc': 'start', title: 'Records this meeting in a note' })] : []));
|
|
322
|
+
}
|
|
323
|
+
function stopBuild() {
|
|
324
|
+
const d = F.data; const f = endMs(); const left = Number.isFinite(f) ? Math.round((f - nowMs()) / 60000) : 0;
|
|
325
|
+
return [el('div', { class: 'fc-stopsheet', 'data-fc': 'stop-sheet' },
|
|
326
|
+
el('p', { class: 'fc-p' }, d && d.talk ? 'PMOS keeps the notes and shows you what it caught.'
|
|
327
|
+
: left > 0 ? left + ' min of the meeting is left. If you stop now, the notes use what was recorded so far.' : 'PMOS keeps the notes and writes them up from the recording.'),
|
|
328
|
+
el('div', { class: 'iacts' }, pbtn('primary', 'Stop recording', 'stop', { 'data-fc': 'stop-ok' }), pbtn('ghost', 'Keep recording', null, { 'data-fc': 'stop-keep' })))];
|
|
329
|
+
}
|
|
330
|
+
function materialBuild() { return stageBody(F.data, phaseOf()); }
|
|
331
|
+
|
|
332
|
+
/* the live note, in a sheet: what PMOS picked out so far, or the last lines of the transcript (GET /api/notes/one) */
|
|
333
|
+
const NOTE_MS = 6000;
|
|
334
|
+
async function readNote() {
|
|
335
|
+
const f = F; if (!f || !f.data || !f.data.note) return;
|
|
336
|
+
const id = f.data.note.id;
|
|
337
|
+
if (f.noteBusy || (f.noteDoc && f.noteDoc.id === id && Date.now() - f.noteDoc.at < NOTE_MS)) return;
|
|
338
|
+
f.noteBusy = true;
|
|
339
|
+
try {
|
|
340
|
+
const [dn, vn, demo] = await Promise.all([import('./dest-notes.js'), import('./view-notes.js'), isDemo() ? import('./c3-demo-focus.js') : null]);
|
|
341
|
+
const j = demo ? demo.focusNote(id, f.data) : await pmGet('/api/notes/one?id=' + encodeURIComponent(id));
|
|
342
|
+
const n = j && j.ok !== false ? j.note : null;
|
|
343
|
+
f.noteDoc = { id, at: Date.now(), n, parts: n ? dn.partsOf(n) : null, speakerOf: vn.speakerOf };
|
|
344
|
+
} catch { f.noteDoc = { id, at: Date.now(), n: null, parts: null }; }
|
|
345
|
+
f.noteBusy = false;
|
|
346
|
+
if (F === f) refreshSheet();
|
|
347
|
+
}
|
|
348
|
+
function noteBuild() {
|
|
349
|
+
const doc = F.noteDoc; const tab = F.ui.noteTab || 'note';
|
|
350
|
+
const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Show' },
|
|
351
|
+
...[['note', 'Note'], ['said', 'Transcript']].map(([k, l]) => button({ variant: 'ghost', label: l, attrs: { 'aria-pressed': String(tab === k), 'data-fc': 'note-tab', 'data-tab': k } })));
|
|
352
|
+
if (!doc) return [el('div', { class: 'fc-notesheet', 'data-fc': 'note-sheet' }, seg, el('p', { class: 'dim fc-note', role: 'status' }, 'Opening the note.'))];
|
|
353
|
+
if (!doc.n) return [el('div', { class: 'fc-notesheet', 'data-fc': 'note-sheet' }, seg, el('p', { class: 'dim fc-note', role: 'status' }, 'The note could not be read. It is kept on your computer.'))];
|
|
354
|
+
const body = [];
|
|
355
|
+
if (tab === 'said') {
|
|
356
|
+
const lines = (doc.n.transcript || []).filter((x) => x && x.text && !x.dropped).slice(-12);
|
|
357
|
+
body.push(lines.length ? el('ul', { class: 'fc-said', role: 'list', 'data-fc': 'note-said' }, ...lines.map((x) => el('li', null, el('b', null, (doc.speakerOf ? doc.speakerOf(doc.n, x.source) : x.source) + ' '), x.text)))
|
|
358
|
+
: el('p', { class: 'dim fc-note' }, 'Nothing is heard yet.'));
|
|
359
|
+
body.push(el('p', { class: 'dim fc-note' }, 'Fast lines are provisional and corrected at each pause.'));
|
|
360
|
+
} else {
|
|
361
|
+
const p = doc.parts || { overview: '', decisions: [], actions: [], questions: [] };
|
|
362
|
+
const list = (h, rows, fmt) => (rows.length ? el('div', { 'data-fc-part': h.toLowerCase() }, el('div', { class: 'dim mono fc-lbl' }, h + ' · ' + rows.length), el('ul', { class: 'fc-said', role: 'list' }, ...rows.map((x) => el('li', null, fmt(x))))) : null);
|
|
363
|
+
if (p.overview) body.push(el('p', { class: 'fc-p' }, p.overview));
|
|
364
|
+
body.push(list('Decisions', p.decisions || [], (x) => x.text), list('Actions', p.actions || [], (x) => [x.text, x.owner ? ' · ' + x.owner : '', x.due ? ' · due ' + x.due : ''].join('')), list('Open questions', p.questions || [], (x) => x.text));
|
|
365
|
+
if (!p.overview && !(p.decisions || []).length && !(p.actions || []).length && !(p.questions || []).length) body.push(el('p', { class: 'dim fc-note' }, 'PMOS writes the note as the talk goes. Nothing is picked out yet.'));
|
|
366
|
+
}
|
|
367
|
+
return [el('div', { class: 'fc-notesheet', 'data-fc': 'note-sheet' }, seg, ...body.filter(Boolean), el('p', { class: 'dim fc-note' }, 'The whole note opens on your computer.'))];
|
|
368
|
+
}
|
|
369
|
+
function openNoteSheet() { F.ui.noteTab = F.ui.noteTab || 'note'; showSheet('note', 'Live note', 'notes', noteBuild); readNote(); }
|
|
370
|
+
|
|
252
371
|
/** The full focus page, in a new tab: the compact page's way to everything it leaves out. */
|
|
253
372
|
function fullUrl() {
|
|
254
373
|
const u = new URL('/c3.html', location.origin);
|
|
@@ -276,6 +395,23 @@ function leftText(d, ph) {
|
|
|
276
395
|
return { txt: warn ? nc + ' topic' + (nc > 1 ? 's' : '') + ' left for ' + left + ' min' : '', warn };
|
|
277
396
|
}
|
|
278
397
|
|
|
398
|
+
/** The thin bar of the stretches of talk, numbered by topic, with a mark for now. */
|
|
399
|
+
function trackEl(d) {
|
|
400
|
+
const s = startMs(); const f = endMs();
|
|
401
|
+
const total = Number.isFinite(s) && Number.isFinite(f) ? (f - s) / 60000 : 60;
|
|
402
|
+
const track = d.runs.map((r) => {
|
|
403
|
+
const open = r.to_min == null;
|
|
404
|
+
const to = open ? Math.max(r.from_min, (nowMs() - s) / 60000) : r.to_min;
|
|
405
|
+
const w = Math.max(0, (to - r.from_min) / total * 100);
|
|
406
|
+
const t = d.topics.find((x) => x.id === r.topic);
|
|
407
|
+
const seg = el('i', { class: open ? 'c' : 'd', ...(open ? { 'data-fc': 'currun' } : {}), title: (t ? t.text : '') + ' · ' + Math.round(to - r.from_min) + ' min' }, w >= 6 ? String(topicNo(d, r.topic)) : '');
|
|
408
|
+
seg.style.setProperty('--fc-left', Math.min(100, r.from_min / total * 100) + '%');
|
|
409
|
+
seg.style.setProperty('--fc-width', Math.min(100, w) + '%');
|
|
410
|
+
return seg;
|
|
411
|
+
});
|
|
412
|
+
return el('div', { class: 'fc-track', role: 'img', 'aria-label': 'What the room talked about, in order. Numbers match the topic list.' }, el('div', { class: 'fc-runs' }, ...track), el('span', { class: 'fc-nowm', 'data-fc': 'nowm' }));
|
|
413
|
+
}
|
|
414
|
+
|
|
279
415
|
function drawTop(d, ph) {
|
|
280
416
|
const e = d.event; const rec = recState(d); const ini = (d.brief && d.brief.initiative) || null; const lt = leftText(d, ph);
|
|
281
417
|
const sig = [e.title, e.at, e.end, e.attendees, ini, e.initiative, ph, rec, lt.txt, d.topics.map((t) => t.id), d.runs, F.status === 'ok' && F.stale ? 1 : 0];
|
|
@@ -289,17 +425,6 @@ function drawTop(d, ph) {
|
|
|
289
425
|
: [el('div', null, el('b', { class: 'mono', 'data-fc': 'big' }, clockText((nowMs() - s) / 1000)), el('span', { class: 'dim mono' }, 'elapsed')),
|
|
290
426
|
Number.isFinite(f) ? el('div', null, el('b', { class: 'mono', 'data-fc': 'left' }, clockText((f - nowMs()) / 1000)), el('span', { class: 'dim mono' }, 'left')) : null];
|
|
291
427
|
const recLabel = rec === 'recording' ? 'REC' : rec === 'paused' ? 'Paused' : rec === 'ended' ? 'Ended' : '';
|
|
292
|
-
const track = d.runs.map((r) => {
|
|
293
|
-
const total = Number.isFinite(s) && Number.isFinite(f) ? (f - s) / 60000 : 60;
|
|
294
|
-
const open = r.to_min == null;
|
|
295
|
-
const to = open ? Math.max(r.from_min, (nowMs() - s) / 60000) : r.to_min;
|
|
296
|
-
const w = Math.max(0, (to - r.from_min) / total * 100);
|
|
297
|
-
const t = d.topics.find((x) => x.id === r.topic);
|
|
298
|
-
const seg = el('i', { class: open ? 'c' : 'd', ...(open ? { 'data-fc': 'currun' } : {}), title: (t ? t.text : '') + ' · ' + Math.round(to - r.from_min) + ' min' }, w >= 6 ? String(topicNo(d, r.topic)) : '');
|
|
299
|
-
seg.style.setProperty('--fc-left', Math.min(100, r.from_min / total * 100) + '%');
|
|
300
|
-
seg.style.setProperty('--fc-width', Math.min(100, w) + '%');
|
|
301
|
-
return seg;
|
|
302
|
-
});
|
|
303
428
|
return [
|
|
304
429
|
el('div', { class: 'fc-id' }, leaveBtn(),
|
|
305
430
|
el('div', { class: 'fc-tt' }, el('h1', null, e.title),
|
|
@@ -310,7 +435,7 @@ function drawTop(d, ph) {
|
|
|
310
435
|
recLabel ? el('span', { class: 'dim mono fc-recd', 'data-fc': 'rec', 'data-rec': rec, title: rec === 'recording' ? 'Recording this meeting' : '' }, rec === 'recording' ? el('span', { class: 'fc-dot' }) : null, recLabel) : null,
|
|
311
436
|
rec === 'recording' || rec === 'paused' ? pbtn('outline', 'End meeting', 'stop', { 'data-fc': 'end', title: 'Stops the recording and saves the note' })
|
|
312
437
|
: rec !== 'ended' && ph !== 'after' ? pbtn('primary', 'Start recording', 'play', { 'data-fc': 'start', title: 'Records this meeting in a note' }) : null),
|
|
313
|
-
|
|
438
|
+
trackEl(d),
|
|
314
439
|
F.stale ? el('p', { class: 'dim mono fc-stale', role: 'status' }, 'The last read failed. Showing what PMOS had.') : null,
|
|
315
440
|
];
|
|
316
441
|
});
|
|
@@ -368,29 +493,33 @@ function beforeList(d, links) {
|
|
|
368
493
|
}
|
|
369
494
|
|
|
370
495
|
function drawStage(d, ph) {
|
|
496
|
+
const links = linksOf(d);
|
|
497
|
+
const cur = currentOf(d);
|
|
498
|
+
const sig = [ph, links, F.ui.stage, F.ui.preview, cur && cur.text, d.brief && [d.brief.prep, d.brief.matters, d.brief.last]];
|
|
499
|
+
region('stage', sig, () => stageBody(d, ph));
|
|
500
|
+
}
|
|
501
|
+
/** The stage: the material the meeting links, as a card, or the loaded preview; before the start, what is prepared. The phone shows it in a sheet. */
|
|
502
|
+
function stageBody(d, ph) {
|
|
371
503
|
const links = linksOf(d);
|
|
372
504
|
const cur = currentOf(d);
|
|
373
505
|
const embeddable = links.filter((l) => l.preview);
|
|
374
506
|
const pick = links[F.ui.stage] || embeddable[0] || links[0] || null;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
el('div', { class: 'fc-strip' }, el('span', { class: 'dim mono fc-src' }, icon('lock', 'sm'), '
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
button({ variant: 'outline', label: [icon('eye', 'sm'), 'Show the preview here'], attrs: { 'data-fc': 'preview-on', title: 'Load the ' + pick.app + ' preview into this page' } })) : null];
|
|
392
|
-
return [sect({ ic: 'doc', title: cur ? 'Material · ' + cur.text : 'Material', right, attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': pick.preview && F.ui.preview === pick.url ? pick.kind : 'card' } }, ...body)];
|
|
393
|
-
});
|
|
507
|
+
if (ph === 'before') {
|
|
508
|
+
const rows = beforeList(d, links);
|
|
509
|
+
return [sect({ ic: 'doc', title: 'Before it starts', attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': 'before' } },
|
|
510
|
+
rows.length ? el('ul', { class: 'list', role: 'list' }, ...rows) : el('div', { class: 'fold' }, icon('moon', 'sm'), 'Nothing is prepared for this meeting yet. Nothing is linked in the invite.'))];
|
|
511
|
+
}
|
|
512
|
+
if (!pick) return [sect({ ic: 'doc', title: 'Material', attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': 'none' } }, el('div', { class: 'fold' }, icon('moon', 'sm'), 'No material is linked to this meeting.'))];
|
|
513
|
+
const right = links.length > 1 ? el('div', { class: 'seg', role: 'group', 'aria-label': 'Material', 'data-fc': 'material' },
|
|
514
|
+
...links.map((l, i) => button({ variant: 'ghost', label: [icon(l.preview ? 'doc' : 'out', 'sm'), l.app], attrs: { 'aria-pressed': String(l === pick), 'data-fc': 'material-pick', 'data-i': String(i), title: l.label } }))) : null;
|
|
515
|
+
const body = pick.preview && F.ui.preview === pick.url
|
|
516
|
+
? [el('div', { class: 'fc-frame', 'data-fc': 'preview-wrap' }, el('iframe', { src: pick.preview, title: 'Preview of ' + pick.label + ' in ' + pick.app, loading: 'lazy', referrerpolicy: 'no-referrer', allow: 'fullscreen',
|
|
517
|
+
sandbox: 'allow-scripts allow-same-origin allow-popups allow-forms', 'data-fc': 'preview', 'data-app': pick.kind })),
|
|
518
|
+
el('div', { class: 'fc-strip' }, el('span', { class: 'dim mono fc-src' }, icon('lock', 'sm'), 'Shown by ' + pick.app + ' itself. PMOS shows nothing to the room.'), el('span', { class: 'fc-grow' }), openBtn(pick), pick.present ? presentBtn(pick) : null)]
|
|
519
|
+
: [el('div', { class: 'fc-card', 'data-fc': 'link-card' }, el('span', { class: 'ki' }, icon(pick.preview ? 'doc' : 'out')), el('div', { class: 'nb' }, el('b', null, pick.label), el('span', { class: 'dim mono' }, pick.app + (pick.host && pick.host !== pick.app ? ' · ' + pick.host : ''))), openBtn(pick), pick.present ? presentBtn(pick) : null),
|
|
520
|
+
pick.preview ? el('div', { class: 'fc-strip' }, el('span', { class: 'dim mono fc-src' }, icon('lock', 'sm'), 'PMOS loads nothing from ' + pick.app + ' until you ask.'), el('span', { class: 'fc-grow' }),
|
|
521
|
+
button({ variant: 'outline', label: [icon('eye', 'sm'), 'Show the preview here'], attrs: { 'data-fc': 'preview-on', title: 'Load the ' + pick.app + ' preview into this page' } })) : null];
|
|
522
|
+
return [sect({ ic: 'doc', title: cur ? 'Material · ' + cur.text : 'Material', right, attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': pick.preview && F.ui.preview === pick.url ? pick.kind : 'card' } }, ...body)];
|
|
394
523
|
}
|
|
395
524
|
|
|
396
525
|
/* ── captured, under the stage in the last minutes ─────────────────────────────────────────────────────────────── */
|
|
@@ -528,8 +657,10 @@ function tick() {
|
|
|
528
657
|
|
|
529
658
|
/* ── side sheets: Topics and Captured ─────────────────────────────────────────────────────────────────────────── */
|
|
530
659
|
|
|
531
|
-
function refreshSheet() {
|
|
660
|
+
function refreshSheet(force) {
|
|
532
661
|
if (!F || !F.sheetOpen || !F.sheet) return;
|
|
662
|
+
if (!force && (F.sheet.kind === 'material' || F.sheet.kind === 'stop' || F.sheet.kind === 'chip')) return; // a loaded preview is not loaded again on each read
|
|
663
|
+
if (!force && F.sheet.kind === 'note') readNote();
|
|
533
664
|
const b = F.sheet.body;
|
|
534
665
|
if (!b.isConnected) return;
|
|
535
666
|
const a = document.activeElement;
|
|
@@ -605,6 +736,10 @@ function openRef(ref) {
|
|
|
605
736
|
if (!ref) return;
|
|
606
737
|
const t = ref.type;
|
|
607
738
|
if (F && F.pop) { openFull(); return; } // the compact page is a small frame: what a note names opens in the full page
|
|
739
|
+
if (F && F.phone) { // the phone has no desktop pages to go to
|
|
740
|
+
if (t === 'note' && F.data && F.data.note && ref.id === F.data.note.id) openNoteSheet(); else toast('That opens on your computer.');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
608
743
|
closeFocus();
|
|
609
744
|
if (t === 'initiative') { import('./c3-init-extras.js').then((m) => { if (!m.goInitiative(ref.id)) goTab('init'); }).catch(() => goTab('init')); return; }
|
|
610
745
|
if (t === 'note') { window.dispatchEvent(new CustomEvent('c3-open-note', { detail: { id: ref.id } })); return; }
|
|
@@ -619,7 +754,7 @@ function openRef(ref) {
|
|
|
619
754
|
}
|
|
620
755
|
function openChip(c) {
|
|
621
756
|
if (!c) return;
|
|
622
|
-
if (c.ref) { openRef(c.ref); return; }
|
|
757
|
+
if (c.ref && !(F && F.phone && (c.detail || c.sub))) { openRef(c.ref); return; }
|
|
623
758
|
if (c.url) { openUrl(c.url); return; }
|
|
624
759
|
showSheet('chip', c.label, 'doc', () => [el('div', { class: 'fc-chipsheet', 'data-fc': 'chip-sheet' }, el('p', { class: 'fc-p' }, c.detail || c.sub || c.label), c.from ? el('p', { class: 'dim fc-note' }, 'From ' + c.from + '.') : null)]);
|
|
625
760
|
}
|
|
@@ -655,6 +790,9 @@ function onClick(ev) {
|
|
|
655
790
|
if (!d) return;
|
|
656
791
|
if (k === 'topics') { showSheet('topics', 'Topics', 'todo', topicsBuild); return; }
|
|
657
792
|
if (k === 'captured') { showSheet('captured', 'Captured so far', 'check', capturedBuild); return; }
|
|
793
|
+
if (k === 'note' && F.phone) { openNoteSheet(); return; }
|
|
794
|
+
if (k === 'material') { showSheet('material', linksOf(d).some((l) => l.kind === 'slides') ? 'Your slides' : 'Material', 'doc', materialBuild); return; }
|
|
795
|
+
if (k === 'stop') { showSheet('stop', d.talk ? 'Stop recording?' : 'Stop recording ' + d.event.title + '?', 'stop', stopBuild); return; }
|
|
658
796
|
if (k === 'note') { const id = d.note && d.note.id; closeFocus(); if (id) window.dispatchEvent(new CustomEvent('c3-open-note', { detail: { id } })); return; }
|
|
659
797
|
if (k === 'chip') { openChip(F.chips[Number(t.dataset.i)]); return; }
|
|
660
798
|
if (k === 'preview-on') { const ls = linksOf(d); const pk = ls[F.ui.stage] || ls.find((l) => l.preview) || ls[0]; F.ui.preview = pk && pk.url; drawStage(d, phaseOf()); return; }
|
|
@@ -687,4 +825,10 @@ document.addEventListener('click', (ev) => {
|
|
|
687
825
|
return;
|
|
688
826
|
}
|
|
689
827
|
if (k === 'add-topic') { F.ui.addTopic = !F.ui.addTopic; refreshSheet(); return; }
|
|
828
|
+
if (k === 'open-link' || k === 'present') { openUrl(t.dataset.url); return; }
|
|
829
|
+
if (k === 'preview-on') { const ls = linksOf(F.data); const pk = ls[F.ui.stage] || ls.find((l) => l.preview) || ls[0]; F.ui.preview = pk && pk.url; refreshSheet(true); return; }
|
|
830
|
+
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; refreshSheet(true); return; }
|
|
831
|
+
if (k === 'note-tab') { F.ui.noteTab = t.dataset.tab === 'said' ? 'said' : 'note'; refreshSheet(true); return; }
|
|
832
|
+
if (k === 'stop-keep') { closeSheet(); return; }
|
|
833
|
+
if (k === 'stop-ok') { closeSheet(); endMeeting().then(() => { if (F && F.phone) closeFocus(); }); return; }
|
|
690
834
|
});
|