create-pmos 0.501.0 → 0.503.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.
@@ -34,13 +34,13 @@
34
34
  * aboutOptions(row | null, W | null, choices | null, notesText) → the What it is about choices (pure)
35
35
  * openMeeting(ref, { ws }) → opens the card. ref: a note id, { note }, { event, date }, a calendar week row or a brief-page today row. */
36
36
  import * as IM from './impact.js'; // Your impact's meeting moment (batch 16 item 7)
37
- import { open as vOpen, current as vCur, back as vBack, czSummary, czSections } from './cozy-viewer.js';
37
+ import { open as vOpen, current as vCur, back as vBack, czSummary, czSections, wide as vWide } from './cozy-viewer.js';
38
38
  import { pmGet, pmPost, pressFetch } from './http.js';
39
39
  import { ownTab, bring, tabUrl } from './rec-tab-core.js';
40
40
  import { noteSheet, aboutChoices, aboutBody, aboutNow } from './premium-vw-meet.js';
41
41
  import { weekView, noteOfRow, sameMeeting, recordable, prevOf, taskEvent, linkBody, mmss, focusView, replayModel, RECORD_LINE, FOCUS_LINE, STOP_LINE, CONSENT_LINE, recCore } from './premium-meeting.js';
42
42
  import { APPS, recall, remember } from './listen-sources.js';
43
- import { replayView, playerHtml, partHtml, keptHtml, phoneHtml, editHtml, mentions, phraseOf as phraseOfPage } from './premium-mtg-replay.js';
43
+ import { replayView, phoneHtml, editHtml, mentions, phraseOf as phraseOfPage, deskHtml, leftHtml, wx, placesOf, viewTexts, wordFieldHtml } from './premium-mtg-replay.js';
44
44
  import { announce } from './rec-state.js';
45
45
  import { presentInRecording } from './cozy-rec-bridge.js'; // while a meeting records, Present goes to the recording tab (batch 12 item 1, rule 10)
46
46
 
@@ -224,7 +224,7 @@ export function meetModel(D, now = Date.now()) {
224
224
  const since = S && S.since ? { sub: S.since.day.replace(/^\w+ /, '') + ' · ' + plural(S.since.items.length, 'step') + ' then, ' + S.since.fresh.length + ' new',
225
225
  rows: S.since.items.map((x) => ({ tag: x.state === 'done' ? 'Done' : x.state === 'again' || x.state === 'raised' ? 'Raised again' : 'Not mentioned', k: x.state === 'done' ? 'ok' : x.state === 'again' || x.state === 'raised' ? 'warn' : '', text: x.text, owner: x.owner }))
226
226
  .concat(S.since.fresh.map((x) => ({ tag: 'New', k: 'new', text: x.text, owner: x.owner }))) } : null;
227
- const replay = rep ? replayView(rep, { n, points: D.points || {}, edits: D.edits || {}, lessons: D.lessons || [], now }) : null;
227
+ const replay = rep ? replayView(rep, { n, points: D.points || {}, edits: D.edits || {}, lessons: D.lessons || [], terms: D.terms || [], now }) : null;
228
228
  const trk = rep ? { len: rep.len } : null;
229
229
  const decided = S ? S.decisions.length : 0, nsteps = steps.length, nq = S ? S.questions.length : 0;
230
230
  /* ⋯ (xvMore order: change it, look back, then stop or remove last) */
@@ -308,12 +308,12 @@ function weightBar(cls, parts) {
308
308
  return `<svg class="${cls}" aria-hidden="true" width="100%" height="100%">${rects}</svg>`;
309
309
  }
310
310
  const fillBar = (f, k) => `<svg class="vzm-fb" aria-hidden="true" width="100%" height="8"><rect width="100%" height="8" rx="4" class="bg"/>${f > 0 ? `<rect width="${(Math.min(1, f) * 100).toFixed(1)}%" height="8" rx="4" class="fg" data-k="${k}"/>` : ''}</svg>`;
311
- const lineHtml = (l, i, ui, seek) => {
311
+ const lineHtml = (l, i, ui, seek, W) => {
312
312
  const naming = ui.naming === i;
313
313
  const who = naming ? `<span class="vz-name"><input data-a="namein" data-i="${i}" aria-label="Name this speaker" value="${esc(l.who)}" maxlength="60"><button type="button" class="vz-link vz-hv" data-a="nameok" data-i="${i}">Save</button></span>`
314
314
  : `<button type="button" class="vz-who vz-hv" data-a="name" data-i="${i}" title="Name this speaker">${esc(l.who)}</button>`;
315
315
  const t = seek && Number.isFinite(l.t) ? `<button type="button" class="vzm-t vz-hv" data-a="trseek" data-t="${l.t}" aria-label="Play from ${esc(mmss(l.t))}">${esc(mmss(l.t))}</button>` : `<time>${esc(mmss(l.t))}</time>`;
316
- return `<div class="vz-line">${t}<span>${who} ${esc(l.text)}</span></div>`;
316
+ return `<div class="vz-line">${t}<span>${who} <span class="vz-said">${W ? wx(W, l.text) : esc(l.text)}</span></span></div>`;
317
317
  };
318
318
  /** A round press that the icon says (Record, Stop, Highlight, Pause, Resume): no words, the name is its label and its tooltip. */
319
319
  const roundBtn = (a, label, icon, tone) => `<button type="button" class="vzm-rb" data-a="${a}" data-tone="${tone || ''}" aria-label="${esc(label)}" title="${esc(label)}">${svg(icon, tone === 'red' ? 22 : 17)}</button>`;
@@ -346,8 +346,10 @@ export function summaryHtml(m, ui = {}) {
346
346
  const N = m.notes, V = N.replay;
347
347
  const text = N.summary || (m.private ? 'This note is private: only what you typed is kept.' : 'No summary was written for this meeting.');
348
348
  if (V) {
349
- const line = (V.raised > V.points.length ? V.points.length + ' of ' + V.raised + ' points kept' : plural(V.points.length, 'point') + ' kept') + (V.toConfirm ? ' · ' + V.toConfirm + ' to confirm' : '');
350
- return czSummary({ line, tone: V.toConfirm ? 'you' : 'ok', text, extra: metaLine([m.sub, serPos(m)]), primary: V.toConfirm ? { label: 'Confirm ' + V.toConfirm, act: 'confirmall' } : null });
349
+ /* MvC: the summary first, two lines and Read it all; Confirm N sits in the kept points' header, so the summary has no primary */
350
+ const long = text.length > 220;
351
+ return czSummary({ text: wx(V.W, text), textHtml: true, extra: `<div class="vzr-sm">${long ? `<button type="button" class="vzr-more2" data-a="sumall" aria-expanded="${!!ui.sumAll}">${ui.sumAll ? 'Show less' : 'Read it all'}</button>` : ''}${metaLine([m.sub, serPos(m)])}</div>` })
352
+ .replace('<div class="czs-sum">', `<div class="czs-sum vzr-sum${ui.sumAll || !long ? ' all' : ''}">`);
351
353
  }
352
354
  const said = [N.decisions.length && plural(N.decisions.length, 'decision'), N.steps.length && plural(N.steps.length, 'next step'), N.questions.length && plural(N.questions.length, 'open question')].filter(Boolean).join(' · ');
353
355
  return czSummary({ line: said || 'Notes', text, extra: metaLine([m.sub, serPos(m)]) });
@@ -388,7 +390,8 @@ function liveAgendaHtml(m, ui) {
388
390
  return change + (Fo.agenda.length ? Fo.agenda.map((a) => `<div class="vzm-ag" data-k="${a.k}"><span>${a.k === 'now' || a.k === 'late' ? `<b>${a.n} ${esc(a.text)}</b>` : esc(a.n + ' ' + a.text)}</span>${fillBar(a.fill, a.k)}<small>${esc(a.say)}</small></div>`).join('') : '<p class="vz-empty">The invite lists no topics yet.</p>');
389
391
  }
390
392
  function transcriptHtml(m, ui, lines, seek) {
391
- return (lines.length ? lines.map((l, i) => lineHtml(l, i, ui, seek)).join('') : `<p class="vz-empty">${m.private ? 'This note is private: only what you type is kept.' : m.mk === 'live' ? 'Nothing has been said yet.' : 'Nothing was transcribed.'}</p>`)
393
+ const W = m.notes && m.notes.replay ? m.notes.replay.W : null; // the word fixes (MvF) show in the transcript too
394
+ return (lines.length ? lines.map((l, i) => lineHtml(l, i, ui, seek, W)).join('') : `<p class="vz-empty">${m.private ? 'This note is private: only what you type is kept.' : m.mk === 'live' ? 'Nothing has been said yet.' : 'Nothing was transcribed.'}</p>`)
392
395
  + `<p class="vz-note vz-fine">${esc(LIVE_FOOT)}</p>`;
393
396
  }
394
397
  function stepHtml(a, ui) {
@@ -400,12 +403,28 @@ function stepHtml(a, ui) {
400
403
  }
401
404
  const decisionsHtml = (N) => N.decisions.map((d) => `<div class="vzm-trow vzm-top"><span class="vzm-grow">${esc(d.text)}</span>${d.why ? `<span class="vzm-mut vzm-sm vzm-why">${esc(d.why)}</span>` : ''}</div>`).join('')
402
405
  + IM.meetingLine(); // Your impact (batch 16 item 7, GmA4Moments): one quiet line under the decisions, only while it is on
403
- /** Replay (XvAReplay): the player and "In this part" in one card; phone: the phone's Replay. The editor shows where the PM pressed. */
404
- function replayCardHtml(V, ui) {
405
- const part = { ...ui, edit: ui.editIn === 'part' ? ui.edit : null };
406
- return `<div class="vzp vzp-card vzp-dt">${playerHtml(V, ui)}${partHtml(V, part)}</div><div class="vzp vzp-card vzp-mb">${phoneHtml(V, ui)}</div>`;
406
+ /** Names and terms (MvN): the people of the invite, and the words fixed in place (here, and in other meetings: the guesses). */
407
+ function namesHtml(m, V) {
408
+ const row = (name, meta, extra = '') => `<div class="vzm-trow"><span class="vzm-grow"><b>${esc(name)}</b>${meta ? ` <span class="vzm-mut vzm-sm">${esc(meta)}</span>` : ''}</span>${extra}</div>`;
409
+ const rows = m.people.map((p) => row(p, 'Invite'))
410
+ .concat(V.W.fix.map((x) => row(x.means, 'heard as “' + x.heard + '” · fixed here', `<button type="button" class="vzm-btn vzm-ghost" data-a="wundo" data-id="${esc(x.id)}">Undo</button>`)))
411
+ .concat(V.W.guess.map((x) => row(x.means, 'heard as “' + x.heard + '” · fixed in another meeting')));
412
+ if (!rows.length) return '';
413
+ return `<p class="vzm-sm vzm-mut">Select a word that was heard wrong in the summary, a quote or the transcript to fix it everywhere. Later notes spell it your way.</p>` + rows.join('');
414
+ }
415
+ const namesCount = (m, V) => m.people.length + V.W.fix.length + V.W.guess.length;
416
+ /** The one strip under the kept points (MvC): the parts this module owns. Left out is the replay module's own. */
417
+ function stripOf(m, ui) {
418
+ const N = m.notes, P = m.prep, L = m.live, V = N.replay;
419
+ return [
420
+ N.steps.length ? { k: 'steps', title: 'Next steps', n: String(N.stepsSub && /proposed/.test(N.stepsSub) ? N.stepsSub : N.steps.length), html: N.steps.map((a) => stepHtml(a, ui)).join('') } : null,
421
+ N.since ? { k: 'since', title: 'Since the last one', n: String(N.since.rows.length), html: tagRows(N.since.rows) } : null,
422
+ N.facts.length ? { k: 'facts', title: 'Facts mentioned', n: String(N.facts.length), html: tagRows(N.facts) } : null,
423
+ namesCount(m, V) ? { k: 'names', title: 'Names and terms', n: String(namesCount(m, V)), html: namesHtml(m, V) } : null,
424
+ { k: 'prep', title: 'Prep and material', n: m.material.length ? String(m.material.length) : '', html: (P.agenda.length ? agendaHtml(P) : '') + materialHtml(m.material, ui) },
425
+ { k: 'tr', title: 'Transcript', n: N.lines.length ? plural(N.lines.length, 'line') : '', html: `<p class="vzm-lbl">Your notes</p>${mineHtml(L)}${L.hl.length ? '<p class="vzm-lbl">Highlights</p>' + hlHtml(L) : ''}<p class="vzm-lbl">${esc(N.trTitle)}</p>${transcriptHtml(m, ui, N.lines, true)}` },
426
+ ];
407
427
  }
408
- const countOf = (V) => [['decision', 'decision'], ['question', 'question'], ['action', 'action']].map(([k, w]) => { const c = V.points.filter((p) => p.kind === k).length; return c ? plural(c, w) : ''; }).filter(Boolean).join(' · ');
409
428
 
410
429
  /** The folding rows of each state (shell A czSections). An empty part is left out (zero is silence). */
411
430
  export function sectionsOf(m, ui = {}) {
@@ -436,9 +455,8 @@ export function sectionsOf(m, ui = {}) {
436
455
  const facts = N.facts.length ? { id: 'facts', title: 'Facts mentioned', count: N.facts.length, html: tagRows(N.facts) } : null;
437
456
  const prep = { id: 'prep', title: 'Prep and material', count: m.material.length ? plural(m.material.length, 'item') : '', html: (P.agenda.length ? agendaHtml(P) : '') + materialHtml(m.material, ui) };
438
457
  const tr = { id: 'tr', title: 'Transcript', count: N.lines.length ? plural(N.lines.length, 'line') : '', html: `<p class="vzm-lbl">Your notes</p>${mineHtml(L)}${L.hl.length ? '<p class="vzm-lbl">Highlights</p>' + hlHtml(L) : ''}<p class="vzm-lbl">${esc(N.trTitle)}</p>${transcriptHtml(m, ui, N.lines, !!V)}` };
439
- if (V) {
440
- const kept = { ...ui, edit: ui.editIn === 'kept' ? ui.edit : null };
441
- return [{ id: 'kept', title: 'All ' + V.points.length + ' kept', count: countOf(V), html: keptHtml(V, kept) }, steps, since, facts, prep, tr];
458
+ if (V) { // the phone: the phone's Replay lists the points; Left out and the rest fold below it
459
+ return [V.left.length ? { id: 'left', title: 'Left out', count: V.left.length, html: leftHtml(V) } : null, steps, since, facts, prep, tr];
442
460
  }
443
461
  return [
444
462
  steps,
@@ -460,7 +478,12 @@ export function meetHtml(m, ui = {}) {
460
478
  if (ui.src && m.mk === 'live') h.push(appsHtml(m.live));
461
479
  if (ui.why) h.push(`<div class="vz-why" role="note"><p>${esc(RECORD_LINE)}</p><p>${esc(FOCUS_LINE)}</p></div>`);
462
480
  if (ui.msg) h.push(`<p class="vz-msg" role="status" data-tone="${ui.msg.tone || ''}"><span>${esc(ui.msg.text)}</span>${ui.msg.undo ? '<button type="button" class="vz-link vz-hv" data-a="msgundo">Undo</button>' : ''}</p>`);
463
- if (m.mk === 'notes' && m.notes.replay) h.push(replayCardHtml(m.notes.replay, ui));
481
+ const V = m.mk === 'notes' ? m.notes.replay : null;
482
+ if (V) { // a recording (MvC): the desk is the two columns and the docked timeline; the phone keeps its Replay and the folds
483
+ h.push(deskHtml(V, ui, { summary: summaryHtml(m, ui), strip: stripOf(m, ui) }));
484
+ h.push(`<div class="vzp vzp-card vzp-mb">${phoneHtml(V, ui)}</div><div class="vzr-mbonly">${czSections(sectionsOf(m, ui), { key: 'meeting-' + m.mk })}</div>`);
485
+ return `<div class="vzr-mbonly">${summaryHtml(m, ui)}</div><div class="vz vz-mtg vz-rec" data-mk="${m.mk}">${h.join('')}</div>`;
486
+ }
464
487
  h.push(czSections(sectionsOf(m, ui), { key: 'meeting-' + m.mk }));
465
488
  return summaryHtml(m, ui) + `<div class="vz vz-mtg" data-mk="${m.mk}">${h.join('')}</div>`;
466
489
  }
@@ -609,7 +632,8 @@ async function load(me, quiet) {
609
632
  people: (row && row.who && row.who.length ? row.who : F && F.ok && F.event && F.event.attendees && F.event.attendees.length ? F.event.attendees : S && S.context ? S.context.people : []),
610
633
  rec: { held, started: held ? C.started() : null }, elapsedBase: nr ? nr.meeting_ms || 0 : 0, frames: one && one.screen ? (one.screen.frames || []).length : 0,
611
634
  canRecord: row ? recordable(row) : !!(n && (n.state === 'idle' || !n.state)), app: recall(safeStorage()).app, steps, points: pts && pts.ok ? pts.points || {} : (me.D && me.D.points) || {},
612
- edits: pts && pts.ok ? pts.edits || {} : (me.D && me.D.edits) || {}, lessons: pts && pts.ok ? pts.lessons || [] : (me.D && me.D.lessons) || [] };
635
+ edits: pts && pts.ok ? pts.edits || {} : (me.D && me.D.edits) || {}, lessons: pts && pts.ok ? pts.lessons || [] : (me.D && me.D.lessons) || [],
636
+ terms: pts && pts.ok ? pts.terms || [] : (me.D && me.D.terms) || [] };
613
637
  draw(me);
614
638
  }
615
639
  async function keyOf(text) {
@@ -634,11 +658,19 @@ function draw(me) {
634
658
  vOpen({ id: me.vid, kind: m.kind, title: m.head, icon: 'meet', url: meetingUrl({ ...refNow(me), ws: me.ws }), render: (b) => paint(me, b), onClose: () => closed(me), more: moreOf(me, m) });
635
659
  widen('mtg');
636
660
  me.reopen = false;
661
+ wideOnce(me, m);
637
662
  return;
638
663
  }
639
664
  if (busy) return; // the PM is typing: the next read draws it
640
665
  paint(me, body);
641
666
  }
667
+ /** A recording (MvC) needs room for its two columns: the Viewer opens wide once per meeting when it is narrower. ⤢ takes it back. */
668
+ function wideOnce(me, m) {
669
+ if (me.wided || !m.notes.replay) return;
670
+ me.wided = true;
671
+ const v = document.getElementById('czViewer');
672
+ if (v && v.offsetWidth && v.offsetWidth < 900) { try { vWide(true); } catch { /* the shell has no wide */ } }
673
+ }
642
674
  const refNow = (me) => (me.n ? { note: me.n.id } : me.row ? { event: me.row.id, date: me.row.day } : me.R);
643
675
  /** ⋯ of the meeting: the model's list, each with its press. */
644
676
  function moreOf(me, m) {
@@ -681,11 +713,12 @@ function paint(me, body) {
681
713
  body.querySelectorAll('input[type="file"][data-a="matfile"]').forEach((i) => { i.onchange = (e) => { e.stopPropagation(); change(me, e); }; });
682
714
  body.oninput = (e) => input(me, e);
683
715
  body.onkeydown = (e) => keydown(me, e, body);
716
+ body.onmouseup = () => { if (me.model && me.model.notes.replay) setTimeout(() => { if (MT === me) selectWord(me); }, 0); }; // a word selected to fix (MvF)
684
717
  }
685
718
  /** The open popover goes in its anchor, drawn once per paint (its list is redrawn on its own while the PM types). */
686
719
  function placePop(me, body) {
687
720
  const P = me.ui.pop; if (P !== 'mat' && P !== 'about') return;
688
- const anc = P === 'mat' ? body.querySelector('[data-pop="mat"]') : body.querySelector('[data-pop="head"]'); if (!anc) return;
721
+ const anc = P === 'mat' ? shown(body, '[data-pop="mat"]') : body.querySelector('[data-pop="head"]'); if (!anc) return;
689
722
  if (P === 'mat') { anc.innerHTML = matPopHtml(me.mat || { row: me.row }); if (!me.matShown && anc.scrollIntoView) { me.matShown = true; anc.scrollIntoView({ block: 'nearest' }); } }
690
723
  else anc.innerHTML = me.about ? aboutPopHtml(me.about, me.aboutQ || '', me.row ? 'Applies to every meeting of “' + me.row.title + '”. The calendar itself is never written.' : 'Applies to this note.') : '<div class="vzm-pop"><p class="vz-empty">Reading what it can be about.</p></div>';
691
724
  }
@@ -694,6 +727,8 @@ function keydown(me, e, body) {
694
727
  if (e.key === 'Enter' && a === 'namein') { e.preventDefault(); nameIt(me, +e.target.dataset.i, e.target.value); }
695
728
  if (e.key === 'Enter' && a === 'minin') { e.preventDefault(); setMin(me, +e.target.dataset.i, e.target.value); }
696
729
  if (e.key === 'Enter' && a === 'matlink') { e.preventDefault(); addLink(me, e.target.value); }
730
+ if (a === 'wfin' && e.key === 'Enter') { e.preventDefault(); const f = e.target.closest('.vzr-fe'); if (f) fixWord(me, f.dataset.w, e.target.value); }
731
+ if (a === 'wfin' && e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); paint(me, body); }
697
732
  if (e.key === 'Escape' && me.ui.naming != null) { me.ui.naming = null; paint(me, body); }
698
733
  if (e.key === 'Escape' && me.ui.pop) { e.stopPropagation(); me.ui.pop = ''; paint(me, body); }
699
734
  if (a === 'seek' && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) { e.preventDefault(); const P = ensurePlay(me); if (P) seek(me, P.pos + (e.key === 'ArrowRight' ? 10000 : -10000), false); }
@@ -745,8 +780,8 @@ async function press(me, e) {
745
780
  ui.ask = null; const r = await pmPost('/api/notes/private', { id: me.n.id, private: true });
746
781
  say(me, r && r.ok ? 'Private. Only what you type is kept.' : 'Not changed: ' + ((r && r.error) || 'no answer'), r && r.ok ? '' : 'bad'); return load(me);
747
782
  }
748
- case 'name': ui.naming = +t.dataset.i; repaint(me); { const i = bodyEl().querySelector('[data-a="namein"]'); if (i) { i.focus(); i.select(); } } return;
749
- case 'nameok': { const i = bodyEl().querySelector('[data-a="namein"]'); return nameIt(me, +t.dataset.i, i ? i.value : ''); }
783
+ case 'name': ui.naming = +t.dataset.i; repaint(me); { const i = shown(bodyEl(), '[data-a="namein"]'); if (i) { i.focus(); i.select(); } } return;
784
+ case 'nameok': { const i = t.closest('.vz-name') && t.closest('.vz-name').querySelector('[data-a="namein"]'); return nameIt(me, +t.dataset.i, i ? i.value : ''); }
750
785
  case 'min': {
751
786
  const row = t.closest('.vz-ag'), a0 = m.prep.agenda[+t.dataset.i]; if (!a0) return;
752
787
  t.outerHTML = `<span class="vz-min"><input type="number" min="0" max="240" step="5" data-a="minin" data-i="${t.dataset.i}" value="${a0.min}" aria-label="Minutes for ${esc(a0.text)}"></span>`;
@@ -770,15 +805,24 @@ async function press(me, e) {
770
805
  case 'trseek': { seek(me, +t.dataset.t, true); const c = bodyEl() && bodyEl().querySelector('.vzp-card'); if (c && c.scrollIntoView) c.scrollIntoView({ block: 'nearest' }); return; }
771
806
  case 'seek': { const r = t.getBoundingClientRect(); return seek(me, Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)) * m.notes.replay.len, false); }
772
807
  case 'shotbig': if (m.notes.replay && m.notes.replay.noteId) window.open('/api/notes/screen?id=' + encodeURIComponent(m.notes.replay.noteId) + '&t=' + t.dataset.t, '_blank', 'noopener'); return;
773
- case 'pf': ui.filter = t.dataset.k; return repaint(me);
774
- case 'tick': { const id = t.dataset.id; if (ui.sel.has(id)) ui.sel.delete(id); else ui.sel.add(id); return repaint(me); }
775
808
  case 'leftout': ui.leftOpen = !ui.leftOpen; return repaint(me);
776
- case 'bulk': return bulk(me, t.dataset.k, [...ui.sel]);
809
+ /* the recording view (MvC): select a point, play it, confirm it, the strip, the filmstrip, the summary */
810
+ case 'psel': return selectPoint(me, t.dataset.id);
811
+ case 'pplay': return seek(me, +t.dataset.t, true);
812
+ case 'pconf': { const ids = String(t.dataset.ids || '').split(',').filter(Boolean), on = t.getAttribute('aria-pressed') === 'true'; return verdict(me, ids, on ? null : 'confirmed', on ? 'No longer confirmed.' : ids.length > 1 ? 'Confirmed, with its follow-up.' : 'Confirmed.'); }
813
+ case 'pwrong': ui.edit = null; ui.draft = null; return verdict(me, [t.dataset.id], 'wrong', 'Moved to Left out.', true);
814
+ case 'strip': ui.strip = ui.strip === t.dataset.k ? '' : t.dataset.k; return repaint(me);
815
+ case 'allshots': ui.allShots = !ui.allShots; return repaint(me);
816
+ case 'sumall': ui.sumAll = !ui.sumAll; return repaint(me);
817
+ /* a word fixed in place (MvF) */
818
+ case 'wfix': return openWordField(me, t, t.dataset.w, t.dataset.g);
819
+ case 'wfok': { const f = t.closest('.vzr-fe'), i = f && f.querySelector('input'); return f ? fixWord(me, f.dataset.w, i ? i.value : '') : null; }
820
+ case 'wundo': return forgetWord(me, t.dataset.id);
777
821
  case 'confirmall': return bulk(me, 'confirmed', m.notes.replay.points.filter((p) => !p.verdict).map((p) => p.id));
778
822
  case 'unwrong': return verdict(me, [t.dataset.id], null, 'It is back in the kept points.');
779
823
  case 'ptask': return bulk(me, 'task', [t.dataset.id]);
780
824
  /* a correction (XvAReplayEdit) */
781
- case 'pedit': return startEdit(me, t.dataset.id, t.closest('.czs-body') ? 'kept' : 'part');
825
+ case 'pedit': return startEdit(me, t.dataset.id, 'ev');
782
826
  case 'ekind': ui.draft = { ...ui.draft, kind: t.dataset.k, text: draftText() }; return repaint(me);
783
827
  case 'elearn': ui.draft = { ...ui.draft, learn: ui.draft.learn === false, text: draftText() }; return repaint(me);
784
828
  case 'ecancel': ui.edit = null; ui.draft = null; return repaint(me);
@@ -793,6 +837,56 @@ async function press(me, e) {
793
837
  }
794
838
  }
795
839
  const draftText = () => { const ta = bodyEl() && bodyEl().querySelector('.vzp-ed [data-a="etext"]'); return ta ? ta.value : undefined; };
840
+ /** The first match the PM can see (the desk and the phone both draw some parts; the stylesheet shows one). */
841
+ const shown = (root, sel) => (root ? [...root.querySelectorAll(sel)].find((x) => x.offsetParent !== null) || root.querySelector(sel) : null);
842
+ /** Press a kept point (MvC): it is the selected one, and the replay moves to its time (it does not play). */
843
+ function selectPoint(me, id) {
844
+ const V = me.model && me.model.notes.replay, p = V && V.points.find((x) => x.id === id); if (!p) return;
845
+ const ui = me.ui; ui.pt = id; if (ui.edit && ui.edit !== id) { ui.edit = null; ui.draft = null; }
846
+ if (p.t != null) { const P = ensurePlay(me); if (P) { P.pos = Math.max(0, Math.min(V.len, p.t)); if (P.aud) { try { P.aud.currentTime = P.pos / 1000; } catch { /* not loaded */ } } ui.pos = P.pos; me.shotNow = V.shotAt(P.pos); } }
847
+ repaint(me);
848
+ }
849
+ /* ── a word fixed in place (MvF): the texts it reaches, the field, the fix, Undo ── */
850
+ function wordTexts(me) {
851
+ const m = me.model, V = m && m.notes.replay; if (!V) return [];
852
+ return viewTexts(V).concat([m.notes.summary], m.notes.lines.map((l) => l.text));
853
+ }
854
+ function openWordField(me, node, heard, guess) {
855
+ const w = String(heard || '').trim(); if (!w || !me.n) return;
856
+ const n = placesOf(wordTexts(me), w);
857
+ const box = document.createElement('span'); box.innerHTML = wordFieldHtml(w, guess || w, n);
858
+ const f = box.firstElementChild;
859
+ if (node.nodeType === 1) node.replaceWith(f); else return;
860
+ const i = f.querySelector('input'); if (i) { i.focus(); i.select(); }
861
+ }
862
+ /** A selection in the summary, a quote, Left out or the transcript (one to four words): it becomes the field. */
863
+ function selectWord(me) {
864
+ const sel = window.getSelection && window.getSelection(); if (!sel || sel.isCollapsed || !sel.rangeCount || me.ui.edit) return;
865
+ const r = sel.getRangeAt(0), tn = r.startContainer;
866
+ if (tn !== r.endContainer || tn.nodeType !== 3 || !tn.parentElement || !tn.parentElement.closest('.czs-p, .vzr-q, .vz-said, .vzp-lr > span')) return;
867
+ const raw = r.toString(), w = raw.trim();
868
+ if (!w || w.length > 60 || w.split(/\s+/).length > 4 || !/^[\p{L}\p{N}][\p{L}\p{N} .'&+-]*$/u.test(w)) return;
869
+ const lead = raw.length - raw.trimStart().length, trail = raw.length - raw.trimEnd().length;
870
+ if (lead) r.setStart(tn, r.startOffset + lead); if (trail) r.setEnd(tn, r.endOffset - trail);
871
+ const mark = document.createElement('span'); r.deleteContents(); r.insertNode(mark); sel.removeAllRanges();
872
+ openWordField(me, mark, w, w);
873
+ }
874
+ async function fixWord(me, heard, means) {
875
+ const w = String(heard || '').trim(), to = String(means || '').replace(/\s+/g, ' ').trim();
876
+ if (!w || !to || to === w || !me.n) return repaint(me);
877
+ const places = placesOf(wordTexts(me), w);
878
+ const r = await pmPost('/api/meeting-points/term', { note: me.n.id, heard: w, means: to });
879
+ if (!r || !r.ok) return say(me, 'Not fixed: ' + ((r && r.error) || 'no answer'), 'bad');
880
+ me.D.terms = r.terms || me.D.terms; draw(me);
881
+ say(me, (places > 1 ? 'Fixed in ' + places + ' places' : 'Fixed') + '. Later notes spell it “' + to + '”.', '', async () => forgetWord(me, r.term && r.term.id, true));
882
+ }
883
+ async function forgetWord(me, id, undo) {
884
+ if (!id) return;
885
+ const f = await pmPost('/api/meeting-points/forget', { lesson: id });
886
+ if (!f || !f.ok) return say(me, 'Not undone: ' + ((f && f.error) || 'no answer'), 'bad');
887
+ me.D.terms = (me.D.terms || []).filter((x) => x.id !== id); draw(me);
888
+ say(me, undo ? 'Undone. The word shows as it was heard.' : 'The fix is gone. The word shows as it was heard.');
889
+ }
796
890
  /** Opens the correction of one kept point where the PM pressed it: "In this part" or the All kept fold. */
797
891
  function startEdit(me, id, where) {
798
892
  const V = me.model && me.model.notes.replay, p = V && V.points.find((x) => x.id === id); if (!p) return;