create-pmos 0.241.0 → 0.242.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 (39) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  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/workspace/guide/console-calendar.md +14 -0
  7. package/workspace/guide/notes.md +22 -1
  8. package/workspace/lib/meeting-material.js +151 -0
  9. package/workspace/lib/meeting-replay.js +170 -0
  10. package/workspace/lib/notes-screen.js +128 -8
  11. package/workspace/lib/route-modules.js +2 -0
  12. package/workspace/lib/routes-meeting-focus.js +5 -3
  13. package/workspace/lib/routes-meeting-material.js +143 -0
  14. package/workspace/lib/routes-meeting-replay.js +75 -0
  15. package/workspace/lib/routes-notes.js +37 -4
  16. package/workspace/package-lock.json +2 -2
  17. package/workspace/package.json +1 -1
  18. package/workspace/public/pm/c3-calendar.js +15 -3
  19. package/workspace/public/pm/c3-demo-material.js +58 -0
  20. package/workspace/public/pm/c3-demo.js +5 -1
  21. package/workspace/public/pm/c3-focus.js +83 -16
  22. package/workspace/public/pm/c3-listen.css +10 -0
  23. package/workspace/public/pm/c3-material.css +22 -0
  24. package/workspace/public/pm/c3-material.js +311 -0
  25. package/workspace/public/pm/c3-notes.js +12 -4
  26. package/workspace/public/pm/c3-replay.css +127 -0
  27. package/workspace/public/pm/c3-replay.js +413 -0
  28. package/workspace/public/pm/dest-notes.js +4 -2
  29. package/workspace/public/pm/listen-sheet.js +94 -0
  30. package/workspace/public/pm/listen-sources.js +82 -0
  31. package/workspace/public/pm/notes-aside.js +62 -0
  32. package/workspace/public/pm/notes-screen-capture.js +23 -10
  33. package/workspace/public/pm/notes-screen-view.js +18 -5
  34. package/workspace/public/pm/notes-shots.js +55 -0
  35. package/workspace/public/pm/shell-recording.js +10 -2
  36. package/workspace/public/pm/view-notes.js +48 -8
  37. package/workspace/test-lists/meeting-material.json +6 -0
  38. package/workspace/test-lists/meeting-replay.json +6 -0
  39. package/workspace/test-lists/mm-capture.json +15 -0
@@ -26,6 +26,7 @@ import { pmPost } from './http.js';
26
26
  import { notesView, openNote, noteOfEvent, primeNotes } from './c3-notes.js'; // Calendar › Notes (initiative c3-notes-place)
27
27
  import { onNotes } from './notes-links.js';
28
28
  import { openSheet } from './c3-sheet.js';
29
+ import { materialSection } from './c3-material.js'; // Material on the meeting card (initiative meeting-material-replay)
29
30
 
30
31
  const LAYERS = [
31
32
  ['meetings', 'Meetings'],
@@ -669,6 +670,15 @@ const focusBtn = (e, date, ui) => button({ label: [c3ic('target', 'sm'), 'Focus'
669
670
  attrs: { 'data-cal3': 'focus', title: 'Open the meeting focus page: the clock, the topics, the material and PMOS beside the conversation' },
670
671
  onClick: () => { if (ui) { ui.st.sel = null; closeMeetingSheet(ui.r); ui.redraw(); } import('./c3-focus.js').then((m) => m.openFocus({ event: e, date })).catch(() => toast({ text: 'The focus page could not load.', tone: 'error' })); } });
671
672
 
673
+ /* "Replay": the full-screen replay of a meeting that has ended and has a recorded note (c3-replay.js); it closes the popup first, as Focus does */
674
+ const replayBtn = (e, ui) => {
675
+ const n = noteOfEvent(e);
676
+ if (!n || n.private || n.state === 'recording' || n.state === 'paused' || !(n.segments > 0 || (n.words || 0) > 0)) return null;
677
+ return button({ label: [c3ic('play', 'sm'), 'Replay'], variant: 'ghost', size: 'sm',
678
+ attrs: { 'data-cal3': 'replay', title: 'Open the meeting replay: the topics, the key points, the screen and the conversation along the audio' },
679
+ onClick: () => { if (ui) { ui.st.sel = null; closeMeetingSheet(ui.r); ui.redraw(); } import('./c3-replay.js').then((m) => m.openReplay({ note: n.id })).catch(() => toast({ text: 'The replay could not load.', tone: 'error' })); } });
680
+ };
681
+
672
682
  function meetingDetail(ui, hit, inSheet) {
673
683
  const { M, h } = ui; const e = hit.e;
674
684
  const s = h.timeOf(e.at); const f = s === null ? null : h.endH(e);
@@ -692,7 +702,7 @@ function meetingDetail(ui, hit, inSheet) {
692
702
  if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
693
703
  else if (past && (!rec || rec.held === false)) body = [fold('moon', 'Nothing held for this meeting: no note, decisions or linked items yet.', { 'data-cal3': 'nothing-held' })];
694
704
  else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
695
- else body = upcomingBody(ui, rec, items);
705
+ else body = upcomingBody(ui, rec, items, e, hit.date);
696
706
  const b = !past && rec && rec.brief ? rec.brief : null;
697
707
  const lv = !past && rec ? rec.level || null : null;
698
708
  const iniId = lv && lv.kind !== 'initiative' ? null : typeof e.initiative === 'string' && !(lv && lv.by === 'pm') ? e.initiative : b && b.initiative ? b.initiative.id : null;
@@ -707,7 +717,7 @@ function meetingDetail(ui, hit, inSheet) {
707
717
  // the popup: the sheet's header carries the title and Close; at Half and wider the note sits beside what came out of it
708
718
  const side = noteCard(ui, e);
709
719
  return [
710
- el('div', { class: 'cal3-dt' }, el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date, ui) : null),
720
+ el('div', { class: 'cal3-dt' }, el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date, ui) : null, past ? replayBtn(e, ui) : null),
711
721
  el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
712
722
  chipsRow(),
713
723
  past || !M.ctx ? null : levelRow(ui, e, lv),
@@ -827,7 +837,7 @@ function openBoardTab(ui, board, tab) {
827
837
  ui.h.gotoTab('init');
828
838
  }
829
839
 
830
- function upcomingBody(ui, rec, items) {
840
+ function upcomingBody(ui, rec, items, e, date) {
831
841
  const { h } = ui;
832
842
  const b = rec && rec.brief ? rec.brief : null;
833
843
  const lv = rec && rec.level ? rec.level : null;
@@ -857,6 +867,8 @@ function upcomingBody(ui, rec, items) {
857
867
  el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
858
868
  x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
859
869
  x.ref && REF[x.ref.type] ? refLink(ui, x.ref, { title: x.text }) : null))])));
870
+ if (e) out.push(materialSection({ event: e, date, level: lv, brief: b,
871
+ back: () => { if (!ui.st.sel) { ui.st.sel = { type: 'event', id: e.id }; ui.redraw(); } } }));
860
872
  if (b && ((b.links || []).length || (b.refs || []).length)) out.push(el('div', { class: 'chips2', 'data-cal3': 'meeting-links' },
861
873
  ...(b.links || []).map((l) => el('a', { class: 'chip2', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
862
874
  ...(b.refs || []).map((r) => r.link ? el('a', { class: 'chip2', href: r.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null)
@@ -0,0 +1,58 @@
1
+ /* pm/c3-demo-material.js — DEMO answers for the material of a meeting (initiative meeting-material-replay, part 1) on /c3.html?demo=1.
2
+ * c3-demo.js passes GET and POST /api/meeting-material and GET /api/meeting-material/suggest here, and adds the material of the meeting to the
3
+ * answer of GET /api/meeting-focus. The data is the approved mockup's: PMOS suggests the Partner API decision deck, the weekly report and a
4
+ * policy document; a search finds three more. The store lives in the page: a reload forgets it and nothing reaches the server. Real mode never loads this file. */
5
+
6
+ const rows = []; // { key, scope, event, item, topic, present, removed? } in the order they were added; the last of an item wins
7
+ const TOPICS = ['Context and goal', 'Options: soft vs hard limits', 'Partner impact: top 20 traffic', 'Partner comms and timing', 'Decide and next steps']
8
+ .map((text) => ({ id: text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''), text }));
9
+ const WS = '881665c933';
10
+ const SUGGESTED = [
11
+ { kind: 'pmos-deck', ref: 'planning/reports/rs1/2026-10-21.md', title: 'Partner API rate limits: decision deck', meta: 'Reports · deck · draft', why: 'Your deck, edited Fri 9 Oct', present: true, ws: WS },
12
+ { kind: 'report', ref: 'planning/reports/rs3/2026-10-14.md', title: 'Partner API weekly report · cycle 41', meta: 'Reports · document · issued', why: 'The report this review reads each week', present: false, ws: WS },
13
+ { kind: 'pmos-doc', ref: 'planning/prd/partner-agreement-prd.md', title: 'Partner agreement v3: fair use (section 4.2)', meta: 'IDPF-07 documents', why: 'Named in the last note: "check the agreement"', present: false, ws: WS },
14
+ ];
15
+ const FIND = [
16
+ { kind: 'pmos-deck', ref: 'planning/reports/rs1/2026-10-07.md', title: 'Payments 1.5 release review deck', meta: 'Reports · deck · issued', present: true, ws: WS },
17
+ { kind: 'report', ref: 'planning/reports/rs3/2026-10-02.md', title: 'Monthly product review · September', meta: 'Reports · document · issued', present: false, ws: WS },
18
+ { kind: 'pmos-doc', ref: 'planning/prd/idpf-07-charter.md', title: 'IDPF-07 charter', meta: 'IDPF-07 documents', present: false, ws: WS },
19
+ ];
20
+ const norm = (t) => String(t || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
21
+ const keyOf = (e) => (e && e.series ? 's:' + e.series : 't:' + norm(e && e.title));
22
+ const idOf = (r) => [r.key, r.scope === 'series' ? '' : r.event || '', r.item.kind, r.item.ref || r.item.url].join('|');
23
+ const classify = (u) => {
24
+ const s = String(u || '').trim(); let x; try { x = new URL(/^[a-z][a-z0-9+.-]*:/i.test(s) ? s : 'https://' + s); } catch { return null; }
25
+ if (x.protocol !== 'https:') return null;
26
+ if (x.hostname === 'docs.google.com' && /^\/presentation\/d\//.test(x.pathname)) return { kind: 'slides', url: x.href };
27
+ if (x.hostname === 'docs.google.com' && /^\/document\/d\//.test(x.pathname)) return { kind: 'gdoc', url: x.href };
28
+ if (/\.atlassian\.net$/.test(x.hostname) && x.pathname.startsWith('/wiki')) return { kind: 'conf', url: x.href };
29
+ return { kind: 'link', url: x.href };
30
+ };
31
+
32
+ /** The items of one meeting, in the shape of GET /api/meeting-material. event: { id?, title, series? } */
33
+ export function materialFor(event) {
34
+ const key = keyOf(event); const held = new Map();
35
+ for (const r of rows) { const id = idOf(r); if (r.removed) held.delete(id); else held.set(id, r); }
36
+ return [...held.values()].filter((r) => r.key === key && (r.scope === 'series' || r.event === String(event.id || ''))).map((r) => ({
37
+ id: Math.abs([...idOf(r)].reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 7)).toString(16), kind: r.item.kind, ref: r.item.ref || null, url: r.item.url || null, title: r.item.title,
38
+ ws: r.item.ws || null, topic: r.topic || null, present: r.present === true, scope: r.scope, from: 'you', by: 'pm:wawan', at: null }));
39
+ }
40
+
41
+ export function materialGet(path, url) {
42
+ const q = new URL(url, location.origin).searchParams;
43
+ if (path === '/api/meeting-material/suggest') {
44
+ const t = (q.get('q') || '').toLowerCase();
45
+ return { ok: true, suggested: SUGGESTED, found: t.length >= 2 ? FIND.filter((x) => x.title.toLowerCase().includes(t)) : [] };
46
+ }
47
+ const event = { id: q.get('event') || '', title: q.get('title') || '', series: q.get('series') || '' };
48
+ return { ok: true, key: keyOf(event), items: materialFor(event), ...(q.get('topics') ? { topics: TOPICS } : {}) };
49
+ }
50
+
51
+ export function materialPost(b) {
52
+ const e = b.event || {}; const it = b.item || {};
53
+ const item = it.ref ? { kind: it.kind, ref: it.ref, title: it.title || it.ref, ws: it.ws } : (() => { const c = classify(it.url); return c ? { ...c, title: it.title || c.url } : null; })();
54
+ if (!item) return { ok: false, error: 'a link must be an https address' };
55
+ const scope = b.scope === 'series' ? 'series' : 'meeting';
56
+ rows.push({ key: keyOf(e), scope, event: scope === 'meeting' ? String(e.id || '') : '', item, topic: b.topic || null, present: b.op !== 'remove' && b.present === true, removed: b.op === 'remove' });
57
+ return { ok: true, items: materialFor(e) };
58
+ }
@@ -12,6 +12,7 @@ import { el } from './dom.js';
12
12
  import * as D from './c3-demo-data.js';
13
13
  import { installInit } from './c3-demo-init.js';
14
14
  import { focusGet, focusPost } from './c3-demo-focus.js'; // Meeting focus: the mockup's Partner API review (GET /api/meeting-focus, its ask poll, and the topic and ask POSTs)
15
+ import { materialGet, materialPost, materialFor } from './c3-demo-material.js'; // Meeting material: GET and POST /api/meeting-material, its suggestions, and the material in the Focus read
15
16
  import { installBoards } from './c3-demo-boards.js'; // Initiatives boards: answers every /api/boards GET and POST (installed outermost, in installDemo)
16
17
  import { installCoreDocs } from './c3-demo-coredocs.js'; // Core documents of each level: GET /api/core-docs and its POSTs (installed outermost, after installBoards)
17
18
  import './c3-demo-cal-ix.js'; // Calendar and the ribbon: sets window.__c3calix (calendar-views.js reads it in demo only)
@@ -53,7 +54,9 @@ function getRoute(path, url) {
53
54
  switch (path) {
54
55
  case '/api/briefing': return D.briefing(url);
55
56
  case '/api/calendar-views': return withCalendarPage(D.calendarViews(url));
56
- case '/api/meeting-focus': case '/api/meeting-focus/ask': case '/api/meeting-focus/now': return focusGet(path, url);
57
+ case '/api/meeting-focus': { const o = focusGet(path, url); return o && o.ok ? { ...o, material: [...materialFor({ id: q.get('event') || '', title: q.get('title') || '' }), ...materialFor(o.event)] } : o; } // what was asked for, and the sample meeting the demo answers with
58
+ case '/api/meeting-focus/ask': case '/api/meeting-focus/now': return focusGet(path, url);
59
+ case '/api/meeting-material': case '/api/meeting-material/suggest': return materialGet(path, url);
57
60
  case '/api/pm-items': return D.pmItems();
58
61
  case '/api/runs': return D.runs();
59
62
  case '/api/releases': return D.releases();
@@ -78,6 +81,7 @@ function postRoute(path, body) {
78
81
  let b = {};
79
82
  try { b = body ? JSON.parse(body) : {}; } catch { b = {}; }
80
83
  if (path.startsWith('/api/meeting-focus/')) return focusPost(path, body);
84
+ if (path === '/api/meeting-material') return materialPost(b);
81
85
  if (path === '/api/pm-items/add') return { ok: true, item: { id: 'item-demo-new' + (++added), title: String(b.line || '').trim() || 'New item', due: null, priority: null, waiting_on: null, job: null, steps: [], created_at: D.NOW_ISO + 'Z' } };
82
86
  if (path === '/api/pm-items/remove') return { ok: true, undo_ms: 8000 };
83
87
  if (path === '/api/calendar-links') return (window.__c3links = (window.__c3links || []).concat([b])), { ok: true, link: b.kind === 'none' ? null : { kind: b.kind, ws: b.ws || null, board: b.board || null, initiative: b.initiative || null, title: b.title || null, by: 'pm:wawan' } }; // nothing is saved
@@ -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; } }