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.
Files changed (69) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/payload/state/log.md +1 -1
  7. package/workspace/c3-report-design-real-test.js +13 -2
  8. package/workspace/c3-report-design-test.js +25 -3
  9. package/workspace/c3-report-structure-real-test.js +38 -1
  10. package/workspace/c3-report-structure-test.js +21 -6
  11. package/workspace/guide/console-calendar.md +15 -1
  12. package/workspace/guide/notes.md +26 -1
  13. package/workspace/guide/report-structure.md +3 -3
  14. package/workspace/guide/reports-page.md +3 -1
  15. package/workspace/guide/reports-set-up.md +5 -1
  16. package/workspace/lib/headless.js +7 -2
  17. package/workspace/lib/meeting-material.js +151 -0
  18. package/workspace/lib/meeting-replay.js +184 -0
  19. package/workspace/lib/notes-screen.js +128 -8
  20. package/workspace/lib/report-structure.js +49 -5
  21. package/workspace/lib/route-modules.js +2 -0
  22. package/workspace/lib/routes-meeting-focus.js +5 -3
  23. package/workspace/lib/routes-meeting-material.js +143 -0
  24. package/workspace/lib/routes-meeting-replay.js +75 -0
  25. package/workspace/lib/routes-notes.js +37 -4
  26. package/workspace/lib/routes-report-setups.js +16 -2
  27. package/workspace/lib/routes-report-structure.js +176 -5
  28. package/workspace/lib/routes-reports.js +10 -0
  29. package/workspace/package-lock.json +2 -2
  30. package/workspace/package.json +1 -1
  31. package/workspace/public/pm/c3-audience-link.js +21 -0
  32. package/workspace/public/pm/c3-audience.js +46 -0
  33. package/workspace/public/pm/c3-boot.js +2 -1
  34. package/workspace/public/pm/c3-calendar.js +15 -3
  35. package/workspace/public/pm/c3-demo-material.js +58 -0
  36. package/workspace/public/pm/c3-demo.js +5 -1
  37. package/workspace/public/pm/c3-focus.js +83 -16
  38. package/workspace/public/pm/c3-listen.css +10 -0
  39. package/workspace/public/pm/c3-material.css +22 -0
  40. package/workspace/public/pm/c3-material.js +321 -0
  41. package/workspace/public/pm/c3-notes.js +12 -4
  42. package/workspace/public/pm/c3-replay.css +133 -0
  43. package/workspace/public/pm/c3-replay.js +429 -0
  44. package/workspace/public/pm/c3-report-deck.css +5 -0
  45. package/workspace/public/pm/c3-report-deck.js +41 -5
  46. package/workspace/public/pm/c3-report-setup.css +5 -0
  47. package/workspace/public/pm/c3-report-setup.js +20 -2
  48. package/workspace/public/pm/c3-report-store.js +24 -2
  49. package/workspace/public/pm/c3-report-structure.css +4 -0
  50. package/workspace/public/pm/c3-report-structure.js +26 -8
  51. package/workspace/public/pm/c3-shell.js +2 -0
  52. package/workspace/public/pm/dest-notes.js +4 -2
  53. package/workspace/public/pm/dest-reports.js +10 -1
  54. package/workspace/public/pm/listen-sheet.js +94 -0
  55. package/workspace/public/pm/listen-sources.js +82 -0
  56. package/workspace/public/pm/notes-aside.js +62 -0
  57. package/workspace/public/pm/notes-screen-capture.js +23 -10
  58. package/workspace/public/pm/notes-screen-view.js +18 -5
  59. package/workspace/public/pm/notes-shots.js +55 -0
  60. package/workspace/public/pm/report-view.js +23 -1
  61. package/workspace/public/pm/reports-copy.js +5 -0
  62. package/workspace/public/pm/shell-recording.js +10 -2
  63. package/workspace/public/pm/view-notes.js +48 -8
  64. package/workspace/server.js +8 -1
  65. package/workspace/skills/report-structure-pull.skill +45 -0
  66. package/workspace/test-lists/meeting-material.json +6 -0
  67. package/workspace/test-lists/meeting-replay.json +6 -0
  68. package/workspace/test-lists/mm-capture.json +15 -0
  69. 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
- const linksOf = (d) => ((d && d.event && d.event.links) || []).map((l) => ({ label: l.label, ...classify(l.url) }));
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) => 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' });
488
- const presentBtn = (l) => 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.' });
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 embeddable = links.filter((l) => l.preview);
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' }, 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),
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 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; }
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 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; }
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
+ }