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
@@ -111,7 +111,7 @@ async function read(s) {
111
111
  draw(s); return;
112
112
  }
113
113
  if (!s.open && s.rows.length) { s.open = s.rows[0].id; return read(s); }
114
- if (one && one.ok && one.note && one.note.id === s.open) { s.note = one.note; s.shots = (one.screen && one.screen.frames) || []; s.rec = one.recording || null; s.place = one.place || null; }
114
+ if (one && one.ok && one.note && one.note.id === s.open) { s.note = one.note; s.shots = (one.screen && one.screen.frames) || []; s.aside = (one.screen && one.screen.aside) || 0; s.rec = one.recording || null; s.place = one.place || null; }
115
115
  draw(s);
116
116
  }
117
117
 
@@ -124,7 +124,7 @@ async function loadBoards(s) {
124
124
 
125
125
  function choose(s, id, phase) {
126
126
  if (s.open === id && !s.ser) return;
127
- s.open = id; s.note = null; s.phase = phase || null; s.shots = []; s.rec = null; s.place = null; s.ser = null; s.thread = null;
127
+ s.open = id; s.note = null; s.phase = phase || null; s.shots = []; s.aside = 0; s.rec = null; s.place = null; s.ser = null; s.thread = null;
128
128
  draw(s); read(s);
129
129
  }
130
130
  /* open a meeting series on the right (the series page) */
@@ -376,6 +376,13 @@ function replayOf(s, n) {
376
376
  s.audio = { key, el: a };
377
377
  return a;
378
378
  }
379
+ /* "Replay": the full-screen replay of the meeting (c3-replay.js): the topics, key points, shots and conversation along the audio */
380
+ function openReplayBtn(s, n) {
381
+ return btn('chip2 lk', { 'data-cal3-notes': 'open-replay', title: 'Open the meeting replay: the topics, the key points, the screen and the conversation along the audio' }, [c3ic('play', 'sm'), 'Replay'], () => {
382
+ if (s.audio && s.audio.el) { try { s.audio.el.pause(); } catch { /* none */ } }
383
+ import('./c3-replay.js').then((m) => m.openReplay({ note: n.id })).catch(() => toast('The replay could not load.'));
384
+ });
385
+ }
379
386
  function seek(s, ms) {
380
387
  const a = s.audio && s.audio.el;
381
388
  if (!a) return;
@@ -464,7 +471,7 @@ function after(s, n, mode) {
464
471
  const kids = [
465
472
  el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('notes'), 'Notes'), recordButton(s, n, mode)),
466
473
  el('div', { class: 'chips2' }, el('b', null, n.title || 'Untitled note'), el('span', { class: 'dim mono cal3-sm' }, [dayOf(startOf(n)), mins(lenOf(n))].filter(Boolean).join(' · ')), ...names.map(avatar)),
467
- ...(() => { const rp = mode ? null : replayOf(s, n); return rp ? [el('div', { class: 'cal3-replay', 'data-cal3-notes': 'replay' }, el('h4', null, 'Replay'), rp, el('p', { class: 'dim cal3-sm' }, 'Kept on this machine.'))] : []; })(),
474
+ ...(() => { const rp = mode ? null : replayOf(s, n); return rp ? [el('div', { class: 'cal3-replay', 'data-cal3-notes': 'replay' }, el('div', { class: 'c3cv-head' }, el('h4', null, 'Replay'), openReplayBtn(s, n)), rp, el('p', { class: 'dim cal3-sm' }, 'Kept on this machine.'))] : []; })(),
468
475
  ...cf.map((c) => el('div', { class: 'fold w', 'data-cal3-notes': 'conflict' }, c3ic('alert', 'sm'), '“' + c.text + '” differs from ', el('span', { class: 'mono' }, c.doc + (c.line ? ':' + c.line : '')))),
469
476
  p.overview || !mode ? editable(true, 'p', p.overview, 'Summary', (t) => saveFinal(s, n, 'overview', 0, t), { class: 'cal3-ov', 'data-cal3-notes': p.overview ? 'overview' : 'no-overview' }, p.from === 'final' ? null : 'Write a summary of the meeting')
470
477
  : el('p', { class: 'dim cal3-ov', 'data-cal3-notes': 'no-overview' }, 'The overview is written when you stop recording.'),
@@ -491,7 +498,8 @@ function after(s, n, mode) {
491
498
  if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length && !(s.shots || []).length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
492
499
  // the screenshots of the shared screen, after Stop, as the main app's Notes shows them (notes-screen-view.js)
493
500
  const shots = mode || n.private ? [] : s.shots || [];
494
- if (shots.length) kids.push(el('div', { 'data-cal3-notes': 'screen' }, screenStrip({ id: n.id, frames: shots, seek: s.audio ? (ms) => seek(s, ms) : null, replay: s.audio ? s.audio.el : null })));
501
+ const asideN = mode || n.private ? 0 : s.aside || 0; // shots set aside as another app: "N set aside" opens their review
502
+ if (shots.length || asideN) kids.push(el('div', { 'data-cal3-notes': 'screen' }, screenStrip({ id: n.id, frames: shots, aside: asideN, seek: s.audio ? (ms) => seek(s, ms) : null, replay: s.audio ? s.audio.el : null })));
495
503
  const tr = transcriptOf(n);
496
504
  if (!mode && tr.length) kids.push(el('details', { 'data-cal3-notes': 'transcript-fold' }, el('summary', { class: 'dim cal3-sm' }, 'Transcript · ' + tr.length + (tr.length === 1 ? ' line' : ' lines')),
497
505
  el('div', { class: 'term cal3-term' }, tr.map((x) => V.fmtTime(x.t || 0) + ' ' + x.text).join('\n'))));
@@ -0,0 +1,133 @@
1
+ /* pm/c3-replay.css — the meeting replay (initiative meeting-material-replay, part 2; the approved mockup #replay). Scoped under .c3replay.
2
+ * It is Focus played back, so the header, the track, the stage and the conversation follow c3-focus.css. Tokens only. */
3
+ .c3.rpl-open { container-type: normal; }
4
+ .c3replay { position: fixed; inset: 0; z-index: 50; overflow: auto; background: var(--background); color: var(--foreground); }
5
+ .c3replay [hidden] { display: none !important; }
6
+ .c3replay .rpl-page { display: grid; gap: var(--sp-2); max-width: 1480px; margin: 0 auto; padding: var(--sp-3) var(--sp-4) var(--sp-6, 32px); box-sizing: border-box; min-width: 0; }
7
+ .c3replay .rpl-page > * { min-width: 0; }
8
+ .c3replay .rpl-grow { flex: 1 1 auto; }
9
+ .c3replay .tag { display: inline-flex; align-items: center; gap: 4px; }
10
+ .c3replay .tag.rpl-warn, .c3replay .rpl-warn { color: var(--warning); }
11
+ .c3replay audio { display: none; }
12
+
13
+ /* header: who and when, the clock, the play controls, then the track across the full width */
14
+ .c3replay .rpl-top { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 6px var(--sp-4); align-items: center; padding: var(--sp-2) var(--sp-3); }
15
+ .c3replay .rpl-id { display: flex; gap: var(--sp-3); align-items: flex-start; min-width: 0; }
16
+ .c3replay .rpl-tt { min-width: 0; display: grid; gap: 4px; }
17
+ .c3replay .rpl-tt h1 { margin: 0; font-size: var(--fs-4); font-weight: 600; line-height: 1.2; overflow-wrap: anywhere; }
18
+ .c3replay .rpl-tt .chips2 { flex-wrap: wrap; gap: 6px; font-size: var(--fs-1); display: flex; align-items: center; }
19
+ .c3replay .rpl-warnc { color: var(--warning); }
20
+ .c3replay .rpl-clock > div { display: grid; justify-items: start; }
21
+ .c3replay .rpl-clock b { font-size: var(--fs-5); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
22
+ .c3replay .rpl-clock .dim { font-size: var(--fs-1); }
23
+ .c3replay .rpl-acts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
24
+
25
+ /* the track is the scrub bar: marks above, the stretches of talk, PMOS's notes below, the playhead across */
26
+ .c3replay .rpl-track { grid-column: 1 / -1; position: relative; }
27
+ .c3replay .rpl-rail { position: relative; cursor: pointer; }
28
+ .c3replay .rpl-mks { position: relative; height: 26px; }
29
+ .c3replay .rpl-runs { position: relative; height: 18px; border-radius: 6px; background: var(--muted); }
30
+ .c3replay .rpl-pms { position: relative; height: 10px; }
31
+ .c3replay .rpl-run { position: absolute; top: 0; bottom: 0; left: var(--fc-left, 0%); width: var(--fc-width, 0%); min-width: 0; min-height: 0; height: auto; padding: 0; margin: 0; box-sizing: border-box;
32
+ border: 0; border-right: 2px solid var(--card); border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; overflow: hidden;
33
+ background: color-mix(in oklab, var(--acc) 26%, var(--muted)); color: var(--foreground); font: 500 var(--fs-1) var(--mono); }
34
+ @media (hover: hover) { .c3replay .rpl-run:hover { background: color-mix(in oklab, var(--acc) 40%, var(--muted)); } }
35
+ .c3replay .rpl-run.c { background: var(--acc); color: var(--background); }
36
+ .c3replay .rpl-mkdot { position: absolute; top: 2px; left: var(--fc-left, 0%); transform: translateX(-50%); display: inline-grid; place-items: center; width: 20px; height: 20px; min-width: 0; min-height: 0; padding: 0; margin: 0;
37
+ border-radius: 50%; border: 1px solid var(--border); background: var(--card); color: var(--foreground); cursor: pointer; z-index: 1; }
38
+ .c3replay .rpl-mkdot .ic { width: 12px; height: 12px; }
39
+ .c3replay .rpl-mkdot.k-decision { border-color: var(--acc); color: var(--acc); }
40
+ .c3replay .rpl-mkdot.warn { border-color: var(--warning); color: var(--warning); }
41
+ .c3replay .rpl-mkdot.pm { top: 0; width: 8px; height: 8px; border: 0; background: color-mix(in oklab, var(--acc) 55%, transparent); }
42
+ .c3replay .rpl-now { position: absolute; top: 22px; bottom: 6px; width: 2px; margin-left: -1px; left: var(--fc-left, 0%); background: var(--foreground); pointer-events: none; transition: left .5s linear; }
43
+ .c3replay .rpl-ticks { position: relative; height: 14px; margin-top: 2px; font-size: var(--fs-1); }
44
+ .c3replay .rpl-tick { position: absolute; left: var(--fc-left, 0%); }
45
+ .c3replay .rpl-ticks .end { position: absolute; right: 0; }
46
+ .c3replay .rpl-legend { grid-column: 1 / -1; display: flex; gap: 14px; flex-wrap: wrap; font-size: var(--fs-1); }
47
+ .c3replay .rpl-lg { display: inline-flex; align-items: center; gap: 4px; }
48
+ .c3replay .rpl-lg.k-decision { color: var(--acc); }
49
+ .c3replay .rpl-lgdot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
50
+ .c3replay .rpl-lg.pm .rpl-lgdot { background: color-mix(in oklab, var(--acc) 55%, transparent); }
51
+ .c3replay .rpl-lg.warn .rpl-lgdot { border: 1px solid var(--warning); }
52
+ .c3replay .rpl-run:focus-visible, .c3replay .rpl-mkdot:focus-visible, .c3replay .rpl-lb:focus-visible, .c3replay .rpl-shotb:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
53
+
54
+ /* the now line */
55
+ .c3replay .rpl-nowl { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; padding: 0 var(--sp-1); min-height: 28px; }
56
+ .c3replay .rpl-nowt { display: inline-flex; align-items: center; gap: 8px; min-width: 0; font-size: var(--fs-3); flex-wrap: wrap; }
57
+ .c3replay .rpl-nowt .ki { width: 22px; height: 22px; }
58
+ .c3replay .rpl-nowt .mono { font-size: var(--fs-2); font-variant-numeric: tabular-nums; }
59
+
60
+ /* stage, key points, topics */
61
+ .c3replay .rpl-main { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 380px); gap: var(--sp-3); align-items: start; }
62
+ .c3replay .rpl-s { display: grid; gap: var(--sp-3); min-width: 0; }
63
+ .c3replay .rpl-stagewrap, .c3replay .rpl-keywrap, .c3replay .rpl-topwrap { min-width: 0; }
64
+ .c3replay .rpl-stagewrap:empty, .c3replay .rpl-keywrap:empty, .c3replay .rpl-topwrap:empty { display: none; }
65
+ .c3replay .rpl-stage .sh2 h2 { text-transform: none; letter-spacing: 0; font-size: var(--fs-3); font-weight: 600; color: var(--foreground); }
66
+ .c3replay .rpl-screen { display: grid; margin-top: var(--sp-2); }
67
+ .c3replay .rpl-shotb { display: block; width: 100%; height: auto; min-height: 0; padding: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--muted); cursor: pointer; }
68
+ .c3replay .rpl-shot { display: block; width: 100%; height: auto; max-height: 56vh; object-fit: contain; background: var(--muted); }
69
+ .c3replay .rpl-strip { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: var(--sp-2) 0 0; }
70
+ .c3replay .rpl-strip .chip2.on { background: var(--background); color: var(--foreground); box-shadow: 0 0 0 1px var(--acc); }
71
+ .c3replay .rpl-cap { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); }
72
+ .c3replay .rpl-col { min-width: 0; }
73
+ .c3replay .rpl-lbl { display: flex; align-items: center; gap: 6px; font-size: var(--fs-1); padding: var(--sp-2) 0 4px; }
74
+ .c3replay .rpl-none { font-size: var(--fs-2); padding: 4px 0; }
75
+ .c3replay .rpl-src { margin: var(--sp-2) 0 0; font-size: var(--fs-1); line-height: 1.5; }
76
+ .c3replay .rpl-key.cur, .c3replay .rpl-topic.cur { background: color-mix(in oklab, var(--acc) 8%, transparent); border-radius: 8px; }
77
+ .c3replay .rpl-key .chips, .c3replay .rpl-topic .chips { flex-wrap: wrap; }
78
+ .c3 .c3replay .nrow .nb > .chips > :nth-child(n+3) { display: inline-flex !important; } /* c3-quiet.css shows two chips a row; a key point and a topic show all */
79
+
80
+ /* the conversation: transcript lines and PMOS's notes in time order, the current line marked and followed */
81
+ .c3replay .rpl-w { position: sticky; top: var(--sp-3); min-width: 0; }
82
+ .c3replay .rpl-cv { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; max-height: min(760px, calc(100vh - 140px)); min-height: 280px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); overflow: hidden; }
83
+ .c3replay .rpl-cvh { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: var(--fs-2); flex-wrap: wrap; }
84
+ .c3replay .rpl-cvh .seg button { font-size: var(--fs-1); }
85
+ .c3replay .rpl-cvs { list-style: none; margin: 0; padding: 8px; display: grid; gap: 2px; overflow: auto; align-content: start; min-height: 0; position: relative; }
86
+ .c3replay .rpl-lb { display: grid; gap: 2px; width: 100%; height: auto; min-height: 0; padding: 6px 8px; border: 0; border-radius: 6px; background: none; color: inherit; text-align: left; font: inherit; white-space: normal; cursor: pointer; justify-content: stretch; }
87
+ .c3replay .rpl-lb > .rpl-lt, .c3replay .rpl-lb > .rpl-pb { display: block; overflow: visible; }
88
+ .c3replay .rpl-lb > .rpl-lh, .c3replay .rpl-lb > .rpl-cvwho { display: flex; overflow: visible; }
89
+ @media (hover: hover) { .c3replay .rpl-lb:hover { background: var(--muted); } }
90
+ .c3replay .rpl-l.on .rpl-lb { background: color-mix(in oklab, var(--acc) 10%, transparent); box-shadow: inset 2px 0 0 var(--acc); }
91
+ .c3replay .rpl-l.fut, .c3replay .rpl-pm.fut { opacity: .55; }
92
+ .c3replay .rpl-lh { display: flex; gap: 6px; align-items: center; font-size: var(--fs-1); }
93
+ .c3replay .rpl-lh .tag { margin-left: auto; }
94
+ .c3replay .rpl-lt { font-size: var(--fs-3); line-height: 1.45; }
95
+ .c3replay .rpl-kt.k-decision { color: var(--acc); }
96
+ .c3replay .rpl-pm .rpl-lb { background: color-mix(in oklab, var(--acc) 6%, transparent); }
97
+ .c3replay .rpl-cvwho { display: flex; align-items: center; gap: 6px; font-size: var(--fs-1); color: var(--muted-foreground); }
98
+ .c3replay .rpl-cvwho b { color: var(--acc); font-weight: 600; }
99
+ .c3replay .rpl-cvwho .rpl-time { margin-left: auto; }
100
+ .c3replay .fc-avdot { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; background: color-mix(in oklab, var(--acc) 14%, transparent); color: var(--acc); flex: 0 0 auto; }
101
+ .c3replay .fc-avdot .ic { width: 10px; height: 10px; }
102
+ .c3replay .rpl-cat::before { content: "· "; }
103
+ .c3replay .rpl-pb { font-size: var(--fs-2); color: var(--muted-foreground); line-height: 1.45; }
104
+ .c3replay .rpl-cvin { display: flex; gap: 6px; align-items: center; padding: 8px 10px; border-top: 1px solid var(--border); }
105
+ .c3replay .rpl-cvin input { flex: 1; min-width: 0; height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--background); color: var(--foreground); font: inherit; font-size: var(--fs-2); }
106
+ .c3replay .rpl-cvin input:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 1px; }
107
+
108
+ @media (max-width: 1100px) { .c3replay .rpl-main { grid-template-columns: minmax(0, 1fr) minmax(280px, 340px); } }
109
+ @media (max-width: 900px) {
110
+ .c3replay .rpl-page { padding: var(--sp-2) 16px var(--sp-5, 24px); }
111
+ .c3replay .rpl-main { grid-template-columns: minmax(0, 1fr); }
112
+ .c3replay .rpl-w { position: static; }
113
+ .c3replay .rpl-cv { max-height: 520px; }
114
+ .c3replay .rpl-cap { grid-template-columns: minmax(0, 1fr); }
115
+ }
116
+ @media (max-width: 760px) {
117
+ .c3replay .rpl-top { grid-template-columns: minmax(0, 1fr); }
118
+ .c3replay .rpl-id { flex-direction: column; gap: var(--sp-2); }
119
+ .c3replay .rpl-acts { justify-content: flex-start; }
120
+ .c3replay .rpl-ticks .rpl-tick:not(:first-child) { display: none; }
121
+ }
122
+ @media (pointer: coarse) {
123
+ .c3replay .rpl-mkdot { width: 28px; height: 28px; top: 0; }
124
+ .c3replay .rpl-mkdot.pm { width: 12px; height: 12px; }
125
+ .c3replay .rpl-lb { min-height: 44px; }
126
+ }
127
+ @media (prefers-reduced-motion: reduce) { .c3replay .rpl-now { transition: none; } }
128
+
129
+ /* meeting-material-replay-gaps: where a recording was off between two starts */
130
+ .c3replay .rpl-gap { position: absolute; top: 0; bottom: 0; left: var(--fc-left, 0%); width: var(--fc-width, 0%); min-width: 0; min-height: 0; height: auto; padding: 0; margin: 0; box-sizing: border-box; border: 0; border-left: 1px dashed var(--muted-foreground); border-right: 1px dashed var(--muted-foreground); background: repeating-linear-gradient(135deg, transparent 0 4px, var(--border) 4px 6px); cursor: pointer; }
131
+ .c3replay .rpl-gap:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
132
+ .c3replay .rpl-gapnote { display: flex; gap: 8px; align-items: center; font-size: var(--fs-2); }
133
+ .c3replay .rpl-gapkey { display: inline-block; width: 22px; height: 10px; flex: none; border: 1px dashed var(--muted-foreground); background: repeating-linear-gradient(135deg, transparent 0 3px, var(--border) 3px 5px); }
@@ -0,0 +1,429 @@
1
+ /* pm/c3-replay.js — the meeting replay of console3 (initiative meeting-material-replay, part 2; the mockup the PM approved on 2026-10-14, #replay).
2
+ * openReplay({ note, event, date }) closeReplay() window.C3Replay = { open, close, state }
3
+ * Focus played back, full screen over console3 like Focus (inside #c3-root; the header, tabs and dock stay in the DOM, set inert).
4
+ * One read, GET /api/meeting-replay: the note (transcript, decisions, next steps, questions), the Focus state (topics, stretches, PMOS's notes)
5
+ * and the kept shots. The page plays the note's audio (GET /api/notes/recording) and ticks every 500 ms:
6
+ * header title and time, Back 10 s, Play or Pause, 1x 1.5x 2x; the topic track as the scrub bar: stretches numbered by topic, a mark for each
7
+ * decision, question and next step (amber when it needs you: no owner, still open), a dot for each PMOS note, the playhead
8
+ * now line the topic the room was on, what is on screen, the count of key points
9
+ * stage the shot on screen at the playing time (a press opens it large), the strip of shots, "N set aside"
10
+ * key points decisions, next steps and questions, each with the time to play from; Confirm all makes a task of each next step
11
+ * topics minutes and the first time each came up
12
+ * conversation the transcript lines and PMOS's notes in time order; the current line is marked and followed; All, Key points, PMOS; search
13
+ * A press on a stretch, a mark, a key point, a topic, a shot or a line plays from there. Without audio (none was kept) the page keeps its own
14
+ * clock, so the track, the shots and the lines still follow. Everything stays on this machine. */
15
+ import { el } from './dom.js';
16
+ import { pmGet } from './http.js';
17
+ import { ensureC3Css } from './pm-work-view.js';
18
+ import { ensureComponentsCss } from './primitives.js';
19
+ import { icon, sect, pbtn, lk, btn, nrow, put, geo } from './c3-ini-ui.js';
20
+ import { toast } from './c3-toast.js';
21
+ import { openShot, whyText, shotIndexAt } from './notes-screen-view.js';
22
+
23
+ const TICK_MS = 500;
24
+ const NEAR_KEY_MS = 30000; // a key point is "now" this long after its line
25
+ const BACK_MS = 10000;
26
+ const KIC = { decision: 'scale', action: 'todo', question: 'reply' };
27
+ const KWORD = { decision: 'Decision', action: 'Next step', question: 'Question' };
28
+ const KPLURAL = { decision: 'Decisions', action: 'Next steps', question: 'Questions' };
29
+ const pad = (n) => String(n).padStart(2, '0');
30
+ const NO_NOTE = 'That replay link does not match a note on this computer. It may be from another computer, or the note was deleted. Open it from Calendar, Notes.';
31
+ /** A note id, as lib/notes-audio.js names it. The ?replay= link is checked against this before any request is made. Pure. */
32
+ export const NOTE_ID_RE = /^n-[0-9a-z]{4,16}-[0-9a-f]{4}$/;
33
+ /** The gap that holds time t (the recording was off), or null. Pure. */
34
+ export const gapAt = (gaps, t) => (gaps || []).find((g) => t >= g.from_ms && t < g.to_ms) || null;
35
+
36
+ /** 3:07, or 1:03:07 from an hour on. Pure. */
37
+ export function mmss(ms) {
38
+ const s = Math.max(0, Math.round((Number(ms) || 0) / 1000));
39
+ const h = Math.floor(s / 3600);
40
+ return (h ? h + ':' + pad(Math.floor(s / 60) % 60) : String(Math.floor(s / 60))) + ':' + pad(s % 60);
41
+ }
42
+ /** The index of the stretch the room was in at the time: the last stretch that began at or before it, or -1 before the first. Pure. */
43
+ export function runIndexAt(runs, t) {
44
+ let at = -1;
45
+ (runs || []).forEach((r, i) => { if (r.from_ms <= t) at = i; });
46
+ return at;
47
+ }
48
+ /** The index of the last line said at or before the time, or -1. Pure. */
49
+ export function lineIndexAt(lines, t) {
50
+ let lo = 0; let hi = (lines || []).length - 1; let at = -1;
51
+ while (lo <= hi) { const mid = (lo + hi) >> 1; if (lines[mid].t <= t) { at = mid; lo = mid + 1; } else hi = mid - 1; }
52
+ return at;
53
+ }
54
+
55
+ let S = null; // the open page
56
+
57
+ function ensureCss() {
58
+ ensureC3Css(); ensureComponentsCss();
59
+ if (document.querySelector('link[data-c3-replay]')) return;
60
+ const l = document.createElement('link');
61
+ l.rel = 'stylesheet'; l.href = new URL('./c3-replay.css', import.meta.url).href; l.setAttribute('data-c3-replay', '');
62
+ document.head.appendChild(l);
63
+ }
64
+
65
+ /* ── open and close ───────────────────────────────────────────────────────────────────────────────────────────── */
66
+
67
+ export function closeReplay() {
68
+ if (!S) return;
69
+ const s = S; S = null;
70
+ clearInterval(s.timer);
71
+ try { s.audio.pause(); s.audio.removeAttribute('src'); s.audio.load(); } catch { /* no audio */ }
72
+ s.root.remove();
73
+ const host = document.getElementById('c3-root');
74
+ if (host) host.classList.remove('rpl-open');
75
+ const grid = host && host.querySelector('.c3grid'); if (grid) grid.inert = false;
76
+ const comp = host && host.querySelector('[data-c3ph]'); if (comp) comp.inert = false;
77
+ if (s.opener && document.contains(s.opener) && typeof s.opener.focus === 'function') { try { s.opener.focus({ preventScroll: true }); } catch { /* not focusable */ } }
78
+ }
79
+
80
+ export function openReplay({ note, event, date } = {}) {
81
+ closeReplay();
82
+ ensureCss();
83
+ const host = document.getElementById('c3-root') || document.body;
84
+ const root = el('div', { class: 'c3replay', role: 'region', 'aria-label': 'Meeting replay', 'data-c3replay': '', 'data-rpl-state': 'loading' },
85
+ el('div', { class: 'rpl-page' }, el('div', { class: 'fold', 'data-rpl': 'status' }, icon('clock', 'sm'), 'Reading the meeting …')));
86
+ S = { root, note: String(note || ''), event: event && (event.id || event) ? String(event.id || event) : '', date: String(date || ''), data: null, t: 0, play: false, speed: 1, filter: 'all', q: '',
87
+ audio: null, soundOff: false, timer: null, done: new Set(), r: {}, ix: { run: -2, shot: -2, line: -2 },
88
+ opener: document.activeElement && document.activeElement !== document.body ? document.activeElement : null };
89
+ host.classList.add('rpl-open');
90
+ host.append(root);
91
+ const grid = host.querySelector && host.querySelector('.c3grid'); if (grid) grid.inert = true;
92
+ const comp = host.querySelector && host.querySelector('[data-c3ph]'); if (comp) comp.inert = true;
93
+ load(S);
94
+ }
95
+
96
+ async function load(s) {
97
+ if (!s.note && !s.event) { fail(s, NO_NOTE); return; } // a ?replay= link that is not a note id: no request is made
98
+ const p = new URLSearchParams();
99
+ if (s.note) p.set('note', s.note);
100
+ if (s.event) p.set('event', s.event);
101
+ if (s.date) p.set('date', s.date);
102
+ let d = null;
103
+ try { d = await pmGet('/api/meeting-replay?' + p.toString()); } catch (e) { d = { ok: false, error: String((e && e.message) || e) }; }
104
+ if (S !== s) return;
105
+ if (!d || !d.ok) { fail(s, d && d.error === 'no such note' ? NO_NOTE : (d && d.error) || 'The replay could not be read.'); return; }
106
+ s.data = d;
107
+ s.root.dataset.rplState = 'ok';
108
+ if (d.audio) {
109
+ s.audio = el('audio', { preload: 'metadata', src: d.audio.url, 'data-rpl': 'audio' });
110
+ s.audio.addEventListener('ended', () => { if (S === s) { s.play = false; paintTop(s); } });
111
+ s.audio.addEventListener('error', () => { if (S === s) s.soundOff = true; });
112
+ }
113
+ build(s);
114
+ s.timer = setInterval(() => tick(s), TICK_MS);
115
+ const back = s.root.querySelector('[data-rpl="leave"]'); if (back) back.focus({ preventScroll: true });
116
+ }
117
+
118
+ function fail(s, message) {
119
+ s.root.dataset.rplState = 'error';
120
+ put(s.root, el('div', { class: 'rpl-page' }, el('div', { class: 'fold', 'data-rpl': 'status' }, icon('alert', 'sm'), message),
121
+ el('div', { class: 'iacts' }, pbtn('ghost', 'Back', 'prev', { 'data-rpl': 'close' }))));
122
+ }
123
+
124
+ /* ── time ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
125
+
126
+ const lenOf = (s) => Math.max(1, s.data.length_ms || 0);
127
+ const wall = (s, ms) => (s.data.start_ms == null ? mmss(ms) : (() => { const d = new Date(s.data.start_ms + ms); return pad(d.getHours()) + ':' + pad(d.getMinutes()); })());
128
+ const pct = (s, ms) => Math.max(0, Math.min(100, ms / lenOf(s) * 100));
129
+
130
+ function playNow(s) {
131
+ s.play = true;
132
+ if (s.audio && !s.soundOff) {
133
+ s.audio.playbackRate = s.speed;
134
+ const p = s.audio.play();
135
+ if (p && typeof p.catch === 'function') p.catch(() => { s.soundOff = true; });
136
+ }
137
+ }
138
+ function pauseNow(s) { s.play = false; if (s.audio) { try { s.audio.pause(); } catch { /* none */ } } }
139
+
140
+ /** Move the playhead. `andPlay`: start playing from there (a track, a key point, a topic, a shot or a line); Back 10 s keeps the state. */
141
+ function seek(s, ms, andPlay) {
142
+ const len = lenOf(s);
143
+ s.t = Math.max(0, Math.min(len, Math.round(ms)));
144
+ if (s.audio && !s.soundOff) { try { s.audio.currentTime = s.t / 1000; } catch { /* not ready yet */ } }
145
+ if (andPlay) playNow(s);
146
+ paintTop(s);
147
+ paintTime(s, true);
148
+ }
149
+
150
+ function tick(s) {
151
+ if (S !== s || !s.data) return;
152
+ const len = lenOf(s);
153
+ if (s.play) {
154
+ if (s.audio && !s.soundOff && !s.audio.paused) s.t = Math.round(s.audio.currentTime * 1000);
155
+ else s.t = Math.min(len, s.t + TICK_MS * s.speed); // no sound (none kept, or it cannot play): the page keeps the time
156
+ const gp = gapAt(s.data.gaps, s.t);
157
+ if (gp && s.t < len) { s.t = gp.to_ms; if (s.audio && !s.soundOff) { try { s.audio.currentTime = s.t / 1000; } catch { /* not ready */ } } } // the recording was off: play on from where it began again
158
+ if (s.t >= len) { s.t = len; s.play = false; if (s.audio) { try { s.audio.pause(); } catch { /* none */ } } paintTop(s); }
159
+ }
160
+ paintTime(s, false);
161
+ }
162
+
163
+ /* ── building ─────────────────────────────────────────────────────────────────────────────────────────────────── */
164
+
165
+ function build(s) {
166
+ const r = s.r;
167
+ r.top = el('header', { class: 'card rpl-top', 'aria-label': 'Meeting replay' });
168
+ r.now = el('div', { class: 'rpl-nowl', 'data-rpl': 'now-line' });
169
+ r.stage = el('div', { class: 'rpl-stagewrap' });
170
+ r.keys = el('div', { class: 'rpl-keywrap' });
171
+ r.topics = el('div', { class: 'rpl-topwrap' });
172
+ r.convo = el('section', { class: 'rpl-cv', 'aria-label': 'What was said, with PMOS\'s notes' });
173
+ put(s.root, el('div', { class: 'rpl-page' }, r.top, r.now,
174
+ el('div', { class: 'rpl-main' }, el('div', { class: 'rpl-s' }, r.stage, r.keys, r.topics), el('aside', { class: 'rpl-w', 'aria-label': 'Conversation' }, r.convo)), s.audio));
175
+ buildTop(s);
176
+ buildKeys(s);
177
+ buildTopics(s);
178
+ buildConvo(s);
179
+ s.ix = { run: -2, shot: -2, line: -2 };
180
+ paintTime(s, true);
181
+ }
182
+
183
+ function chipsOf(s) {
184
+ const d = s.data;
185
+ const start = d.meeting.at ? new Date(d.meeting.at) : null;
186
+ const day = start && Number.isFinite(start.getTime()) ? start.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' }) : '';
187
+ const len = Math.round(lenOf(s) / 60000);
188
+ const kept = d.shots.length;
189
+ return el('div', { class: 'chips2 dim mono' },
190
+ el('span', null, [day, d.start_ms != null ? wall(s, 0) + ' to ' + wall(s, lenOf(s)) : '', len + ' min'].filter(Boolean).join(' · ')),
191
+ d.meeting.attendees.length ? el('span', null, d.meeting.attendees.length + (d.meeting.attendees.length === 1 ? ' person' : ' people')) : null,
192
+ el('span', { title: 'Audio and screen shots are kept on this machine' }, icon('lock', 'sm'), (d.audio ? 'audio' : 'no audio') + (kept ? ' and ' + kept + (kept === 1 ? ' screen shot' : ' screen shots') : '')),
193
+ (d.recordings || 1) > 1 ? el('span', { class: 'tag warn', 'data-rpl': 'recordings', title: 'The recording was stopped and started again. The gap is marked on the track.' }, icon('alert', 'sm'), d.recordings + ' recordings') : null,
194
+ d.aside_count ? lk({ 'data-rpl': 'aside', class: 'rpl-warnc', title: 'Shots that looked like another app. They are not in the replay.' }, d.aside_count + ' set aside') : null);
195
+ }
196
+
197
+ /** '7 min', or '40 s' under a minute. Pure. */
198
+ export const gapText = (ms) => (ms < 60000 ? Math.max(1, Math.round(ms / 1000)) + ' s' : Math.round(ms / 60000) + ' min');
199
+
200
+ function trackOf(s) {
201
+ const d = s.data;
202
+ const marks = d.keys.filter((k) => k.t != null).map((k) => {
203
+ const b = btn({ class: 'rpl-mkdot k-' + k.k + (k.warn ? ' warn' : ''), 'data-rpl-seek': String(k.t), 'data-rpl-mark': k.id, title: KWORD[k.k] + ' at ' + wall(s, k.t) + ': ' + k.text, 'aria-label': KWORD[k.k] + ' at ' + wall(s, k.t) }, icon(KIC[k.k], 'sm'));
204
+ return geo(b, { '--fc-left': pct(s, k.t) + '%' });
205
+ });
206
+ const pms = d.notes.map((n) => geo(btn({ class: 'rpl-mkdot pm', 'data-rpl-seek': String(n.t), 'data-rpl-pm': n.id, title: 'PMOS note at ' + wall(s, n.t) + ': ' + n.body, 'aria-label': 'PMOS note at ' + wall(s, n.t) }), { '--fc-left': pct(s, n.t) + '%' }));
207
+ const topicText = (id) => (d.topics.find((t) => t.id === id) || {}).text || '';
208
+ const runs = d.runs.map((rn, i) => {
209
+ const w = (rn.to_ms - rn.from_ms) / lenOf(s) * 100;
210
+ const b = btn({ class: 'rpl-run', 'data-rpl-seek': String(rn.from_ms), 'data-rpl-run': String(i), title: (rn.n ? rn.n + '. ' : '') + topicText(rn.topic) + ' · ' + wall(s, rn.from_ms) + ' · ' + Math.max(1, Math.round((rn.to_ms - rn.from_ms) / 60000)) + ' min' }, w >= 5 && rn.n ? String(rn.n) : '');
211
+ return geo(b, { '--fc-left': pct(s, rn.from_ms) + '%', '--fc-width': Math.max(0.5, w) + '%' });
212
+ });
213
+ const gaps = (d.gaps || []).map((g, i) => geo(btn({ class: 'rpl-gap', 'data-rpl-seek': String(g.to_ms), 'data-rpl-gap': String(i), title: 'Not recorded ' + wall(s, g.from_ms) + ' to ' + wall(s, g.to_ms) + ' (' + gapText(g.to_ms - g.from_ms) + '). A press plays from where the recording began again.', 'aria-label': 'Not recorded ' + wall(s, g.from_ms) + ' to ' + wall(s, g.to_ms) }),
214
+ { '--fc-left': pct(s, g.from_ms) + '%', '--fc-width': Math.max(0.6, (g.to_ms - g.from_ms) / lenOf(s) * 100) + '%' }));
215
+ const head = geo(el('span', { class: 'rpl-now', 'data-rpl': 'head' }), { '--fc-left': '0%' });
216
+ const ticks = [0, 15, 30, 45, 60, 75, 90].map((m) => m * 60000).filter((ms) => ms < lenOf(s) * 0.92)
217
+ .map((ms) => geo(el('span', { class: 'rpl-tick' }, wall(s, ms)), { '--fc-left': pct(s, ms) + '%' }));
218
+ return el('div', { class: 'rpl-track', role: 'group', 'aria-label': 'The meeting by topic, with decisions, questions and next steps. Press a stretch or a mark to play from there.' },
219
+ el('div', { class: 'rpl-rail', 'data-rpl': 'rail' }, el('div', { class: 'rpl-mks' }, ...marks), el('div', { class: 'rpl-runs', 'data-rpl': 'bar' }, ...runs, ...gaps), el('div', { class: 'rpl-pms' }, ...pms), head),
220
+ el('div', { class: 'rpl-ticks dim mono' }, ...ticks, el('span', { class: 'end' }, wall(s, lenOf(s)))));
221
+ }
222
+
223
+ function buildTop(s) {
224
+ const d = s.data;
225
+ const legend = el('div', { class: 'rpl-legend dim' },
226
+ ...['decision', 'question', 'action'].map((k) => el('span', { class: 'rpl-lg k-' + k }, icon(KIC[k], 'sm'), KPLURAL[k])),
227
+ el('span', { class: 'rpl-lg pm' }, el('i', { class: 'rpl-lgdot' }), 'PMOS notes'), el('span', { class: 'rpl-lg warn' }, el('i', { class: 'rpl-lgdot' }), 'needs you'));
228
+ put(s.r.top,
229
+ el('div', { class: 'rpl-id' },
230
+ pbtn('ghost', 'Back to the note', 'prev', { 'data-rpl': 'leave', title: 'Back to the note in Calendar' }),
231
+ el('div', { class: 'rpl-tt' }, el('h1', null, d.meeting.title), chipsOf(s))),
232
+ el('div', { class: 'rpl-clock' }, el('div', null, el('b', { class: 'mono', 'data-rpl': 't' }, mmss(s.t)),
233
+ el('span', { class: 'dim mono' }, 'of ' + mmss(lenOf(s)) + ' · ', el('span', { 'data-rpl': 'clk' }, wall(s, s.t))))),
234
+ el('div', { class: 'rpl-acts', 'data-rpl': 'acts' }),
235
+ trackOf(s), legend,
236
+ (d.gaps || []).length ? el('div', { class: 'rpl-gapnote dim', 'data-rpl': 'gapnote' }, el('i', { class: 'rpl-gapkey', 'aria-hidden': 'true' }),
237
+ d.gaps.map((g) => 'Not recorded ' + wall(s, g.from_ms) + ' to ' + wall(s, g.to_ms) + ' (' + gapText(g.to_ms - g.from_ms) + ').').join(' ') + ' You stopped the recording and started it again. Playing across a gap skips it.') : null);
238
+ paintTop(s);
239
+ }
240
+
241
+ /** The play controls: they change with Play, Pause and the speed, so they are drawn on their own. */
242
+ function paintTop(s) {
243
+ const acts = s.r.top && s.r.top.querySelector('[data-rpl="acts"]');
244
+ if (!acts) return;
245
+ const play = pbtn('primary', s.play ? 'Pause' : 'Play', s.play ? 'pause' : 'play', { 'data-rpl': 'play', 'aria-pressed': String(s.play) });
246
+ put(acts, pbtn('ghost', 'Back 10 s', 'undo', { 'data-rpl': 'back' }), play,
247
+ el('div', { class: 'seg ini-seg', role: 'group', 'aria-label': 'Speed' }, ...[1, 1.5, 2].map((v) => btn({ 'aria-pressed': String(s.speed === v), 'data-rpl-speed': String(v) }, v + '×'))));
248
+ }
249
+
250
+ function nowLine(s) {
251
+ const d = s.data;
252
+ const ri = runIndexAt(d.runs, s.t);
253
+ const run = ri >= 0 ? d.runs[ri] : null;
254
+ const tp = run ? d.topics.find((t) => t.id === run.topic) : null;
255
+ const si = shotIndexAt(d.shots, s.t);
256
+ const need = d.keys.filter((k) => k.warn).length;
257
+ return [el('span', { class: 'rpl-nowt' }, el('span', { class: 'ki hot' }, icon('play', 'sm')),
258
+ tp ? [el('span', { class: 'mono dim' }, run.n || ''), el('b', { 'data-rpl': 'topic' }, tp.text), el('span', { class: 'mono dim' }, wall(s, run.from_ms) + ' to ' + wall(s, run.to_ms))] : el('span', { class: 'dim' }, d.topics.length ? 'Before the first topic' : 'No topics were kept for this meeting')),
259
+ el('span', { class: 'rpl-grow' }),
260
+ d.shots.length ? el('span', { class: 'chip2', 'data-rpl': 'shot-state' }, icon('eye', 'sm'), si >= 0 ? 'Screen shot from ' + wall(s, d.shots[si].t) : 'Nothing shared yet') : null,
261
+ d.keys.length ? lk({ 'data-rpl': 'keys' }, icon('check', 'sm'), d.keys.length + (d.keys.length === 1 ? ' key point' : ' key points') + (need ? ' · ' + need + ' need you' : '')) : null];
262
+ }
263
+
264
+ function stageNode(s) {
265
+ const d = s.data;
266
+ const si = shotIndexAt(d.shots, s.t);
267
+ const sh = si >= 0 ? d.shots[si] : null;
268
+ const strip = d.shots.map((x, i) => btn({ class: 'chip2' + (i === si ? ' on' : ''), 'data-rpl-seek': String(x.t), 'data-rpl-shot': String(x.t), 'aria-pressed': String(i === si), title: whyText(x.why) }, wall(s, x.t)));
269
+ const screen = sh
270
+ ? btn({ class: 'rpl-shotb', 'data-rpl': 'open-shot', title: 'Open this shot large' },
271
+ el('img', { class: 'rpl-shot', src: '/api/notes/screen?id=' + encodeURIComponent(d.note.id) + '&t=' + Math.round(sh.t), alt: sh.text ? 'Screen at ' + mmss(sh.t) + ': ' + sh.text.slice(0, 200) : 'Screen at ' + mmss(sh.t), 'data-rpl-img': String(sh.t) }))
272
+ : el('div', { class: 'fold', 'data-rpl': 'no-shot' }, icon('moon', 'sm'), d.shots.length ? 'Nothing was shared yet. The first screen shot is at ' + wall(s, d.shots[0].t) + '.' : 'No screen shots were kept for this meeting.');
273
+ return sect({ ic: 'eye', title: 'On screen', attrs: { class: 'rpl-stage', 'data-rpl': 'stage' }, right: sh ? el('span', { class: 'dim mono' }, (whyText(sh.why) ? whyText(sh.why) + ' · ' : '') + wall(s, sh.t)) : null },
274
+ el('div', { class: 'rpl-screen' }, screen),
275
+ d.shots.length || d.aside_count ? el('div', { class: 'rpl-strip', role: 'group', 'aria-label': 'Screen shots kept, by time' }, ...strip, el('span', { class: 'rpl-grow' }),
276
+ d.aside_count ? pbtn('ghost', d.aside_count + ' set aside', 'eye', { 'data-rpl': 'aside' }) : null) : null);
277
+ }
278
+
279
+ function buildKeys(s) {
280
+ const d = s.data;
281
+ const col = (k) => {
282
+ const items = d.keys.filter((x) => x.k === k);
283
+ return el('div', { class: 'rpl-col' }, el('div', { class: 'rpl-lbl' }, icon(KIC[k], 'sm'), ' ' + KPLURAL[k] + ' ', el('span', { class: 'bdg g' }, String(items.length))),
284
+ items.length ? el('ul', { class: 'list', role: 'list' }, ...items.map((x) => keyRow(s, x))) : el('div', { class: 'dim rpl-none' }, 'None caught.'));
285
+ };
286
+ put(s.r.keys, d.keys.length ? sect({ ic: 'check', title: 'Key points', attrs: { 'data-rpl': 'keys-sect' }, right: d.keys.some((x) => x.k === 'action') ? pbtn('primary', 'Confirm all', 'check', { 'data-rpl': 'confirm', title: 'Make a task of each next step on the note\'s initiative' }) : null },
287
+ el('div', { class: 'rpl-cap' }, col('decision'), col('action'), col('question')),
288
+ el('p', { class: 'dim rpl-src' }, 'Each one plays from the moment it was said. Confirm all makes a task of each next step on the note\'s initiative.')) : null);
289
+ }
290
+ function keyRow(s, x) {
291
+ const made = s.done.has(x.id);
292
+ const meta = x.k === 'decision' ? x.by : x.k === 'action' ? [x.owner, x.due && 'due ' + x.due].filter(Boolean).join(' · ') : x.state === 'answered' ? 'answered' : 'still open';
293
+ return nrow({ title: x.text, ic: KIC[x.k], hot: x.warn, cls: 'rpl-key', attrs: { 'data-rpl-key': x.id, ...(x.t != null ? { 'data-rpl-keyt': String(x.t) } : {}) },
294
+ chips: [x.t != null ? lk({ class: 'mono', 'data-rpl-seek': String(x.t), 'data-rpl-play': x.id, title: 'Play from ' + wall(s, x.t) }, icon('play', 'sm'), wall(s, x.t)) : null,
295
+ x.state === 'owner' ? el('span', { class: 'tag rpl-warn' }, icon('user', 'sm'), 'no owner') : meta ? el('span', { class: 'tag' + (x.state === 'open' ? ' rpl-warn' : '') }, meta) : null,
296
+ made ? el('span', { class: 'tag' }, 'task made') : x.k !== 'question' ? el('span', { class: 'tag' }, 'draft') : null] });
297
+ }
298
+
299
+ function buildTopics(s) {
300
+ const d = s.data;
301
+ const rows = d.topics.map((t) => el('li', { class: 'nrow rpl-topic', 'data-rpl-topic': t.id },
302
+ el('span', { class: 'ki' }, t.n ? el('span', { class: 'mono' }, String(t.n)) : icon('clock', 'sm')),
303
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, t.text),
304
+ el('div', { class: 'chips' },
305
+ t.first_t != null ? lk({ class: 'mono', 'data-rpl-seek': String(t.first_t), title: 'Play from the first time it came up' }, icon('play', 'sm'), wall(s, t.first_t)) : null,
306
+ t.parked ? el('span', { class: 'tag rpl-warn' }, 'parked') : t.first_t == null ? el('span', { class: 'tag' }, 'not reached') : el('span', { class: 'tag' }, (t.minutes < 1 ? 'under 1 min' : t.minutes + ' min') + (t.stretches > 1 ? ' in ' + t.stretches + ' stretches' : '')),
307
+ t.by ? el('span', { class: 'tag' }, 'added by ' + t.by) : null)),
308
+ el('span')));
309
+ put(s.r.topics, d.topics.length ? sect({ ic: 'todo', title: 'Topics', count: d.topics.length, attrs: { 'data-rpl': 'topics-sect' } }, el('ul', { class: 'list', role: 'list' }, ...rows)) : null);
310
+ }
311
+
312
+ /* the conversation: built when the filter or the search changes; the current line is marked in place as time moves */
313
+ function buildConvo(s) {
314
+ const d = s.data;
315
+ const q = s.q.trim().toLowerCase();
316
+ const items = [...d.lines.map((l) => ({ t: l.t, l })), ...d.notes.map((n) => ({ t: n.t, n }))].sort((a, b) => a.t - b.t || (a.n ? 1 : 0) - (b.n ? 1 : 0))
317
+ .filter((x) => s.filter === 'all' || (s.filter === 'key' ? x.l && x.l.key : x.n))
318
+ .filter((x) => !q || (x.l ? x.l.text : x.n.body).toLowerCase().includes(q));
319
+ const rows = items.map((x) => {
320
+ if (x.n) {
321
+ return el('li', { class: 'rpl-pm', 'data-rpl-pmt': String(x.t) }, btn({ class: 'rpl-lb', 'data-rpl-seek': String(x.t) },
322
+ el('span', { class: 'rpl-cvwho' }, el('span', { class: 'fc-avdot', 'aria-hidden': 'true' }, icon('spark', 'sm')), el('b', { class: 'mono' }, 'PMOS'), el('span', { class: 'rpl-cat' }, x.n.cat), el('span', { class: 'dim mono rpl-time' }, wall(s, x.t))),
323
+ el('span', { class: 'rpl-pb' }, x.n.body)));
324
+ }
325
+ const l = x.l;
326
+ const k = l.key ? d.keys.find((z) => z.id === l.key.id) : null;
327
+ return el('li', { class: 'rpl-l', 'data-rpl-line': String(l.i), 'data-rpl-linet': String(l.t) }, btn({ class: 'rpl-lb', 'data-rpl-seek': String(l.t) },
328
+ el('span', { class: 'rpl-lh' }, el('b', null, l.who || 'You'), el('span', { class: 'dim mono' }, wall(s, l.t)), k ? el('span', { class: 'tag rpl-kt k-' + k.k }, icon(KIC[k.k], 'sm'), KWORD[k.k]) : null),
329
+ el('span', { class: 'rpl-lt' }, l.text)));
330
+ });
331
+ const seg = el('div', { class: 'seg ini-seg', role: 'group', 'aria-label': 'Show' }, ...[['all', 'All'], ['key', 'Key points'], ['pm', 'PMOS']].map(([k, label]) => btn({ 'aria-pressed': String(s.filter === k), 'data-rpl-filter': k }, label)));
332
+ const input = el('input', { type: 'search', 'data-rpl-q': '', 'aria-label': 'Search what was said', placeholder: 'Search what was said', value: s.q });
333
+ put(s.r.convo, el('div', { class: 'rpl-cvh' }, el('b', null, 'Conversation'), seg),
334
+ el('ol', { class: 'rpl-cvs', role: 'list', 'data-rpl': 'list' }, rows.length ? rows : el('li', { class: 'dim' }, d.lines.length || d.notes.length ? 'Nothing matches.' : 'No words were kept for this meeting.')),
335
+ el('form', { class: 'rpl-cvin', onSubmit: (e) => e.preventDefault() }, input, icon('search', 'sm')));
336
+ s.ix.line = -2;
337
+ }
338
+
339
+ /* ── painting the time ────────────────────────────────────────────────────────────────────────────────────────── */
340
+
341
+ function paintTime(s, force) {
342
+ const d = s.data; const root = s.root;
343
+ const set = (k, v) => { const n = root.querySelector('[data-rpl="' + k + '"]'); if (n && n.textContent !== v) n.textContent = v; };
344
+ set('t', mmss(s.t)); set('clk', wall(s, s.t));
345
+ const head = root.querySelector('[data-rpl="head"]'); if (head) head.style.setProperty('--fc-left', pct(s, s.t) + '%');
346
+ root.dataset.rplT = String(Math.round(s.t));
347
+ const ri = runIndexAt(d.runs, s.t); const si = shotIndexAt(d.shots, s.t); const li = lineIndexAt(d.lines, s.t);
348
+ if (force || ri !== s.ix.run) {
349
+ root.querySelectorAll('.rpl-run').forEach((b) => b.classList.toggle('c', Number(b.dataset.rplRun) === ri));
350
+ const cur = ri >= 0 ? d.runs[ri].topic : null;
351
+ root.querySelectorAll('.rpl-topic').forEach((n) => n.classList.toggle('cur', n.dataset.rplTopic === cur));
352
+ }
353
+ if (force || ri !== s.ix.run || si !== s.ix.shot) put(s.r.now, nowLine(s));
354
+ if (force || si !== s.ix.shot) put(s.r.stage, stageNode(s));
355
+ s.ix.run = ri; s.ix.shot = si;
356
+ root.querySelectorAll('.rpl-key[data-rpl-keyt]').forEach((n) => n.classList.toggle('cur', s.t >= Number(n.dataset.rplKeyt) && s.t - Number(n.dataset.rplKeyt) < NEAR_KEY_MS));
357
+ const list = root.querySelector('[data-rpl="list"]');
358
+ if (list && (force || li !== s.ix.line)) {
359
+ let curNode = null;
360
+ list.querySelectorAll('.rpl-l').forEach((n) => { const at = Number(n.dataset.rplLinet); const on = Number(n.dataset.rplLine) === li; n.classList.toggle('on', on); n.classList.toggle('fut', at > s.t); if (on) { curNode = n; n.setAttribute('aria-current', 'true'); } else n.removeAttribute('aria-current'); });
361
+ list.querySelectorAll('.rpl-pm').forEach((n) => n.classList.toggle('fut', Number(n.dataset.rplPmt) > s.t));
362
+ if (!curNode) { // under a filter the current line may be hidden: follow the last one shown that is not in the future
363
+ const shown = [...list.querySelectorAll('.rpl-l, .rpl-pm')].filter((n) => Number(n.dataset.rplLinet != null ? n.dataset.rplLinet : n.dataset.rplPmt) <= s.t);
364
+ curNode = shown[shown.length - 1] || null;
365
+ }
366
+ if (curNode) list.scrollTop = Math.max(0, curNode.offsetTop - list.offsetTop - list.clientHeight / 3);
367
+ s.ix.line = li;
368
+ }
369
+ }
370
+
371
+ /* ── presses ──────────────────────────────────────────────────────────────────────────────────────────────────── */
372
+
373
+ async function confirmAll(s) {
374
+ const d = s.data;
375
+ const todo = d.keys.filter((k) => k.k === 'action' && !s.done.has(k.id));
376
+ if (!todo.length) { toast('Every next step is already a task.'); return; }
377
+ const { makeTask } = await import('./notes-task.js');
378
+ const n = { id: d.note.id, title: d.note.title, initiative: d.note.initiative, ws: d.note.ws };
379
+ for (const k of todo) {
380
+ const id = await makeTask(n, { text: k.text, owner: k.owner, due: k.due });
381
+ if (!id) break; // makeTask has said why (no initiative, no stage); the rest wait
382
+ s.done.add(k.id);
383
+ if (S !== s) return;
384
+ }
385
+ buildKeys(s); paintTime(s, true);
386
+ }
387
+
388
+ function asideSheet(s) {
389
+ const aside = window.NotesAside;
390
+ if (aside && typeof aside.open === 'function') { aside.open(s.data.note.id); return; }
391
+ toast(s.data.aside_count + (s.data.aside_count === 1 ? ' shot' : ' shots') + ' looked like another app and ' + (s.data.aside_count === 1 ? 'is' : 'are') + ' not in the replay. They are deleted after 7 days.');
392
+ }
393
+
394
+ function onClick(ev) {
395
+ if (!S || !S.data) { if (S && ev.target.closest && ev.target.closest('[data-rpl="close"]')) closeReplay(); return; }
396
+ const s = S;
397
+ if (!s.root.contains(ev.target)) return;
398
+ const t = ev.target.closest('[data-rpl-seek],[data-rpl-speed],[data-rpl-filter],[data-rpl]');
399
+ const rail = ev.target.closest('[data-rpl="rail"]');
400
+ if (!t && !rail) return;
401
+ if (t && t.dataset.rplSeek !== undefined) { ev.preventDefault(); seek(s, Number(t.dataset.rplSeek), true); return; }
402
+ if (t && t.dataset.rplSpeed) { s.speed = Number(t.dataset.rplSpeed); if (s.audio) s.audio.playbackRate = s.speed; paintTop(s); return; }
403
+ if (t && t.dataset.rplFilter) { s.filter = t.dataset.rplFilter; buildConvo(s); paintTime(s, true); return; }
404
+ const k = t && t.dataset.rpl;
405
+ if (k === 'play') { if (s.play) pauseNow(s); else { if (s.t >= lenOf(s)) seek(s, 0, false); playNow(s); } paintTop(s); return; }
406
+ if (k === 'back') { seek(s, s.t - BACK_MS, false); return; }
407
+ if (k === 'leave') { const id = s.data.note.id; closeReplay(); window.dispatchEvent(new CustomEvent('c3-open-note', { detail: { id } })); return; }
408
+ if (k === 'keys') { const n = s.root.querySelector('.rpl-cap'); if (n && n.scrollIntoView) n.scrollIntoView({ behavior: 'smooth', block: 'center' }); return; }
409
+ if (k === 'open-shot') { const i = shotIndexAt(s.data.shots, s.t); if (i >= 0) openShot({ id: s.data.note.id, frames: s.data.shots, index: i, seek: (ms) => seek(s, ms, true) }); return; }
410
+ if (k === 'aside') { asideSheet(s); return; }
411
+ if (k === 'confirm') { confirmAll(s); return; }
412
+ // a press on the empty rail scrubs: the time under the pointer
413
+ if (rail) { const b = rail.getBoundingClientRect(); if (b.width > 0) seek(s, (ev.clientX - b.left) / b.width * lenOf(s), true); }
414
+ }
415
+ function onInput(ev) {
416
+ if (!S || !S.data) return;
417
+ const q = ev.target.closest && ev.target.closest('[data-rpl-q]'); if (!q || !S.root.contains(q)) return;
418
+ const s = S; s.q = q.value; const p = q.selectionStart;
419
+ buildConvo(s); paintTime(s, true);
420
+ const n = s.root.querySelector('[data-rpl-q]'); if (n) { n.focus(); try { n.setSelectionRange(p, p); } catch { /* not a text field */ } }
421
+ }
422
+ document.addEventListener('click', onClick);
423
+ document.addEventListener('input', onInput);
424
+
425
+ if (typeof window !== 'undefined') {
426
+ window.C3Replay = { open: openReplay, close: closeReplay,
427
+ /** What the page is doing, for a test and for the entry points: { open, t, playing, speed } */
428
+ state: () => (S ? { open: true, t: S.t, playing: S.play, speed: S.speed, loaded: !!S.data, sound: !!S.audio && !S.soundOff } : { open: false }) };
429
+ }