create-pmos 0.241.0 → 0.243.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-report-design-real-test.js +13 -2
- package/workspace/c3-report-design-test.js +25 -3
- package/workspace/c3-report-structure-real-test.js +38 -1
- package/workspace/c3-report-structure-test.js +21 -6
- package/workspace/guide/console-calendar.md +15 -1
- package/workspace/guide/notes.md +26 -1
- package/workspace/guide/report-structure.md +3 -3
- package/workspace/guide/reports-page.md +3 -1
- package/workspace/guide/reports-set-up.md +5 -1
- package/workspace/lib/headless.js +7 -2
- package/workspace/lib/meeting-material.js +151 -0
- package/workspace/lib/meeting-replay.js +184 -0
- package/workspace/lib/notes-screen.js +128 -8
- package/workspace/lib/report-structure.js +49 -5
- package/workspace/lib/route-modules.js +2 -0
- package/workspace/lib/routes-meeting-focus.js +5 -3
- package/workspace/lib/routes-meeting-material.js +143 -0
- package/workspace/lib/routes-meeting-replay.js +75 -0
- package/workspace/lib/routes-notes.js +37 -4
- package/workspace/lib/routes-report-setups.js +16 -2
- package/workspace/lib/routes-report-structure.js +176 -5
- package/workspace/lib/routes-reports.js +10 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-audience-link.js +21 -0
- package/workspace/public/pm/c3-audience.js +46 -0
- package/workspace/public/pm/c3-boot.js +2 -1
- package/workspace/public/pm/c3-calendar.js +15 -3
- package/workspace/public/pm/c3-demo-material.js +58 -0
- package/workspace/public/pm/c3-demo.js +5 -1
- package/workspace/public/pm/c3-focus.js +83 -16
- package/workspace/public/pm/c3-listen.css +10 -0
- package/workspace/public/pm/c3-material.css +22 -0
- package/workspace/public/pm/c3-material.js +321 -0
- package/workspace/public/pm/c3-notes.js +12 -4
- package/workspace/public/pm/c3-replay.css +133 -0
- package/workspace/public/pm/c3-replay.js +429 -0
- package/workspace/public/pm/c3-report-deck.css +5 -0
- package/workspace/public/pm/c3-report-deck.js +41 -5
- package/workspace/public/pm/c3-report-setup.css +5 -0
- package/workspace/public/pm/c3-report-setup.js +20 -2
- package/workspace/public/pm/c3-report-store.js +24 -2
- package/workspace/public/pm/c3-report-structure.css +4 -0
- package/workspace/public/pm/c3-report-structure.js +26 -8
- package/workspace/public/pm/c3-shell.js +2 -0
- package/workspace/public/pm/dest-notes.js +4 -2
- package/workspace/public/pm/dest-reports.js +10 -1
- package/workspace/public/pm/listen-sheet.js +94 -0
- package/workspace/public/pm/listen-sources.js +82 -0
- package/workspace/public/pm/notes-aside.js +62 -0
- package/workspace/public/pm/notes-screen-capture.js +23 -10
- package/workspace/public/pm/notes-screen-view.js +18 -5
- package/workspace/public/pm/notes-shots.js +55 -0
- package/workspace/public/pm/report-view.js +23 -1
- package/workspace/public/pm/reports-copy.js +5 -0
- package/workspace/public/pm/shell-recording.js +10 -2
- package/workspace/public/pm/view-notes.js +48 -8
- package/workspace/server.js +8 -1
- package/workspace/skills/report-structure-pull.skill +45 -0
- package/workspace/test-lists/meeting-material.json +6 -0
- package/workspace/test-lists/meeting-replay.json +6 -0
- package/workspace/test-lists/mm-capture.json +15 -0
- package/workspace/test-lists/report-structure-gaps.json +5 -0
|
@@ -28,6 +28,8 @@ import { ensureComponentsCss, button } from './primitives.js';
|
|
|
28
28
|
import { icon, sect, pbtn, lk, ib, put } from './c3-ini-ui.js';
|
|
29
29
|
import { openSheet, closeSheet } from './c3-sheet.js';
|
|
30
30
|
import { toast } from './c3-toast.js';
|
|
31
|
+
import * as Shots from './notes-shots.js';
|
|
32
|
+
import { openAddMaterial, openItem, presentItem, audienceItem, pmosUrl } from './c3-material.js'; // the PM's own material (initiative meeting-material-replay)
|
|
31
33
|
|
|
32
34
|
const POLL_MS = 3000;
|
|
33
35
|
const LAST_MINUTES = 5; // the last minutes of the meeting: the captured items show under the stage
|
|
@@ -43,8 +45,12 @@ export function clockText(sec) {
|
|
|
43
45
|
return (neg ? '-' : '') + (h ? h + ':' + pad(m) : String(m)) + ':' + pad(x);
|
|
44
46
|
}
|
|
45
47
|
/** What a link is, and how the page shows it: every link is a card; a Google Slides or Docs link also has a /preview address that the page loads only when the PM presses Show the preview here (no-network: the page reaches nothing by itself). Pure. */
|
|
48
|
+
const PMOS_APP = { 'pmos-deck': 'PMOS deck', 'pmos-doc': 'PMOS document', report: 'PMOS report' };
|
|
46
49
|
export function classify(url) {
|
|
47
50
|
const raw = String(url || '').trim();
|
|
51
|
+
// an item PMOS itself holds (a deck, a document, a report cycle): pmos:<kind>:<workspace>:<path>. It opens in PMOS, not in a tab.
|
|
52
|
+
const pm = /^pmos:(pmos-deck|pmos-doc|report):([^:]*):(.+)$/.exec(raw);
|
|
53
|
+
if (pm) return { kind: 'pmos', pk: pm[1], app: PMOS_APP[pm[1]], url: raw, host: 'PMOS', ref: pm[3] };
|
|
48
54
|
// A link pasted without its scheme (docs.google.com/…) is a web link.
|
|
49
55
|
let u; try { u = new URL(/^[a-z][a-z0-9+.-]*:/i.test(raw) ? raw : 'https://' + raw); } catch { return { kind: 'link', app: 'Link', url: String(url), host: '' }; }
|
|
50
56
|
const m = /^\/(presentation|document)\/d\/([A-Za-z0-9_-]+)/.exec(u.pathname);
|
|
@@ -60,7 +66,7 @@ export function classify(url) {
|
|
|
60
66
|
let F = null; // the open page: { id, date, root, r (regions), sig, data, offset, ui, timers, sheet, ... }
|
|
61
67
|
|
|
62
68
|
function ensureCss() {
|
|
63
|
-
ensureC3Css(); ensureComponentsCss();
|
|
69
|
+
ensureC3Css(); ensureComponentsCss(); Shots.ensureListenCss();
|
|
64
70
|
if (document.querySelector('link[data-c3-focus]')) return;
|
|
65
71
|
const l = document.createElement('link');
|
|
66
72
|
l.rel = 'stylesheet'; l.href = new URL('./c3-focus.css', import.meta.url).href; l.setAttribute('data-c3-focus', '');
|
|
@@ -211,7 +217,24 @@ function region(name, sig, build) {
|
|
|
211
217
|
return true;
|
|
212
218
|
}
|
|
213
219
|
const phaseOf = () => { const n = nowMs(); const s = startMs(); const e = endMs(); return Number.isFinite(s) && n < s ? 'before' : Number.isFinite(e) && n >= e ? 'after' : 'during'; };
|
|
214
|
-
|
|
220
|
+
/** The meeting's material: the links of the invite and the brief, then the PM's own (d.material, with topic and present). A PMOS deck is presented in PMOS. */
|
|
221
|
+
function linksOf(d) {
|
|
222
|
+
const mine = ((d && d.material) || []).map((m) => {
|
|
223
|
+
const c = classify(['pmos-deck', 'pmos-doc', 'report'].includes(m.kind) ? pmosUrl(m) : m.url);
|
|
224
|
+
return { label: m.title || c.url, ...c, mat: m, topic: m.topic || null, you: m.present === true, ...(c.kind === 'pmos' ? { present: m.kind === 'pmos-deck' } : {}) };
|
|
225
|
+
});
|
|
226
|
+
const own = new Set(mine.map((l) => l.url));
|
|
227
|
+
const given = ((d && d.event && d.event.links) || []).map((l) => ({ label: l.label, ...classify(l.url) })).filter((l) => !own.has(l.url));
|
|
228
|
+
return [...given, ...mine].map((l, i) => ({ ...l, i }));
|
|
229
|
+
}
|
|
230
|
+
const normTopic = (t) => String(t || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
231
|
+
/** The material tied to the topic the room is on (by its text or its id), or null. */
|
|
232
|
+
const forTopic = (links, cur) => (cur ? links.find((l) => l.topic && (normTopic(l.topic) === normTopic(cur.text) || normTopic(l.topic) === normTopic(cur.id))) || null : null);
|
|
233
|
+
/** The item on the stage: the one you pressed, else the material for the topic the room is on, else the first. */
|
|
234
|
+
function stagePick(d) {
|
|
235
|
+
const links = linksOf(d);
|
|
236
|
+
return (F.ui.stagePin ? links[F.ui.stage] : forTopic(links, currentOf(d))) || links[F.ui.stage] || links.find((l) => l.preview) || links[0] || null;
|
|
237
|
+
}
|
|
215
238
|
const topicNo = (d, id) => d.topics.findIndex((t) => t.id === id) + 1;
|
|
216
239
|
const currentOf = (d) => d.topics.find((t) => t.id === d.current) || null;
|
|
217
240
|
const openRunOf = (d) => (d.runs || []).filter((x) => x.to_min == null).pop() || null;
|
|
@@ -332,7 +355,7 @@ function stopBuild() {
|
|
|
332
355
|
return [el('div', { class: 'fc-stopsheet', 'data-fc': 'stop-sheet' },
|
|
333
356
|
el('p', { class: 'fc-p' }, d && d.talk ? 'PMOS keeps the notes and shows you what it caught.'
|
|
334
357
|
: 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.'),
|
|
335
|
-
el('div', { class: 'iacts' }, pbtn('primary', 'Stop recording', 'stop', { 'data-fc': 'stop-ok' }), pbtn('ghost', 'Keep recording', null, { 'data-fc': 'stop-keep' })))];
|
|
358
|
+
el('div', { class: 'iacts' }, pbtn('primary', 'Stop recording', 'stop', { 'data-fc': 'stop-ok' }), d && d.note && !isDemo() && !(F && F.phone) ? pbtn('outline', 'Stop and open the replay', 'play', { 'data-fc': 'stop-replay' }) : null, pbtn('ghost', 'Keep recording', null, { 'data-fc': 'stop-keep' })))];
|
|
336
359
|
}
|
|
337
360
|
function materialBuild() { return stageBody(F.data, phaseOf()); }
|
|
338
361
|
|
|
@@ -419,9 +442,17 @@ function trackEl(d) {
|
|
|
419
442
|
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' }));
|
|
420
443
|
}
|
|
421
444
|
|
|
445
|
+
/* Pause shots: the chip beside REC stops the pictures and keeps the sound (notes-shots.js; the same switch is on the recording chip) */
|
|
446
|
+
const shotsSig = () => (Shots.active() ? Shots.chipText() : '');
|
|
447
|
+
function shotsChip() {
|
|
448
|
+
const off = Shots.paused();
|
|
449
|
+
return lk({ class: 'lsn-chip' + (off ? ' off' : ''), 'data-fc': 'shots', 'aria-pressed': String(off), title: Shots.chipTitle(),
|
|
450
|
+
onclick: () => { const now = Shots.setPaused(!Shots.paused()); toast(now ? 'Shots paused. The sound is still recorded.' : 'Shots back on.'); draw(); } }, icon(off ? 'play' : 'eye', 'sm'), Shots.chipText());
|
|
451
|
+
}
|
|
452
|
+
|
|
422
453
|
function drawTop(d, ph) {
|
|
423
454
|
const e = d.event; const rec = recState(d); const ini = (d.brief && d.brief.initiative) || null; const lt = leftText(d, ph);
|
|
424
|
-
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];
|
|
455
|
+
const sig = [e.title, e.at, e.end, e.attendees, ini, e.initiative, ph, rec, shotsSig(), lt.txt, d.topics.map((t) => t.id), d.runs, F.status === 'ok' && F.stale ? 1 : 0];
|
|
425
456
|
region('top', sig, () => {
|
|
426
457
|
const s = startMs(); const f = endMs();
|
|
427
458
|
const people = (e.attendees || []);
|
|
@@ -440,6 +471,7 @@ function drawTop(d, ph) {
|
|
|
440
471
|
el('div', { class: 'fc-clock', role: 'timer', 'aria-live': 'off' }, ...clock, el('div', { class: 'fc-end' }, el('span', { class: 'tag' + (lt.warn ? ' fc-warn' : ''), 'data-fc': 'proj', hidden: !lt.txt }, lt.txt))),
|
|
441
472
|
el('div', { class: 'fc-acts' },
|
|
442
473
|
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,
|
|
474
|
+
rec === 'recording' && Shots.active() ? shotsChip() : null,
|
|
443
475
|
rec === 'recording' || rec === 'paused' ? pbtn('outline', 'End meeting', 'stop', { 'data-fc': 'end', title: 'Stops the recording and saves the note' })
|
|
444
476
|
: rec !== 'ended' && ph !== 'after' ? pbtn('primary', 'Start recording', 'play', { 'data-fc': 'start', title: 'Records this meeting in a note' }) : null),
|
|
445
477
|
trackEl(d),
|
|
@@ -484,8 +516,13 @@ function linkRow(l) {
|
|
|
484
516
|
el('div', { class: 'nb' }, el('span', { class: 'tt' }, l.label), el('div', { class: 'chips' }, el('span', { class: 'chip2' }, icon('out', 'sm'), l.app))),
|
|
485
517
|
el('div', { class: 'acts' }, openBtn(l), l.present ? presentBtn(l) : null));
|
|
486
518
|
}
|
|
487
|
-
const openBtn = (l) =>
|
|
488
|
-
|
|
519
|
+
const openBtn = (l) => (l.kind === 'pmos' ? pbtn('outline', 'Open', null, { 'data-fc': 'pm-open', 'data-i': String(l.i), title: 'Opens it in PMOS' })
|
|
520
|
+
: pbtn('outline', l.kind === 'link' ? 'Open' : 'Open in ' + l.app, null, { 'data-fc': 'open-link', 'data-url': l.url, title: 'Opens it in a new tab' }));
|
|
521
|
+
const presentBtn = (l) => (l.kind === 'pmos'
|
|
522
|
+
? [pbtn('ghost', 'Audience view', 'eye', { 'data-fc': 'pm-audience', 'data-i': String(l.i), title: 'Opens the slides only, full screen, with no notes. Share the screen in the call.' }),
|
|
523
|
+
pbtn('primary', 'Present', 'play', { 'data-fc': 'pm-present', 'data-i': String(l.i), title: 'Opens the presenter view in PMOS with your speaker notes. Press Audience view before you share the screen.' })]
|
|
524
|
+
: pbtn('ghost', 'Present in ' + l.app, 'play', { 'data-fc': 'present', 'data-url': l.present, title: 'Opens the deck in present mode in a new tab. Share that tab in the call.' }));
|
|
525
|
+
const addMaterialBtn = () => pbtn('outline', 'Add material', 'plus', { 'data-fc': 'mat-add', title: 'Link a PMOS document, a report or deck, or a web page to this meeting' });
|
|
489
526
|
|
|
490
527
|
function beforeList(d, links) {
|
|
491
528
|
const b = d.brief || {};
|
|
@@ -502,28 +539,28 @@ function beforeList(d, links) {
|
|
|
502
539
|
function drawStage(d, ph) {
|
|
503
540
|
const links = linksOf(d);
|
|
504
541
|
const cur = currentOf(d);
|
|
505
|
-
const sig = [ph, links, F.ui.stage, F.ui.preview, cur && cur.text, d.brief && [d.brief.prep, d.brief.matters, d.brief.last]];
|
|
542
|
+
const sig = [ph, links, F.ui.stage, F.ui.stagePin, F.ui.preview, cur && cur.text, d.brief && [d.brief.prep, d.brief.matters, d.brief.last]];
|
|
506
543
|
region('stage', sig, () => stageBody(d, ph));
|
|
507
544
|
}
|
|
508
545
|
/** 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. */
|
|
509
546
|
function stageBody(d, ph) {
|
|
510
547
|
const links = linksOf(d);
|
|
511
548
|
const cur = currentOf(d);
|
|
512
|
-
const
|
|
513
|
-
const pick = links[F.ui.stage] || embeddable[0] || links[0] || null;
|
|
549
|
+
const pick = stagePick(d);
|
|
514
550
|
if (ph === 'before') {
|
|
515
551
|
const rows = beforeList(d, links);
|
|
516
552
|
return [sect({ ic: 'doc', title: 'Before it starts', attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': 'before' } },
|
|
517
|
-
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.'))];
|
|
553
|
+
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.'), el('div', { class: 'iacts' }, addMaterialBtn())])];
|
|
518
554
|
}
|
|
519
|
-
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.'))];
|
|
555
|
+
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.'), el('div', { class: 'iacts' }, addMaterialBtn()))];
|
|
520
556
|
const right = links.length > 1 ? el('div', { class: 'seg', role: 'group', 'aria-label': 'Material', 'data-fc': 'material' },
|
|
521
557
|
...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;
|
|
522
558
|
const body = pick.preview && F.ui.preview === pick.url
|
|
523
559
|
? [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',
|
|
524
560
|
sandbox: 'allow-scripts allow-same-origin allow-popups allow-forms', 'data-fc': 'preview', 'data-app': pick.kind })),
|
|
525
561
|
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)]
|
|
526
|
-
: [el('div', { class: 'fc-card', 'data-fc': 'link-card'
|
|
562
|
+
: [el('div', { class: 'fc-card', 'data-fc': 'link-card', 'data-fc-kind': pick.kind }, el('span', { class: 'ki' }, icon(pick.kind === 'pmos' ? (pick.pk === 'pmos-doc' ? 'doc' : 'report') : pick.preview ? 'doc' : 'out')),
|
|
563
|
+
el('div', { class: 'nb' }, el('b', null, pick.label), el('span', { class: 'dim mono' }, pick.app + (pick.host && pick.host !== pick.app ? ' · ' + pick.host : '') + (pick.topic ? ' · for ' + pick.topic : '') + (pick.you ? ' · you present it' : ''))), openBtn(pick), pick.present ? presentBtn(pick) : null),
|
|
527
564
|
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' }),
|
|
528
565
|
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];
|
|
529
566
|
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)];
|
|
@@ -719,6 +756,7 @@ async function ask(question) {
|
|
|
719
756
|
function tick() {
|
|
720
757
|
if (!F || !F.data) return;
|
|
721
758
|
const d = F.data; const n = nowMs(); const s = startMs(); const f = endMs();
|
|
759
|
+
if (F.shotsSeen !== shotsSig()) { const first = F.shotsSeen === undefined; F.shotsSeen = shotsSig(); if (!first) { draw(); return; } } // Pause shots was pressed elsewhere
|
|
722
760
|
if (phaseOf() !== F.sig.phase) { F.sig.phase = phaseOf(); if (F.sig.phaseSet) { draw(); return; } F.sig.phaseSet = true; }
|
|
723
761
|
const set = (k, v) => { for (const x of document.querySelectorAll('[data-fc="' + k + '"]')) if (x.textContent !== v) x.textContent = v; };
|
|
724
762
|
const ph = phaseOf();
|
|
@@ -858,6 +896,22 @@ async function endMeeting() {
|
|
|
858
896
|
|
|
859
897
|
/* ── presses ──────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
860
898
|
|
|
899
|
+
/** Add material, and Open, Present and Audience view of the PM's PMOS material. True when the press was one of these. */
|
|
900
|
+
function materialPress(k, t) {
|
|
901
|
+
const d = F && F.data; if (!d) return false;
|
|
902
|
+
if (k === 'mat-add') {
|
|
903
|
+
const e = d.event || {};
|
|
904
|
+
openAddMaterial({ event: { id: e.id, title: e.title, at: e.at, series: e.series, initiative: e.initiative, links: e.links }, date: d.date, brief: d.brief });
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
if (k !== 'pm-open' && k !== 'pm-present' && k !== 'pm-audience') return false;
|
|
908
|
+
const l = linksOf(d)[Number(t.dataset.i)];
|
|
909
|
+
if (l && l.mat) (k === 'pm-present' ? presentItem : k === 'pm-audience' ? audienceItem : openItem)(l.mat);
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
// the material was added or removed on the Calendar card or in the sheet: read the meeting again, so it shows at once
|
|
913
|
+
window.addEventListener('c3-material-changed', () => { if (F && !F.noteId) load(); });
|
|
914
|
+
|
|
861
915
|
function onClick(ev) {
|
|
862
916
|
const t = ev.target.closest('[data-fc]'); if (!t || !F || !F.root.contains(t)) return;
|
|
863
917
|
const k = t.dataset.fc; const d = F.data;
|
|
@@ -866,6 +920,7 @@ function onClick(ev) {
|
|
|
866
920
|
if (k === 'pop-open') { openFull(); return; }
|
|
867
921
|
if (k === 'choice') { const c = (F.choices || [])[Number(t.dataset.i)]; if (c) chooseMeeting(c, String(c.at || '').slice(0, 10)); return; }
|
|
868
922
|
if (!d) return;
|
|
923
|
+
if (materialPress(k, t)) return;
|
|
869
924
|
if (k === 'topics') { showSheet('topics', 'Topics', 'todo', topicsBuild); return; }
|
|
870
925
|
if (k === 'captured') { showSheet('captured', 'Captured so far', 'check', capturedBuild); return; }
|
|
871
926
|
if (k === 'note' && F.phone) { openNoteSheet(); return; }
|
|
@@ -873,8 +928,8 @@ function onClick(ev) {
|
|
|
873
928
|
if (k === 'stop') { showSheet('stop', d.talk ? 'Stop recording?' : 'Stop recording ' + d.event.title + '?', 'stop', stopBuild); return; }
|
|
874
929
|
if (k === 'note') { const id = d.note && d.note.id; closeFocus(); if (id) window.dispatchEvent(new CustomEvent('c3-open-note', { detail: { id } })); return; }
|
|
875
930
|
if (k === 'chip') { openChip(F.chips[Number(t.dataset.i)]); return; }
|
|
876
|
-
if (k === 'preview-on') { const
|
|
877
|
-
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; drawStage(d, phaseOf()); return; }
|
|
931
|
+
if (k === 'preview-on') { const pk = stagePick(d); F.ui.preview = pk && pk.url; drawStage(d, phaseOf()); return; }
|
|
932
|
+
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; F.ui.stagePin = true; drawStage(d, phaseOf()); return; }
|
|
878
933
|
if (k === 'open-link' || k === 'present') { openUrl(t.dataset.url); return; }
|
|
879
934
|
if (k === 'start') { startRecording(); return; }
|
|
880
935
|
if (k === 'end') { endMeeting(); return; }
|
|
@@ -896,6 +951,7 @@ document.addEventListener('click', (ev) => {
|
|
|
896
951
|
if (!F || !F.sheetOpen || !F.sheet) return;
|
|
897
952
|
const t = ev.target.closest && ev.target.closest('[data-shell="sheet"] [data-fc]'); if (!t) return;
|
|
898
953
|
const k = t.dataset.fc;
|
|
954
|
+
if (materialPress(k, t)) return;
|
|
899
955
|
if (k === 'picker') { F.ui.picker = !F.ui.picker; F.ui.newTopic = false; refreshSheet(); return; }
|
|
900
956
|
if (k === 'new-topic') { F.ui.newTopic = true; refreshSheet(); return; }
|
|
901
957
|
if (k === 'pick') { const text = t.dataset.text; postTopic('set', text).then((ok) => { if (ok) { closeSheet(); toast('Now on: ' + text + '.'); } }); return; }
|
|
@@ -907,10 +963,21 @@ document.addEventListener('click', (ev) => {
|
|
|
907
963
|
}
|
|
908
964
|
if (k === 'add-topic') { F.ui.addTopic = !F.ui.addTopic; refreshSheet(); return; }
|
|
909
965
|
if (k === 'open-link' || k === 'present') { openUrl(t.dataset.url); return; }
|
|
910
|
-
if (k === 'preview-on') { const
|
|
911
|
-
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; refreshSheet(true); return; }
|
|
966
|
+
if (k === 'preview-on') { const pk = stagePick(F.data); F.ui.preview = pk && pk.url; refreshSheet(true); return; }
|
|
967
|
+
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; F.ui.stagePin = true; refreshSheet(true); return; }
|
|
912
968
|
if (k === 'note-tab') { F.ui.noteTab = t.dataset.tab === 'said' ? 'said' : 'note'; refreshSheet(true); return; }
|
|
913
969
|
if (k === 'stop-keep') { closeSheet(); return; }
|
|
914
970
|
if (k === 'pull') { const id = t.dataset.id; F.ui.pulled.push(id); (F.pulledAt ||= {})[id] = nowMs(); F.ui.ni = null; closeSheet(); drawConversation(F.data); return; }
|
|
915
971
|
if (k === 'stop-ok') { closeSheet(); endMeeting().then(() => { if (F && F.phone) closeFocus(); }); return; }
|
|
972
|
+
if (k === 'stop-replay') { endAndReplay(); return; } // End and open the replay
|
|
916
973
|
});
|
|
974
|
+
|
|
975
|
+
/** Stop the recording, leave Focus and open the meeting replay on the note (c3-replay.js). */
|
|
976
|
+
async function endAndReplay() {
|
|
977
|
+
const f = F; const note = f && f.data && f.data.note && f.data.note.id; if (!f || !note) return;
|
|
978
|
+
closeSheet();
|
|
979
|
+
await endMeeting();
|
|
980
|
+
if (F !== f) return;
|
|
981
|
+
closeFocus();
|
|
982
|
+
import('./c3-replay.js').then((m) => m.openReplay({ note })).catch(() => toast('The replay could not load.'));
|
|
983
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* c3-listen.css — the Listen sheet, the set-aside review and the Pause-shots chip (initiative meeting-material-replay, part 3).
|
|
2
|
+
* Loaded by listen-sheet.js (ensureListenCss) and by the Focus page. Every rule is under .lsn- or .fc-shots so it changes nothing else. */
|
|
3
|
+
.c3 .lsn { display: grid; gap: var(--sp-3); }
|
|
4
|
+
.c3 .lsn-warn, .c3 .tag.lsn-warn { color: var(--warning); }
|
|
5
|
+
.c3 .lsn-help { margin: 0; }
|
|
6
|
+
.c3 .lsn-aside { align-items: center; }
|
|
7
|
+
.c3 .lsn-thumb { width: 148px; height: 84px; flex: 0 0 auto; object-fit: cover; border: 1px solid var(--border); background: var(--muted); }
|
|
8
|
+
.c3 .lsn-chip { font-size: 12px; }
|
|
9
|
+
.c3 .lsn-chip.off { color: var(--warning); }
|
|
10
|
+
@media (max-width: 640px) { .c3 .lsn-thumb { width: 96px; height: 60px; } }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* pm/c3-material.css — meeting material (initiative meeting-material-replay, part 1): the Material part of the Calendar's meeting card, the Add material sheet
|
|
2
|
+
* and the PMOS deck on the Focus stage. Tokens only. Classes are .mm- under .c3; the sheet and the card are drawn with the console3 classes they already use. */
|
|
3
|
+
.c3 .mm h4 .bdg { margin-left: 4px; }
|
|
4
|
+
.c3 .mm-row { align-items: center; }
|
|
5
|
+
.c3 .mm-row .cal3-ct { display: grid; gap: 2px; min-width: 0; }
|
|
6
|
+
.c3 .mm-meta { font-size: var(--fs-1); }
|
|
7
|
+
.c3 .mm-row .ib.sm { width: 24px; height: 24px; }
|
|
8
|
+
.c3 .mm .mm-foot { align-items: center; margin-top: 6px; }
|
|
9
|
+
.c3 .mm-sheet { display: grid; gap: var(--sp-3); }
|
|
10
|
+
.c3 .mm-in { width: 100%; box-sizing: border-box; height: 34px; padding: 0 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--background); color: var(--foreground); font: inherit; font-size: var(--fs-3); }
|
|
11
|
+
.c3 .mm-in:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 1px; }
|
|
12
|
+
.c3 .mm-opt.on { background: color-mix(in srgb, var(--acc) 7%, transparent); border-radius: 8px; }
|
|
13
|
+
.c3 .mm-why, .c3 .mm-sel { color: var(--acc); }
|
|
14
|
+
.c3 .mm-opt .nb { min-width: 0; }
|
|
15
|
+
.c3 .mm-opt .chips { flex-wrap: wrap; min-width: 0; }
|
|
16
|
+
.c3 .mm-opt .tag { white-space: normal; overflow-wrap: anywhere; max-width: 100%; }
|
|
17
|
+
.c3 .mm-set { border-color: color-mix(in srgb, var(--acc) 40%, var(--border)); }
|
|
18
|
+
.c3 .mm-f { display: grid; grid-template-columns: 70px minmax(0, 1fr); gap: 8px; align-items: center; padding: 4px 0; }
|
|
19
|
+
.c3 .mm-f .mk-lbl { padding: 0; }
|
|
20
|
+
.c3 .mm-wrap { flex-wrap: wrap; height: auto; }
|
|
21
|
+
@media (max-width: 640px) { .c3 .mm-f { grid-template-columns: minmax(0, 1fr); } }
|
|
22
|
+
@media (pointer: coarse) { .c3 .mm-in { min-height: 44px; } }
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/* pm/c3-material.js — the material of a meeting (initiative meeting-material-replay, part 1; the mockup the PM approved on 2026-10-14, hashes #material, #material-add, #focus-material).
|
|
2
|
+
*
|
|
3
|
+
* materialSection({ event, date, ws, level, brief, back }) → Node the Material part of the meeting card: each linked item with its kind, topic and source,
|
|
4
|
+
* Present or Open, remove, and Add material
|
|
5
|
+
* openAddMaterial({ event, date, ws, level, brief, back }) the Add material sheet: Suggested from the meeting's level, Find in PMOS, Or paste a link; For,
|
|
6
|
+
* Topic and You (Present it / Only read it)
|
|
7
|
+
* openItem(item) · presentItem(item) · audienceItem(item) Open in PMOS or in a new tab; Present (a PMOS deck in the presenter view, Google Slides in present
|
|
8
|
+
* mode); the audience view of a PMOS deck
|
|
9
|
+
* classifyLink(url) · KIND · pmosUrl(item)
|
|
10
|
+
* window.C3Material = { open(event, opts), section, openItem, presentItem, audienceItem } for any page that wants to add or show material
|
|
11
|
+
*
|
|
12
|
+
* It reads and writes GET and POST /api/meeting-material (lib/routes-meeting-material.js). PMOS keeps the link on its own record of the meeting; the calendar invite is not
|
|
13
|
+
* changed. A change fires `c3-material-changed` on window, so the card and Focus read again. Nothing is sent anywhere. */
|
|
14
|
+
import { el } from './dom.js';
|
|
15
|
+
import { pmGet, pmPost } from './http.js';
|
|
16
|
+
import { ensureC3Css } from './pm-work-view.js';
|
|
17
|
+
import { ensureComponentsCss } from './primitives.js';
|
|
18
|
+
import { icon, pbtn, sect, put, lk, ib, list, empty } from './c3-ini-ui.js';
|
|
19
|
+
import { openSheet, closeSheet } from './c3-sheet.js';
|
|
20
|
+
import { toast } from './c3-toast.js';
|
|
21
|
+
import { openAudienceWindow } from './c3-audience-link.js';
|
|
22
|
+
|
|
23
|
+
/** What each kind of item is called, and which icon it takes. `deck`: PMOS presents it itself. `present`: it can be presented at all. */
|
|
24
|
+
export const KIND = {
|
|
25
|
+
'pmos-deck': { tag: 'PMOS deck', ic: 'report', deck: true, present: true },
|
|
26
|
+
'pmos-doc': { tag: 'PMOS document', ic: 'doc' },
|
|
27
|
+
report: { tag: 'PMOS report', ic: 'report' },
|
|
28
|
+
slides: { tag: 'Google Slides', ic: 'out', present: true },
|
|
29
|
+
gdoc: { tag: 'Google Docs', ic: 'out' },
|
|
30
|
+
conf: { tag: 'Confluence', ic: 'out' },
|
|
31
|
+
link: { tag: 'Link', ic: 'out' },
|
|
32
|
+
};
|
|
33
|
+
const BY = { invite: 'from the invite', brief: 'found by today’s brief', you: 'added by you' };
|
|
34
|
+
const isPmos = (k) => k === 'pmos-deck' || k === 'pmos-doc' || k === 'report';
|
|
35
|
+
|
|
36
|
+
function ensureCss() {
|
|
37
|
+
ensureC3Css(); ensureComponentsCss();
|
|
38
|
+
if (typeof document === 'undefined' || document.querySelector('link[data-c3-material]')) return;
|
|
39
|
+
const l = document.createElement('link');
|
|
40
|
+
l.rel = 'stylesheet'; l.href = new URL('./c3-material.css', import.meta.url).href; l.setAttribute('data-c3-material', '');
|
|
41
|
+
document.head.appendChild(l);
|
|
42
|
+
}
|
|
43
|
+
const wsParam = () => { try { return new URLSearchParams(location.search).get('ws') || ''; } catch { return ''; } };
|
|
44
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
45
|
+
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
46
|
+
const dateText = (iso) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? (+m[3]) + ' ' + MON[+m[2] - 1] : ''; };
|
|
47
|
+
const timeText = (iso) => { const d = new Date(iso); return Number.isFinite(d.getTime()) ? pad(d.getHours()) + ':' + pad(d.getMinutes()) : ''; };
|
|
48
|
+
|
|
49
|
+
/** What an address is, as the server decides it (lib/meeting-material.js classifyUrl). null for anything that is not an https address. */
|
|
50
|
+
export function classifyLink(raw) {
|
|
51
|
+
const s = String(raw || '').trim();
|
|
52
|
+
if (!s || /\s/.test(s)) return null;
|
|
53
|
+
let u; try { u = new URL(/^[a-z][a-z0-9+.-]*:/i.test(s) ? s : 'https://' + s); } catch { return null; }
|
|
54
|
+
if (u.protocol !== 'https:' || !u.hostname.includes('.')) return null;
|
|
55
|
+
if (u.hostname === 'docs.google.com' && /^\/presentation\/d\//.test(u.pathname)) return { kind: 'slides', url: u.href, title: 'Google Slides deck', meta: 'read through your Google Drive connection' };
|
|
56
|
+
if (u.hostname === 'docs.google.com' && /^\/document\/d\//.test(u.pathname)) return { kind: 'gdoc', url: u.href, title: 'Google Docs page', meta: 'read through your Google Drive connection' };
|
|
57
|
+
if (/\.atlassian\.net$/.test(u.hostname) && u.pathname.startsWith('/wiki')) return { kind: 'conf', url: u.href, title: 'Confluence page', meta: 'read through your Atlassian connection' };
|
|
58
|
+
return { kind: 'link', url: u.href, title: u.hostname, meta: 'opens in a new tab · PMOS does not read it' };
|
|
59
|
+
}
|
|
60
|
+
/** The pseudo address Focus gives a PMOS item, so its link list can hold it beside web links (c3-focus.js classify). */
|
|
61
|
+
export const pmosUrl = (item) => 'pmos:' + item.kind + ':' + (item.ws || '') + ':' + item.ref;
|
|
62
|
+
|
|
63
|
+
/* ── what is stored ───────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
64
|
+
|
|
65
|
+
const evParams = (event, extra) => {
|
|
66
|
+
const e = event || {};
|
|
67
|
+
const p = new URLSearchParams({ title: String(e.title || e.id || '') });
|
|
68
|
+
if (e.id) p.set('event', String(e.id));
|
|
69
|
+
if (e.series) p.set('series', String(e.series));
|
|
70
|
+
const date = (extra && extra.date) || String(e.at || e.start || '').slice(0, 10);
|
|
71
|
+
if (date) p.set('date', date);
|
|
72
|
+
const ws = (extra && extra.ws) || wsParam(); if (ws) p.set('ws', ws);
|
|
73
|
+
if (extra && extra.topics) p.set('topics', '1');
|
|
74
|
+
return p;
|
|
75
|
+
};
|
|
76
|
+
const evKey = (e) => (e && (e.id || e.title)) + '|' + ((e && e.series) || '');
|
|
77
|
+
const CACHE = new Map(); // event key → { items, at }
|
|
78
|
+
const FRESH_MS = 4000;
|
|
79
|
+
|
|
80
|
+
async function readItems(event, extra) {
|
|
81
|
+
const r = await pmGet('/api/meeting-material?' + evParams(event, extra).toString()).catch(() => null);
|
|
82
|
+
if (r && r.ok && Array.isArray(r.items)) { CACHE.set(evKey(event), { items: r.items, at: Date.now() }); return r; }
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const changed = (event) => { try { window.dispatchEvent(new CustomEvent('c3-material-changed', { detail: { key: evKey(event) } })); } catch { /* no window */ } };
|
|
86
|
+
|
|
87
|
+
async function save(event, body) {
|
|
88
|
+
const e = event || {};
|
|
89
|
+
const r = await pmPost('/api/meeting-material', { event: { id: e.id || undefined, title: e.title || e.id, series: e.series || undefined }, ...body }).catch((err) => ({ ok: false, error: String((err && err.message) || err) }));
|
|
90
|
+
if (!r || !r.ok) { toast({ text: 'The material was not saved: ' + ((r && (r.error || r.reason)) || 'no answer') + '.', tone: 'error', timeout: 6000 }); return null; }
|
|
91
|
+
CACHE.set(evKey(event), { items: r.items || [], at: Date.now() });
|
|
92
|
+
changed(event);
|
|
93
|
+
return r;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── open and present ─────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
97
|
+
|
|
98
|
+
function openWeb(url) { try { window.open(url, '_blank', 'noopener,noreferrer'); } catch { /* a blocked pop-up */ } }
|
|
99
|
+
const slidesPresentUrl = (url) => { const m = /^(https:\/\/docs\.google\.com\/presentation\/d\/[A-Za-z0-9_-]+)/.exec(String(url || '')); return m ? m[1] + '/present' : url; };
|
|
100
|
+
|
|
101
|
+
async function openDocument(item) {
|
|
102
|
+
try {
|
|
103
|
+
const { openDocSheet } = await import('./c3-doc-sheet.js');
|
|
104
|
+
const ws = item.ws || wsParam();
|
|
105
|
+
openDocSheet({ ws, doc: { path: item.ref, name: item.title || item.ref, ty: 'Document', v: '', st: 'current', ini: '', iniName: '' },
|
|
106
|
+
library: () => Promise.resolve(pmGet('/api/docs/library' + (ws ? '?ws=' + encodeURIComponent(ws) : ''))).catch(() => ({ ok: true, files: [], edges: [], kinds: [], categories: [], initiatives: {} })) });
|
|
107
|
+
} catch { toast('The document sheet could not open. The file is ' + item.ref + '.'); }
|
|
108
|
+
}
|
|
109
|
+
async function openReport(item) {
|
|
110
|
+
try {
|
|
111
|
+
const store = await import('./c3-report-store.js');
|
|
112
|
+
const setup = await import('./c3-report-setup.js');
|
|
113
|
+
await store.loadReports(item.ws || wsParam(), true);
|
|
114
|
+
const d = store.RS.docs.find((x) => x.path === item.ref);
|
|
115
|
+
if (d) { setup.openReportDoc(d); return; }
|
|
116
|
+
} catch { /* fall back to the plain document */ }
|
|
117
|
+
openDocument(item);
|
|
118
|
+
}
|
|
119
|
+
/** Open an item: a PMOS item in PMOS (the document sheet), a link in a new tab. */
|
|
120
|
+
export function openItem(item) {
|
|
121
|
+
if (!item) return;
|
|
122
|
+
if (item.kind === 'pmos-doc') return openDocument(item);
|
|
123
|
+
if (isPmos(item.kind)) return openReport(item);
|
|
124
|
+
if (item.url) openWeb(item.url);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The presenter view (or the audience view) of a PMOS deck: the report deck's own (c3-report-deck.js), with your speaker notes. */
|
|
128
|
+
async function showDeck(item, mode) {
|
|
129
|
+
let deck = null;
|
|
130
|
+
try {
|
|
131
|
+
const { createDeck } = await import('./c3-report-deck.js');
|
|
132
|
+
deck = createDeck({ ws: item.ws || wsParam(), path: item.ref, name: item.title || 'Deck', cycle: '' });
|
|
133
|
+
const t0 = Date.now();
|
|
134
|
+
while (!deck.state.loaded && Date.now() - t0 < 10000) await new Promise((r) => setTimeout(r, 60));
|
|
135
|
+
} catch { deck = null; }
|
|
136
|
+
const S = deck && deck.state;
|
|
137
|
+
if (!S || !S.loaded || !S.cyc || !S.cyc.designed || !S.R || S.R.format !== 'deck') {
|
|
138
|
+
toast('This report has no designed deck yet, so it opens as a document.');
|
|
139
|
+
return openItem(item);
|
|
140
|
+
}
|
|
141
|
+
deck.present(mode);
|
|
142
|
+
}
|
|
143
|
+
export function presentItem(item) {
|
|
144
|
+
if (!item) return;
|
|
145
|
+
if (item.kind === 'slides') return openWeb(slidesPresentUrl(item.url));
|
|
146
|
+
if (item.kind === 'pmos-deck') return showDeck(item, 'pres');
|
|
147
|
+
return openItem(item);
|
|
148
|
+
}
|
|
149
|
+
/** Audience view: a PMOS deck opens in a window of its own, slides only (initiative meeting-material-replay-gaps). The window opens at once, on the press, because a browser
|
|
150
|
+
* blocks a window that opens after a wait. When it is blocked a notice says so and offers the full-screen view in this window. */
|
|
151
|
+
export function audienceItem(item) {
|
|
152
|
+
if (item && item.kind === 'pmos-deck') {
|
|
153
|
+
if (openAudienceWindow({ ws: item.ws || wsParam(), path: item.ref, name: item.title || 'Deck' })) return;
|
|
154
|
+
toast({ text: 'The audience window was blocked. Allow pop-ups for this page, or use the full-screen view here.', actionLabel: 'Full-screen view here', timeout: 10000, onAction: () => showDeck(item, 'aud') });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
return openItem(item);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── the Material part of the meeting card ────────────────────────────────────────────────────────────────────────── */
|
|
161
|
+
|
|
162
|
+
/** The links the invite and today's brief already carry, as read-only rows. */
|
|
163
|
+
function givenRows(event, brief) {
|
|
164
|
+
const seen = new Set(); const rows = [];
|
|
165
|
+
const add = (label, url, from) => {
|
|
166
|
+
const c = classifyLink(url); if (!c || seen.has(c.url)) return;
|
|
167
|
+
seen.add(c.url); rows.push({ id: from + ':' + c.url, kind: c.kind, url: c.url, title: label || c.title, topic: null, present: false, scope: null, from });
|
|
168
|
+
};
|
|
169
|
+
for (const l of (event && Array.isArray(event.links) ? event.links : [])) add(l && l.label, l && l.url, 'invite');
|
|
170
|
+
for (const l of (brief && Array.isArray(brief.links) ? brief.links : [])) add(l && l.label, l && l.url, 'brief');
|
|
171
|
+
for (const r of (brief && Array.isArray(brief.refs) ? brief.refs : [])) if (r && r.link) add(r.label, r.link, 'brief');
|
|
172
|
+
return rows;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function rowOf(item, o) {
|
|
176
|
+
const k = KIND[item.kind] || KIND.link;
|
|
177
|
+
const canPresent = !!item.present && k.present;
|
|
178
|
+
const where = item.scope === 'series' ? 'every meeting in the series' : item.scope === 'meeting' ? 'this meeting' : '';
|
|
179
|
+
const meta = [item.topic ? 'for ' + item.topic : null, BY[item.from] || BY.you, where || null, canPresent ? 'you present it' : null].filter(Boolean).join(' · ');
|
|
180
|
+
const act = canPresent
|
|
181
|
+
? lk({ 'data-mm-act': 'present', title: item.kind === 'slides' ? 'Opens it in Google Slides in present mode. Share that tab in the call.' : 'Opens the presenter view in PMOS. Press Audience view before you share the screen.', onclick: () => presentItem(item) }, icon('play', 'sm'), 'Present')
|
|
182
|
+
: lk({ 'data-mm-act': 'open', title: isPmos(item.kind) ? 'Opens it in PMOS' : 'Opens it in a new tab', onclick: () => openItem(item) }, icon('out', 'sm'), 'Open');
|
|
183
|
+
const rm = item.from === 'you' ? ib('Remove ' + item.title, 'x', { 'data-mm-act': 'remove', title: 'Remove from this meeting. The document itself is not changed.',
|
|
184
|
+
onclick: async () => {
|
|
185
|
+
const r = await save(o.event, { op: 'remove', scope: item.scope, item: isPmos(item.kind) ? { kind: item.kind, ref: item.ref, ws: item.ws || undefined } : { url: item.url } });
|
|
186
|
+
if (r) toast('Removed from the meeting. The document itself is not changed.');
|
|
187
|
+
} }) : null;
|
|
188
|
+
return el('li', { class: 'cal3-ci mm-row', 'data-mm-item': item.kind, 'data-mm-from': item.from, ...(item.topic ? { 'data-mm-topic': item.topic } : {}) },
|
|
189
|
+
el('span', { class: 'tag' }, k.tag), el('span', { class: 'cal3-ct' }, item.title, el('span', { class: 'dim mm-meta' }, meta)), act, rm);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function sectionBody(o, items) {
|
|
193
|
+
const rows = [...givenRows(o.event, o.brief), ...(items || [])];
|
|
194
|
+
return [
|
|
195
|
+
el('h4', null, icon('doc', 'sm'), 'Material ', el('span', { class: 'bdg g', 'data-mm': 'count' }, String(rows.length))),
|
|
196
|
+
rows.length ? el('ul', { class: 'cal3-cl', role: 'list' }, ...rows.map((r) => rowOf(r, o))) : el('div', { class: 'fold', 'data-mm': 'none' }, icon('info', 'sm'), items ? 'Nothing is linked to this meeting yet.' : 'Reading the material of this meeting.'),
|
|
197
|
+
el('div', { class: 'chips2 mm-foot' }, pbtn('outline', 'Add material', 'plus', { 'data-mm': 'add', onclick: () => openAddMaterial(o) }), el('span', { class: 'dim cal3-note' }, 'Shows in Focus, by topic. Your calendar invite is not changed.')),
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The Material part of the meeting card (before the meeting). It draws what it holds at once and reads again when it is older than a few seconds. */
|
|
202
|
+
export function materialSection(o) {
|
|
203
|
+
ensureCss();
|
|
204
|
+
const box = el('div', { class: 'cal3-ctx mm', 'data-mm': 'section', 'data-cal3-ctx': 'material' });
|
|
205
|
+
const key = evKey(o.event);
|
|
206
|
+
const draw = () => { const c = CACHE.get(key); put(box, ...sectionBody(o, c ? c.items : null)); };
|
|
207
|
+
draw();
|
|
208
|
+
const heard = () => { if (!box.isConnected) return; draw(); };
|
|
209
|
+
const onChanged = (ev) => { if (!box.isConnected) { window.removeEventListener('c3-material-changed', onChanged); return; } if (!ev.detail || ev.detail.key === key) draw(); };
|
|
210
|
+
window.addEventListener('c3-material-changed', onChanged);
|
|
211
|
+
const c = CACHE.get(key);
|
|
212
|
+
if (!c || Date.now() - c.at > FRESH_MS) readItems(o.event, { date: o.date, ws: o.ws }).then((r) => { if (r) heard(); });
|
|
213
|
+
return box;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ── the Add material sheet ───────────────────────────────────────────────────────────────────────────────────────── */
|
|
217
|
+
|
|
218
|
+
const levelIni = (o) => {
|
|
219
|
+
const lv = o.level; const b = o.brief; const e = o.event || {};
|
|
220
|
+
if (lv && lv.kind === 'initiative' && lv.id) return lv.id;
|
|
221
|
+
if (lv && lv.kind) return '';
|
|
222
|
+
return (b && b.initiative && b.initiative.id) || (typeof e.initiative === 'string' ? e.initiative : '') || '';
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
function pickRow(it, on, onPick) {
|
|
226
|
+
const k = KIND[it.kind] || KIND.link;
|
|
227
|
+
return el('li', { class: 'nrow mm-opt' + (on ? ' on' : ''), 'data-mm-opt': it.kind },
|
|
228
|
+
el('span', { class: 'ki' }, icon(k.ic)),
|
|
229
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, it.title),
|
|
230
|
+
el('div', { class: 'chips' }, el('span', { class: 'chip2' }, icon(k.ic, 'sm'), k.tag), it.meta ? el('span', { class: 'tag' }, it.meta) : null,
|
|
231
|
+
it.why ? el('span', { class: 'tag mm-why' }, icon('spark', 'sm'), it.why) : null)),
|
|
232
|
+
el('div', { class: 'acts' }, on ? el('span', { class: 'tag mm-sel' }, icon('check', 'sm'), 'Picked') : pbtn('ghost', 'Pick', null, { 'data-mm-pick': '', onclick: () => onPick(it) })));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Add material: Suggested from the meeting's level, Find in PMOS, Or paste a link; then For, Topic and You. */
|
|
236
|
+
export function openAddMaterial(o) {
|
|
237
|
+
ensureCss();
|
|
238
|
+
const event = o.event || {};
|
|
239
|
+
const S = { pick: null, scope: 'meeting', topic: null, present: true, q: '', link: '', sug: null, found: [], topics: [], busy: false, token: 0 };
|
|
240
|
+
const root = el('div', { class: 'mm-sheet', 'data-mm': 'sheet' });
|
|
241
|
+
const sugBox = el('div', { 'data-mm': 'suggested' });
|
|
242
|
+
const foundBox = el('div', { 'data-mm': 'found' });
|
|
243
|
+
const pasteBox = el('div', { 'data-mm': 'pasted' });
|
|
244
|
+
const optsBox = el('div', { 'data-mm': 'options' });
|
|
245
|
+
const findIn = el('input', { class: 'mm-in', type: 'search', 'data-mm': 'find', placeholder: 'A document, report or deck', 'aria-label': 'Find a document, report or deck in PMOS', autocomplete: 'off' });
|
|
246
|
+
const linkIn = el('input', { class: 'mm-in', type: 'url', 'data-mm': 'link', placeholder: 'Google Slides, Google Docs, Confluence or any web page', 'aria-label': 'Paste a link', autocomplete: 'off' });
|
|
247
|
+
let timer = null;
|
|
248
|
+
|
|
249
|
+
const pasted = () => { const c = classifyLink(S.link); return c ? { kind: c.kind, url: c.url, title: c.title, meta: c.meta, present: c.kind === 'slides' } : null; };
|
|
250
|
+
const same = (a, b) => !!a && !!b && a.kind === b.kind && (a.ref || a.url) === (b.ref || b.url);
|
|
251
|
+
const choose = (it) => { S.pick = it; S.present = !!(KIND[it.kind] && KIND[it.kind].present) && it.present !== false; paint(); };
|
|
252
|
+
|
|
253
|
+
function paint() {
|
|
254
|
+
put(sugBox, S.sug == null ? el('div', { class: 'fold' }, icon('info', 'sm'), 'Looking in PMOS.') : S.sug.length ? list(S.sug.map((it) => pickRow(it, same(it, S.pick), choose)), { role: 'list' }) : empty('Nothing to suggest yet. Search below, or paste a link.'));
|
|
255
|
+
put(foundBox, S.q.length >= 2 ? (S.found.length ? list(S.found.map((it) => pickRow(it, same(it, S.pick), choose)), { role: 'list' }) : el('div', { class: 'fold' }, icon('info', 'sm'), S.busy ? 'Searching.' : 'Nothing in PMOS matches that.')) : null);
|
|
256
|
+
const p = pasted();
|
|
257
|
+
put(pasteBox, p ? list([pickRow(p, same(p, S.pick), choose)], { role: 'list' }) : el('p', { class: 'dim cal3-note' }, S.link ? 'Paste an https address.' : 'Google Slides gets Present. A web page opens in a new tab and PMOS does not read it.'));
|
|
258
|
+
put(optsBox, S.pick ? settings() : null);
|
|
259
|
+
}
|
|
260
|
+
const seg = (label, rows, cur, key) => el('div', { class: 'seg mm-wrap', role: 'group', 'aria-label': label }, ...rows.map(([v, l]) => pbtn('ghost', l, null, { 'aria-pressed': String(cur === v), [key]: String(v), onclick: () => { S[key === 'data-mm-scope' ? 'scope' : key === 'data-mm-topic' ? 'topic' : 'present'] = v; paint(); } })));
|
|
261
|
+
function settings() {
|
|
262
|
+
const pick = S.pick; const k = KIND[pick.kind] || KIND.link;
|
|
263
|
+
const canPresent = !!k.present;
|
|
264
|
+
const topicName = S.topic;
|
|
265
|
+
const where = event.series ? 'Every meeting in the series' : 'Every meeting with this title';
|
|
266
|
+
const note = canPresent
|
|
267
|
+
? (pick.kind === 'slides' ? 'Present opens it in Google Slides in present mode. You share that tab in the call. ' : 'Present opens the presenter view in PMOS with your speaker notes. The presenter view and the audience view are one switch: press Audience view, then share the screen in the call. ')
|
|
268
|
+
: '';
|
|
269
|
+
const focusNote = topicName ? 'Focus brings it up when the room gets to ' + topicName + '.' : 'Focus keeps it under Material for the whole meeting.';
|
|
270
|
+
return sect({ ic: 'settings', title: 'How it shows', attrs: { class: 'mm-set', 'data-mm': 'settings' } },
|
|
271
|
+
el('div', { class: 'mm-f' }, el('span', { class: 'mk-lbl dim mono' }, 'For'),
|
|
272
|
+
seg('For', [['meeting', 'This meeting · ' + [dateText(String(event.at || event.start || o.date || '')), timeText(event.at || event.start)].filter(Boolean).join(' ')], ['series', where]], S.scope, 'data-mm-scope')),
|
|
273
|
+
S.topics.length ? el('div', { class: 'mm-f' }, el('span', { class: 'mk-lbl dim mono' }, 'Topic'),
|
|
274
|
+
el('div', { class: 'seg mm-wrap', role: 'group', 'aria-label': 'Topic' },
|
|
275
|
+
...S.topics.map((t, i) => pbtn('ghost', (i + 1) + '. ' + t.text, null, { 'aria-pressed': String(S.topic === t.text), 'data-mm-topic': t.id, onclick: () => { S.topic = t.text; paint(); } })),
|
|
276
|
+
pbtn('ghost', 'Whole meeting', null, { 'aria-pressed': String(S.topic == null), 'data-mm-topic': 'none', onclick: () => { S.topic = null; paint(); } }))) : null,
|
|
277
|
+
canPresent ? el('div', { class: 'mm-f' }, el('span', { class: 'mk-lbl dim mono' }, 'You'), seg('Do you present it', [[true, 'Present it'], [false, 'Only read it']], S.present, 'data-mm-pres')) : null,
|
|
278
|
+
el('p', { class: 'dim cal3-note' }, note + focusNote),
|
|
279
|
+
el('div', { class: 'iacts' }, pbtn('primary', 'Add to the meeting', 'plus', { 'data-mm': 'do', onclick: add }), pbtn('ghost', 'Cancel', null, { 'data-mm': 'cancel', onclick: () => { S.pick = null; paint(); } })));
|
|
280
|
+
}
|
|
281
|
+
async function add() {
|
|
282
|
+
const pick = S.pick; if (!pick) return;
|
|
283
|
+
const item = isPmos(pick.kind) ? { kind: pick.kind, ref: pick.ref, title: pick.title, ws: pick.ws || undefined } : { url: pick.url, title: pick.title };
|
|
284
|
+
const r = await save(event, { op: 'add', scope: S.scope, item, topic: S.topic || undefined, present: S.present && !!(KIND[pick.kind] || {}).present });
|
|
285
|
+
if (!r) return;
|
|
286
|
+
closeSheet();
|
|
287
|
+
toast('Added to ' + (S.scope === 'series' ? 'every meeting in the series' : 'this meeting') + '. Focus shows it' + (S.topic ? ' at ' + S.topic : '') + '.');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
findIn.addEventListener('input', () => {
|
|
291
|
+
S.q = findIn.value.trim(); S.busy = S.q.length >= 2; S.found = []; paint();
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
if (!S.busy) return;
|
|
294
|
+
const my = ++S.token;
|
|
295
|
+
timer = setTimeout(async () => {
|
|
296
|
+
const p = new URLSearchParams({ q: S.q }); const ws = o.ws || wsParam(); if (ws) p.set('ws', ws);
|
|
297
|
+
const r = await pmGet('/api/meeting-material/suggest?' + p.toString()).catch(() => null);
|
|
298
|
+
if (my !== S.token) return;
|
|
299
|
+
S.busy = false; S.found = r && r.ok && Array.isArray(r.found) ? r.found : []; paint();
|
|
300
|
+
}, 220);
|
|
301
|
+
});
|
|
302
|
+
linkIn.addEventListener('input', () => { S.link = linkIn.value.trim(); const p = pasted(); if (p) S.pick = p; else if (S.pick && !S.pick.ref && S.pick.url) S.pick = null; paint(); });
|
|
303
|
+
|
|
304
|
+
put(root,
|
|
305
|
+
el('p', { class: 'dim cal3-note' }, (event.title || 'This meeting') + (event.at || event.start ? ' · ' + dateText(event.at || event.start) + ' ' + timeText(event.at || event.start) : '') + '. PMOS keeps the link on its record of this meeting. The invite in your calendar is not changed.'),
|
|
306
|
+
sect({ ic: 'spark', title: 'Suggested for this meeting', attrs: { 'data-mm': 'suggest-sect' } }, sugBox),
|
|
307
|
+
sect({ ic: 'search', title: 'Find in PMOS' }, findIn, foundBox),
|
|
308
|
+
sect({ ic: 'out', title: 'Or paste a link' }, linkIn, pasteBox),
|
|
309
|
+
optsBox);
|
|
310
|
+
paint();
|
|
311
|
+
openSheet({ title: 'Add material', icon: 'doc', wide: true, body: [el('div', { class: 'c3x' }, root)], onClose: () => { clearTimeout(timer); if (typeof o.back === 'function') { try { o.back(); } catch { /* the page is gone */ } } } });
|
|
312
|
+
|
|
313
|
+
const ini = levelIni(o);
|
|
314
|
+
const p = new URLSearchParams(); const ws = o.ws || wsParam(); if (ws) p.set('ws', ws); if (ini) p.set('initiative', ini);
|
|
315
|
+
pmGet('/api/meeting-material/suggest?' + p.toString()).then((r) => { S.sug = r && r.ok && Array.isArray(r.suggested) ? r.suggested : []; paint(); }).catch(() => { S.sug = []; paint(); });
|
|
316
|
+
readItems(event, { date: o.date, ws: o.ws, topics: true }).then((r) => { S.topics = r && Array.isArray(r.topics) ? r.topics : []; paint(); });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (typeof window !== 'undefined') {
|
|
320
|
+
window.C3Material = { open: (event, opts) => openAddMaterial({ event, ...(opts || {}) }), section: materialSection, openItem, presentItem, audienceItem, KIND };
|
|
321
|
+
}
|