create-pmos 0.502.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.
- package/package.json +26 -14
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/briefing-premium-calendar-2-test.js +4 -3
- package/workspace/briefing-premium-calendar-3-test.js +2 -3
- package/workspace/changelog.json +25 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +3 -2
- package/workspace/guide/briefing-premium.md +1 -1
- package/workspace/guide/home.md +1 -0
- package/workspace/guide/notes.md +21 -1
- package/workspace/lib/meeting-terms.js +53 -0
- package/workspace/lib/routes-meeting-points.js +7 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-chat-line-test.js +3 -3
- package/workspace/premium-vw-meet-test.js +28 -17
- package/workspace/public/pm/bd-cols.js +5 -2
- package/workspace/public/pm/bd-deliveries.js +3 -3
- package/workspace/public/pm/bd-pin-card.js +3 -3
- package/workspace/public/pm/bd-pins.js +10 -0
- package/workspace/public/pm/bd-span.js +11 -9
- package/workspace/public/pm/cozy-vw-mtg.css +81 -0
- package/workspace/public/pm/home-line.css +2 -2
- package/workspace/public/pm/home-line.js +42 -13
- package/workspace/public/pm/premium-mtg-replay.js +155 -67
- package/workspace/public/pm/premium-vw-mtg.js +119 -25
|
@@ -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,
|
|
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}
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
404
|
-
function
|
|
405
|
-
const
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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()
|
|
749
|
-
case 'nameok': { const i =
|
|
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
|
-
|
|
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,
|
|
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;
|