create-pmos 0.527.0 → 0.529.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 +57 -29
- 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/adler-moments-test.js +4 -3
- package/workspace/briefing-premium-calendar-3-test.js +11 -9
- package/workspace/briefing-premium-calendar-test.js +1 -1
- package/workspace/changelog.json +56 -0
- package/workspace/ev-everywhere-test.js +2 -6
- package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
- package/workspace/guide/home.md +7 -6
- package/workspace/guide/idle-lab.md +1 -1
- package/workspace/guide/notes.md +19 -7
- package/workspace/guide/reports-set-up.md +4 -1
- package/workspace/lib/bg-queue.js +10 -3
- package/workspace/lib/meeting-replay.js +11 -2
- package/workspace/lib/meeting-suggest.js +105 -0
- package/workspace/lib/meeting-terms.js +57 -17
- package/workspace/lib/okr-goals.js +23 -3
- package/workspace/lib/report-chat.js +3 -3
- package/workspace/lib/routes-lab.js +44 -2
- package/workspace/lib/routes-meeting-points.js +27 -3
- package/workspace/lib/routes-morning-goals.js +5 -1
- package/workspace/lib/routes-report-flow.js +35 -5
- package/workspace/news-viewers-test.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-vw-meet-test.js +17 -11
- package/workspace/product-lane-test.js +1 -1
- package/workspace/program-charter-test.js +2 -2
- package/workspace/public/pm/bd-ini-panel.js +1 -1
- package/workspace/public/pm/bd-span.js +10 -7
- package/workspace/public/pm/cozy-board.css +1 -0
- package/workspace/public/pm/cozy-lab-ins.js +13 -4
- package/workspace/public/pm/cozy-search.js +10 -17
- package/workspace/public/pm/cozy-settings.js +8 -2
- package/workspace/public/pm/cozy-today.css +30 -14
- package/workspace/public/pm/cozy-vw-mtg.css +122 -24
- package/workspace/public/pm/good-morning.js +30 -17
- package/workspace/public/pm/home-app.js +11 -1
- package/workspace/public/pm/home-data.js +4 -1
- package/workspace/public/pm/home-day.js +12 -5
- package/workspace/public/pm/premium-board.js +7 -5
- package/workspace/public/pm/premium-initiative-head.js +9 -1
- package/workspace/public/pm/premium-insights.js +5 -3
- package/workspace/public/pm/premium-meeting.js +2 -1
- package/workspace/public/pm/premium-mtg-replay.js +149 -73
- package/workspace/public/pm/premium-report-read.js +11 -4
- package/workspace/public/pm/premium-report-tab.js +33 -4
- package/workspace/public/pm/premium-vw-mtg.js +176 -53
- package/workspace/public/pm/program-model.js +2 -1
- package/workspace/public/pm/report-chat.js +1 -0
- package/workspace/public/pm/report-flow.js +7 -3
- package/workspace/public/premium-board.html +4 -4
- package/workspace/server.js +1 -1
- package/workspace/task-milestones-test.js +2 -2
- package/workspace/timeline-holds-test.js +3 -3
|
@@ -40,7 +40,7 @@ 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, phoneHtml, editHtml, mentions, phraseOf as phraseOfPage, deskHtml, leftHtml, wx,
|
|
43
|
+
import { replayView, phoneHtml, editHtml, mentions, phraseOf as phraseOfPage, deskHtml, leftHtml, wx, placesBy, wordPopHtml, posOf, lineAtPos, captionHtml } 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
|
|
|
@@ -298,7 +298,7 @@ const SV = { link: '<path d="M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1 1M14 10a4
|
|
|
298
298
|
prod: '<path d="M5 20V11M12 20V5M19 20v-6"/>', ticket: '<path d="M4 7h16v4a2 2 0 0 0 0 4v4H4v-4a2 2 0 0 0 0-4z"/>', warn: '<path d="M12 4l9 16H3zM12 10v4M12 17v.01"/>',
|
|
299
299
|
rec: '<circle cx="12" cy="12" r="5"/>', star: '<path d="M12 3.8l2.5 5.2 5.7.8-4.1 4 1 5.7L12 16.8l-5.1 2.7 1-5.7-4.1-4 5.7-.8z"/>',
|
|
300
300
|
img: '<path d="M4 4.5h16v15H4zM9 10a1.6 1.6 0 1 0 0-.01M20 16l-5-5-8 8"/>', up: '<path d="M12 16V4M7 9l5-5 5 5M5 20h14"/>',
|
|
301
|
-
tab: '<path d="M14 4h6v6M20 4l-8 8M18 14v6H4V6h6"/>', present: '<path d="M3 4h18v12H3zM10 8.5l4 2.5-4 2.5zM8 20h8M12 16v4"/>' };
|
|
301
|
+
tab: '<path d="M14 4h6v6M20 4l-8 8M18 14v6H4V6h6"/>', present: '<path d="M3 4h18v12H3zM10 8.5l4 2.5-4 2.5zM8 20h8M12 16v4"/>', undo: '<path d="M9 14L4 9l5-5M4 9h9a6 6 0 0 1 0 12h-3"/>' };
|
|
302
302
|
const svg = (k, w, extra) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24"${extra || ''}>${SV[k]}</svg>`;
|
|
303
303
|
/* bars split by weight (live topics): an SVG, so each length is an attribute and not an inline style */
|
|
304
304
|
function weightBar(cls, parts) {
|
|
@@ -359,7 +359,8 @@ function agendaHtml(P) {
|
|
|
359
359
|
return P.agenda.length ? `<div class="vzm-agl">${P.agenda.map((a, i) => `<div class="vz-ag"><i class="vz-num" aria-hidden="true">${a.n}</i><span>${esc(a.text)}</span><button type="button" class="vz-min vz-hv" data-a="min" data-i="${i}" title="Change the minutes for this topic" aria-label="${esc(a.text)}: ${a.min} minutes. Change">${a.min} min</button></div>`).join('')}</div>`
|
|
360
360
|
: '<p class="vz-empty">The invite lists no topics yet.</p>';
|
|
361
361
|
}
|
|
362
|
-
|
|
362
|
+
/** Tagged rows (What matters, Since the last one, Facts). W: the word fixes, shown in the words (Facts, batch 27 item 12). */
|
|
363
|
+
const tagRows = (rows, W) => rows.map((x) => `<div class="vz-tagrow"><span class="vz-tag" data-k="${x.k}">${esc(x.tag)}</span><span><span class="vz-said">${W ? wx(W, x.text) : esc(x.text)}</span> <small>${esc(x.meta || x.owner || x.src || '')}</small></span></div>`).join('');
|
|
363
364
|
function readyHtml(P) {
|
|
364
365
|
const h = [P.ready.map((r) => `<div class="vz-ready"><span class="vz-mk" data-k="${r.ok ? 'ok' : 'no'}" aria-hidden="true">${r.ok ? svg('check', 12) : '!'}</span><span><b>${esc(r.label)}</b> <em>${esc(r.text)}</em></span></div>`).join('')];
|
|
365
366
|
if (P.purpose) h.push(`<p class="vz-p"><b>What it is for:</b> ${esc(P.purpose)}</p>`);
|
|
@@ -394,32 +395,39 @@ function transcriptHtml(m, ui, lines, seek) {
|
|
|
394
395
|
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>`)
|
|
395
396
|
+ `<p class="vz-note vz-fine">${esc(LIVE_FOOT)}</p>`;
|
|
396
397
|
}
|
|
397
|
-
function stepHtml(a, ui) {
|
|
398
|
+
function stepHtml(a, ui, W) {
|
|
398
399
|
const made = ui.made && ui.made[a.i];
|
|
399
400
|
const act = a.made ? `<span class="vzm-pill" data-k="${a.ticket ? 'you' : ''}">${esc(a.ticket ? a.made : 'Task')}</span>`
|
|
400
401
|
: `<button type="button" class="vzm-btn vzm-soft" data-a="maketask" data-i="${a.i}">Make task</button><button type="button" class="vzm-ib vz-hv" data-a="makewi" data-i="${a.i}" aria-label="Make it a work-item" title="A ticket Drafted in PMOS, shown on the Board. It goes to Jira only through the Commit gate.">${svg('ticket', 15)}</button>`;
|
|
401
|
-
return `<div class="vzm-step"><span class="vzm-grow">${esc(a.text)}</span><span class="vzm-mut vzm-sm">${esc(a.meta.replace('No owner · No date', 'No owner, no date'))}</span>${act}</div>`
|
|
402
|
+
return `<div class="vzm-step"><span class="vzm-grow vz-said">${W ? wx(W, a.text) : esc(a.text)}</span><span class="vzm-mut vzm-sm">${esc(a.meta.replace('No owner · No date', 'No owner, no date'))}</span>${act}</div>`
|
|
402
403
|
+ (made ? `<div class="vzm-made" role="status">${svg('check', 14)}<span>${made.html}</span>${made.undo ? `<button type="button" class="vzm-btn vzm-ghost" data-a="madeundo" data-i="${a.i}">Undo</button>` : ''}</div>` : '');
|
|
403
404
|
}
|
|
404
405
|
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('')
|
|
405
406
|
+ IM.meetingLine(); // Your impact (batch 16 item 7, GmA4Moments): one quiet line under the decisions, only while it is on
|
|
406
|
-
/**
|
|
407
|
+
/** The texts of the notes a word fix reaches, beyond the replay's own (the summary, Next steps, Facts, the transcript). */
|
|
408
|
+
const ownTexts = (m) => ({ summary: m.notes.summary, steps: m.notes.steps.map((x) => x.text), facts: m.notes.facts.map((x) => x.text), lines: m.notes.lines.map((l) => l.text) });
|
|
409
|
+
/** Names and terms (MvN, MpW2): the words fixed here (heard as, the places, Undo; the ✦ ask to remember it for the next meetings), the
|
|
410
|
+
* words fixed in other meetings and remembered, and the people of the invite. */
|
|
407
411
|
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
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
const row = (name, meta, extra = '', cls = '') => `<div class="vzm-trow vzr-trow"><span class="vzm-grow"><b${cls ? ` class="${cls}"` : ''}>${esc(name)}</b>${meta ? ` <span class="vzm-mut vzm-sm">${esc(meta)}</span>` : ''}</span>${extra}</div>`;
|
|
413
|
+
const T = ownTexts(m);
|
|
414
|
+
const fixes = V.W.fix.map((x) => { const n = placesBy(V, T, x.heard).n;
|
|
415
|
+
return row(x.means, 'heard as “' + x.heard + '”' + (n ? ' · ' + (n === 1 ? '1 place' : n + ' places') + ' in this meeting' : '') + (x.keep === 'yes' ? ' · remembered' : ''),
|
|
416
|
+
`<button type="button" class="vzm-ib vzr-ib" data-a="wundo" data-id="${esc(x.id)}" aria-label="Undo this fix" title="Undo this fix">${svg('undo', 16)}</button>`, 'vzr-fxn')
|
|
417
|
+
+ (x.keep === 'ask' ? `<div class="vzr-ask"><span class="vzr-star" aria-hidden="true">✦</span><span class="vzm-grow">Remember “${esc(x.means)}” for your next meetings? Heard as “${esc(x.heard)}” here.</span>`
|
|
418
|
+
+ `<button type="button" class="vzr-a" data-a="wkeep" data-id="${esc(x.id)}" data-v="0" aria-label="Not now" title="Not now">${svg('x', 15)}</button><button type="button" class="vzr-a y" data-a="wkeep" data-id="${esc(x.id)}" data-v="1" aria-label="Remember it" title="Remember it">${svg('check', 15)}</button></div>` : ''); });
|
|
419
|
+
const rows = fixes.concat(V.W.guess.map((x) => row(x.means, 'heard as “' + x.heard + '” · fixed in the meeting on ' + (shortDay(x.at) || 'an earlier day').replace(/^(\w+) (\d+)$/, '$2 $1') + ', remembered')))
|
|
420
|
+
.concat(m.people.map((p) => row(p, 'Invite')));
|
|
421
|
+
return rows.join('');
|
|
414
422
|
}
|
|
415
423
|
const namesCount = (m, V) => m.people.length + V.W.fix.length + V.W.guess.length;
|
|
416
424
|
/** The one strip under the kept points (MvC): the parts this module owns. Left out is the replay module's own. */
|
|
417
425
|
function stripOf(m, ui) {
|
|
418
426
|
const N = m.notes, P = m.prep, L = m.live, V = N.replay;
|
|
419
427
|
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,
|
|
428
|
+
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, V.W)).join('') } : null,
|
|
421
429
|
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,
|
|
430
|
+
N.facts.length ? { k: 'facts', title: 'Facts mentioned', n: String(N.facts.length), html: tagRows(N.facts, V.W) } : null,
|
|
423
431
|
namesCount(m, V) ? { k: 'names', title: 'Names and terms', n: String(namesCount(m, V)), html: namesHtml(m, V) } : null,
|
|
424
432
|
{ 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
433
|
{ 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)}` },
|
|
@@ -450,9 +458,9 @@ export function sectionsOf(m, ui = {}) {
|
|
|
450
458
|
];
|
|
451
459
|
}
|
|
452
460
|
const V = N.replay;
|
|
453
|
-
const steps = N.steps.length ? { id: 'steps', title: 'Next steps', count: N.stepsSub || N.steps.length, open: !V, html: N.steps.map((a) => stepHtml(a, ui)).join('') } : null;
|
|
461
|
+
const steps = N.steps.length ? { id: 'steps', title: 'Next steps', count: N.stepsSub || N.steps.length, open: !V, html: N.steps.map((a) => stepHtml(a, ui, V && V.W)).join('') } : null;
|
|
454
462
|
const since = N.since ? { id: 'since', title: 'Since the last one', count: N.since.sub, html: tagRows(N.since.rows) } : null;
|
|
455
|
-
const facts = N.facts.length ? { id: 'facts', title: 'Facts mentioned', count: N.facts.length, html: tagRows(N.facts) } : null;
|
|
463
|
+
const facts = N.facts.length ? { id: 'facts', title: 'Facts mentioned', count: N.facts.length, html: tagRows(N.facts, V && V.W) } : null;
|
|
456
464
|
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) };
|
|
457
465
|
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)}` };
|
|
458
466
|
if (V) { // the phone: the phone's Replay lists the points; Left out and the rest fold below it
|
|
@@ -477,11 +485,12 @@ export function meetHtml(m, ui = {}) {
|
|
|
477
485
|
for (const g of m.gaps) h.push(`<div class="vzm-warn vzm-sm"><span class="vzm-grow">${g.kind === 'off' ? '<b>Earlier:</b> ' + esc(g.text.replace(/^Earlier: /, '')) : esc(g.text)}</span>${g.kind === 'quiet' ? '<button type="button" class="vzm-btn vzm-card" data-a="src">Listen to another app</button>' : ''}</div>`);
|
|
478
486
|
if (ui.src && m.mk === 'live') h.push(appsHtml(m.live));
|
|
479
487
|
if (ui.why) h.push(`<div class="vz-why" role="note"><p>${esc(RECORD_LINE)}</p><p>${esc(FOCUS_LINE)}</p></div>`);
|
|
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>`);
|
|
488
|
+
if (ui.msg && !ui.msg.toast) 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>`);
|
|
481
489
|
const V = m.mk === 'notes' ? m.notes.replay : null;
|
|
482
490
|
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) }));
|
|
491
|
+
h.push(deskHtml(V, ui, { summary: summaryHtml(m, ui), strip: stripOf(m, ui), hint: !!ui.hint }));
|
|
484
492
|
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>`);
|
|
493
|
+
if (ui.msg && ui.msg.toast) h.push(`<div class="vzr-toastw"><p class="vzr-toast" role="status"><span>${esc(ui.msg.text)}</span>${ui.msg.undo ? '<button type="button" data-a="msgundo">Undo</button>' : ''}</p></div>`);
|
|
485
494
|
return `<div class="vzr-mbonly">${summaryHtml(m, ui)}</div><div class="vz vz-mtg vz-rec" data-mk="${m.mk}">${h.join('')}</div>`;
|
|
486
495
|
}
|
|
487
496
|
h.push(czSections(sectionsOf(m, ui), { key: 'meeting-' + m.mk }));
|
|
@@ -559,7 +568,7 @@ export async function openMeeting(ref, o = {}) {
|
|
|
559
568
|
mtgStyle();
|
|
560
569
|
if (!IM.cached()) IM.readWeek().catch(() => {}); // the meeting moment's switch, read once a page
|
|
561
570
|
const R = refOf(ref);
|
|
562
|
-
const me = { R, ws: o.ws || new URLSearchParams(location.search).get('ws') || '', ui: { sel: new Set(), made: {} }, D: null, n: null, row: null, alive: true, timers: [], play: null };
|
|
571
|
+
const me = { R, ws: o.ws || new URLSearchParams(location.search).get('ws') || '', ui: { sel: new Set(), made: {}, hint: hintOn() }, D: null, n: null, row: null, alive: true, timers: [], play: null };
|
|
563
572
|
if (MT) { MT.alive = false; MT.timers.forEach(clearInterval); stopPlay(MT); }
|
|
564
573
|
MT = me;
|
|
565
574
|
const id = 'meeting:' + (R.note || R.event || R.title || 'new');
|
|
@@ -706,14 +715,17 @@ function paint(me, body) {
|
|
|
706
715
|
const line = body.querySelector('.czs-sum .czs-line > span:not(.czs-dot):not(.czs-r)'); if (line) line.dataset.clk = 'state';
|
|
707
716
|
if (me.ui.edit) { const ta = body.querySelector('.vzp-ed [data-a="etext"]'); if (ta && me.ui.focusEdit) { me.ui.focusEdit = false; ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); } }
|
|
708
717
|
placePop(me, body);
|
|
709
|
-
body
|
|
718
|
+
crop(body);
|
|
719
|
+
hoverBar(me, body);
|
|
710
720
|
body.onclick = (e) => press(me, e);
|
|
711
721
|
body.onchange = (e) => change(me, e);
|
|
712
722
|
// the file picker stays open for seconds while the card may draw again; the input's own handler still runs when it was replaced meanwhile
|
|
713
723
|
body.querySelectorAll('input[type="file"][data-a="matfile"]').forEach((i) => { i.onchange = (e) => { e.stopPropagation(); change(me, e); }; });
|
|
714
724
|
body.oninput = (e) => input(me, e);
|
|
715
725
|
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)
|
|
726
|
+
body.onmouseup = (e) => { if (me.model && me.model.notes.replay && !e.target.closest('.vzr-wpop')) setTimeout(() => { if (MT === me) selectWord(me); }, 0); }; // a word selected to fix (MvF)
|
|
727
|
+
body.ondblclick = (e) => { if (me.model && me.model.notes.replay && !e.target.closest('.vzr-wpop, input, textarea, button')) wordAt(me, e); }; // a word double-clicked (MpW1)
|
|
728
|
+
if (me.ui.wpop) placeWordPop(me, body);
|
|
717
729
|
}
|
|
718
730
|
/** The open popover goes in its anchor, drawn once per paint (its list is redrawn on its own while the PM types). */
|
|
719
731
|
function placePop(me, body) {
|
|
@@ -727,8 +739,10 @@ function keydown(me, e, body) {
|
|
|
727
739
|
if (e.key === 'Enter' && a === 'namein') { e.preventDefault(); nameIt(me, +e.target.dataset.i, e.target.value); }
|
|
728
740
|
if (e.key === 'Enter' && a === 'minin') { e.preventDefault(); setMin(me, +e.target.dataset.i, e.target.value); }
|
|
729
741
|
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-
|
|
731
|
-
if (
|
|
742
|
+
if (a === 'wfin' && e.key === 'Enter') { e.preventDefault(); const f = e.target.closest('.vzr-wpop'); if (f) fixWord(me, f.dataset.w, e.target.value); }
|
|
743
|
+
if (e.key === 'Escape' && me.ui.wpop) { e.preventDefault(); e.stopPropagation(); me.ui.wpop = null; paint(me, body); return; }
|
|
744
|
+
if (e.key === 'Escape' && me.ui.chaps) { e.stopPropagation(); me.ui.chaps = false; paint(me, body); return; }
|
|
745
|
+
if (a === 'psel' && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); selectPoint(me, e.target.dataset.id); return; }
|
|
732
746
|
if (e.key === 'Escape' && me.ui.naming != null) { me.ui.naming = null; paint(me, body); }
|
|
733
747
|
if (e.key === 'Escape' && me.ui.pop) { e.stopPropagation(); me.ui.pop = ''; paint(me, body); }
|
|
734
748
|
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); }
|
|
@@ -750,6 +764,8 @@ function tick(me) {
|
|
|
750
764
|
async function press(me, e) {
|
|
751
765
|
const pa = e.target.closest('[data-czs-act]'); // the summary's one primary
|
|
752
766
|
const t = pa || e.target.closest('[data-a]');
|
|
767
|
+
if (me.ui.wpop && !e.target.closest('.vzr-wpop')) { me.ui.wpop = null; repaint(me); if (!t || t.dataset.a === 'psel') return; } // a press outside the word fix closes it
|
|
768
|
+
if (me.ui.chaps && !e.target.closest('.vzr-chs, [data-a="chaps"]')) { me.ui.chaps = false; if (!t) return repaint(me); }
|
|
753
769
|
if (!t) { if (me.ui.pop && !e.target.closest('.vzm-pop')) { me.ui.pop = ''; repaint(me); } return; }
|
|
754
770
|
if (t.disabled) return;
|
|
755
771
|
const a = pa ? pa.dataset.czsAct : t.dataset.a, ui = me.ui, m = me.model, C = rec();
|
|
@@ -801,13 +817,18 @@ async function press(me, e) {
|
|
|
801
817
|
/* Replay */
|
|
802
818
|
case 'play': return togglePlay(me);
|
|
803
819
|
case 'speed': { const P = ensurePlay(me); const sp = [1, 1.5, 2]; ui.speed = sp[(sp.indexOf(ui.speed || 1) + 1) % sp.length]; if (P && P.aud) P.aud.playbackRate = ui.speed; return repaint(me); }
|
|
820
|
+
case 'skip': { const P = ensurePlay(me); if (P) seek(me, posOf(m.notes.replay, ui) + Number(t.dataset.d || 0), false); return; }
|
|
821
|
+
case 'chprev': case 'chnext': return stepChapter(me, a === 'chnext' ? 1 : -1);
|
|
822
|
+
case 'chgo': ui.chaps = false; return seek(me, +t.dataset.t, true);
|
|
823
|
+
case 'chaps': ui.chaps = !ui.chaps; return repaint(me);
|
|
824
|
+
case 'caps': ui.caps = !ui.caps; return repaint(me);
|
|
804
825
|
case 'seekto': if (t.dataset.t !== '') return seek(me, +t.dataset.t, true); return;
|
|
805
826
|
case 'trseek': { seek(me, +t.dataset.t, true); const c = bodyEl() && bodyEl().querySelector('.vzp-card'); if (c && c.scrollIntoView) c.scrollIntoView({ block: 'nearest' }); return; }
|
|
806
827
|
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); }
|
|
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;
|
|
828
|
+
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; // the whole frame, uncropped
|
|
808
829
|
case 'leftout': ui.leftOpen = !ui.leftOpen; return repaint(me);
|
|
809
830
|
/* 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);
|
|
831
|
+
case 'psel': { const sl = window.getSelection && window.getSelection(); if (sl && !sl.isCollapsed && t.contains(sl.anchorNode)) return; return selectPoint(me, t.dataset.id); } // a selection there is a word to fix, not a press
|
|
811
832
|
case 'pplay': return seek(me, +t.dataset.t, true);
|
|
812
833
|
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
834
|
case 'pwrong': ui.edit = null; ui.draft = null; return verdict(me, [t.dataset.id], 'wrong', 'Moved to Left out.', true);
|
|
@@ -815,9 +836,11 @@ async function press(me, e) {
|
|
|
815
836
|
case 'allshots': ui.allShots = !ui.allShots; return repaint(me);
|
|
816
837
|
case 'sumall': ui.sumAll = !ui.sumAll; return repaint(me);
|
|
817
838
|
/* a word fixed in place (MvF) */
|
|
818
|
-
case 'wfix': return
|
|
819
|
-
case 'wfok': { const f = t.closest('.vzr-
|
|
839
|
+
case 'wfix': return openWordPop(me, t, t.dataset.w, { means: t.dataset.g, from: ['fix'], alts: [] });
|
|
840
|
+
case 'wfok': { const f = t.closest('.vzr-wpop'), i = f && f.querySelector('input'); return f ? fixWord(me, f.dataset.w, i ? i.value : '') : null; }
|
|
841
|
+
case 'walt': { const f = t.closest('.vzr-wpop'), i = f && f.querySelector('input'); if (i) { i.value = t.dataset.v; i.focus(); } if (f) f.querySelectorAll('[data-a="walt"]').forEach((b) => b.setAttribute('aria-pressed', String(b === t))); return; }
|
|
820
842
|
case 'wundo': return forgetWord(me, t.dataset.id);
|
|
843
|
+
case 'wkeep': return keepWord(me, t.dataset.id, t.dataset.v === '1');
|
|
821
844
|
case 'confirmall': return bulk(me, 'confirmed', m.notes.replay.points.filter((p) => !p.verdict).map((p) => p.id));
|
|
822
845
|
case 'unwrong': return verdict(me, [t.dataset.id], null, 'It is back in the kept points.');
|
|
823
846
|
case 'ptask': return bulk(me, 'task', [t.dataset.id]);
|
|
@@ -846,46 +869,142 @@ function selectPoint(me, id) {
|
|
|
846
869
|
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
870
|
repaint(me);
|
|
848
871
|
}
|
|
849
|
-
/* ── a word fixed in place (MvF): the
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
const
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
872
|
+
/* ── a word fixed in place (MvF; batch 27 word fix A, MpW1/MpW2): the popup, the ✦ spelling, the fix, Undo, the ✦ ask to remember it ── */
|
|
873
|
+
/** Where a selection may be a word to fix: every text of the notes the page draws (the kept points, their follow-ups, the caption, the
|
|
874
|
+
* summary, Next steps, Facts, Left out, the chapter names, the transcript). */
|
|
875
|
+
const WORD_ZONES = '.czs-p, .vzp-kt, .vzr-fu .vzr-grow, .vzr-cap, .vzr-nt b, .vzr-chr b, .vz-said, .vzp-lr > span, .vzm-step .vzm-grow';
|
|
876
|
+
const WORD_OK = /^[\p{L}\p{N}][\p{L}\p{N} .'&+-]*$/u;
|
|
877
|
+
const textsFor = (me) => ownTexts(me.model);
|
|
878
|
+
/** The popup for heard words at a node: it opens at once with the places; the ✦ spelling is asked for and shown when it comes. */
|
|
879
|
+
function openWordPop(me, node, heard, sug) {
|
|
880
|
+
const w = String(heard || '').trim(), V = me.model && me.model.notes.replay; if (!w || !me.n || !V) return;
|
|
881
|
+
const r = node && node.getBoundingClientRect ? node.getBoundingClientRect() : null;
|
|
882
|
+
me.ui.wpop = { heard: w, sug: sug && sug.means ? sug : null, P: placesBy(V, textsFor(me), w), rect: r ? { left: r.left, top: r.top, bottom: r.bottom, width: r.width } : null };
|
|
883
|
+
const b = bodyEl(); if (b) placeWordPop(me, b);
|
|
884
|
+
if (!me.ui.wpop.sug) askSpelling(me, w);
|
|
885
|
+
}
|
|
886
|
+
function placeWordPop(me, body) {
|
|
887
|
+
const W = me.ui.wpop; if (!W) return;
|
|
888
|
+
let el = body.querySelector('.vzr-wpop');
|
|
889
|
+
const box = document.createElement('div'); box.innerHTML = wordPopHtml(W.heard, W.sug, W.P);
|
|
890
|
+
const fresh = box.firstElementChild;
|
|
891
|
+
if (el) el.replaceWith(fresh); else body.append(fresh);
|
|
892
|
+
el = fresh;
|
|
893
|
+
const phone = window.matchMedia && window.matchMedia('(max-width: 640px)').matches;
|
|
894
|
+
el.classList.toggle('sheet', !!phone);
|
|
895
|
+
if (!phone && W.rect) { // under the words, its arrow at their start; above them when there is no room below
|
|
896
|
+
const vw = window.innerWidth, vh = window.innerHeight, w = Math.min(380, vw - 24);
|
|
897
|
+
const left = Math.max(12, Math.min(vw - w - 12, W.rect.left - 26));
|
|
898
|
+
el.style.setProperty('width', w + 'px'); el.style.setProperty('left', left + 'px');
|
|
899
|
+
const below = W.rect.bottom + 10, h = el.offsetHeight || 260;
|
|
900
|
+
if (below + h > vh - 12 && W.rect.top - h - 10 > 12) { el.style.setProperty('top', (W.rect.top - h - 10) + 'px'); el.classList.add('up'); } else el.style.setProperty('top', below + 'px');
|
|
901
|
+
el.style.setProperty('--arr', Math.max(16, Math.min(w - 28, W.rect.left - left + 8)) + 'px');
|
|
902
|
+
}
|
|
903
|
+
const i = el.querySelector('input'); if (i && document.activeElement !== i) { i.focus(); i.select(); }
|
|
904
|
+
}
|
|
905
|
+
async function askSpelling(me, heard) {
|
|
906
|
+
const m = me.model, F = me.D && me.D.F, mats = (m && m.material) || [];
|
|
907
|
+
const invite = [m.title].concat((m.prep.agenda || []).map((a) => a.text), (F && Array.isArray(F.topics) ? F.topics.map((t) => t.text) : []), mats.map((x) => x.title)).filter(Boolean).slice(0, 40);
|
|
908
|
+
const qs = new URLSearchParams({ note: me.n.id, heard });
|
|
909
|
+
invite.forEach((t) => qs.append('invite', t)); (m.people || []).slice(0, 40).forEach((p) => qs.append('people', p));
|
|
910
|
+
const r = await pmGet('/api/meeting-points/suggest?' + qs.toString());
|
|
911
|
+
const W = me.ui.wpop; if (MT !== me || !W || W.heard !== heard || !r || !r.ok || !r.suggest) return; // nothing found: no ✦ line (zero is silence)
|
|
912
|
+
const b = bodyEl(), i = b && b.querySelector('.vzr-wpop input');
|
|
913
|
+
if (i && i.value.trim() !== heard) return; // the PM is already typing a fix
|
|
914
|
+
W.sug = r.suggest; if (b) placeWordPop(me, b);
|
|
915
|
+
}
|
|
916
|
+
/** A selection of one to four words in the notes: the words stay marked and the popup opens under them. */
|
|
863
917
|
function selectWord(me) {
|
|
864
918
|
const sel = window.getSelection && window.getSelection(); if (!sel || sel.isCollapsed || !sel.rangeCount || me.ui.edit) return;
|
|
865
919
|
const r = sel.getRangeAt(0), tn = r.startContainer;
|
|
866
|
-
if (tn !== r.endContainer || tn.nodeType !== 3 || !tn.parentElement || !tn.parentElement.closest(
|
|
920
|
+
if (tn !== r.endContainer || tn.nodeType !== 3 || !tn.parentElement || !tn.parentElement.closest(WORD_ZONES) || tn.parentElement.closest('.vzr-wpop')) return;
|
|
921
|
+
markWord(me, r, tn, sel);
|
|
922
|
+
}
|
|
923
|
+
/** A word double-clicked: the word under the pointer, found again after the press drew the card (the double-click's own selection is gone). */
|
|
924
|
+
function wordAt(me, e) {
|
|
925
|
+
let r = null;
|
|
926
|
+
if (document.caretRangeFromPoint) r = document.caretRangeFromPoint(e.clientX, e.clientY);
|
|
927
|
+
else if (document.caretPositionFromPoint) { const c = document.caretPositionFromPoint(e.clientX, e.clientY); if (c) { r = document.createRange(); r.setStart(c.offsetNode, c.offset); } }
|
|
928
|
+
const tn = r && r.startContainer; if (!tn || tn.nodeType !== 3 || !tn.parentElement || !tn.parentElement.closest(WORD_ZONES)) return;
|
|
929
|
+
const t = tn.textContent, i = r.startOffset; let a = i, z = i;
|
|
930
|
+
while (a > 0 && /[\p{L}\p{N}'-]/u.test(t[a - 1])) a--;
|
|
931
|
+
while (z < t.length && /[\p{L}\p{N}'-]/u.test(t[z])) z++;
|
|
932
|
+
if (z <= a) return;
|
|
933
|
+
const rg = document.createRange(); rg.setStart(tn, a); rg.setEnd(tn, z);
|
|
934
|
+
markWord(me, rg, tn, window.getSelection && window.getSelection());
|
|
935
|
+
}
|
|
936
|
+
function markWord(me, r, tn, sel) {
|
|
867
937
|
const raw = r.toString(), w = raw.trim();
|
|
868
|
-
if (!w || w.length > 60 || w.split(/\s+/).length > 4 ||
|
|
938
|
+
if (!w || w.length > 60 || w.split(/\s+/).length > 4 || !WORD_OK.test(w)) return;
|
|
869
939
|
const lead = raw.length - raw.trimStart().length, trail = raw.length - raw.trimEnd().length;
|
|
870
940
|
if (lead) r.setStart(tn, r.startOffset + lead); if (trail) r.setEnd(tn, r.endOffset - trail);
|
|
871
|
-
const mark = document.createElement('span');
|
|
872
|
-
|
|
941
|
+
const mark = document.createElement('span'); mark.className = 'vzr-selw'; r.surroundContents(mark); if (sel) sel.removeAllRanges();
|
|
942
|
+
const fx = mark.closest('.vzr-fx');
|
|
943
|
+
openWordPop(me, mark, fx && fx.dataset.w ? fx.dataset.w : w, null);
|
|
873
944
|
}
|
|
874
945
|
async function fixWord(me, heard, means) {
|
|
875
946
|
const w = String(heard || '').trim(), to = String(means || '').replace(/\s+/g, ' ').trim();
|
|
947
|
+
me.ui.wpop = null;
|
|
876
948
|
if (!w || !to || to === w || !me.n) return repaint(me);
|
|
877
|
-
const places =
|
|
949
|
+
const V = me.model.notes.replay, places = placesBy(V, textsFor(me), w).n;
|
|
878
950
|
const r = await pmPost('/api/meeting-points/term', { note: me.n.id, heard: w, means: to });
|
|
879
951
|
if (!r || !r.ok) return say(me, 'Not fixed: ' + ((r && r.error) || 'no answer'), 'bad');
|
|
880
|
-
me.D.terms = r.terms || me.D.terms;
|
|
881
|
-
|
|
952
|
+
me.D.terms = r.terms || me.D.terms; me.ui.strip = 'names'; // the word lands in Names and terms, with the ✦ ask (MpW2)
|
|
953
|
+
hintSeen(me);
|
|
954
|
+
draw(me);
|
|
955
|
+
toastSay(me, places > 1 ? 'Fixed in ' + places + ' places' : 'Fixed', async () => forgetWord(me, r.term && r.term.id, true));
|
|
882
956
|
}
|
|
883
957
|
async function forgetWord(me, id, undo) {
|
|
884
958
|
if (!id) return;
|
|
885
959
|
const f = await pmPost('/api/meeting-points/forget', { lesson: id });
|
|
886
960
|
if (!f || !f.ok) return say(me, 'Not undone: ' + ((f && f.error) || 'no answer'), 'bad');
|
|
887
961
|
me.D.terms = (me.D.terms || []).filter((x) => x.id !== id); draw(me);
|
|
888
|
-
|
|
962
|
+
toastSay(me, undo ? 'Undone. The words show as they were heard.' : 'The fix is gone. The words show as they were heard.');
|
|
963
|
+
}
|
|
964
|
+
/** The ✦ ask: ✓ remembers the fix for the next meetings, ✕ keeps it in this meeting only. */
|
|
965
|
+
async function keepWord(me, id, yes) {
|
|
966
|
+
const r = await pmPost('/api/meeting-points/keep', { lesson: id, keep: yes });
|
|
967
|
+
if (!r || !r.ok) return say(me, 'Not saved: ' + ((r && r.error) || 'no answer'), 'bad');
|
|
968
|
+
me.D.terms = r.terms || me.D.terms; draw(me);
|
|
969
|
+
toastSay(me, yes ? 'Remembered. Your next meetings spell it “' + (r.term ? r.term.means : '') + '”.' : 'Kept for this meeting only.');
|
|
970
|
+
}
|
|
971
|
+
/** The word fix's message is a toast at the bottom of the card (MpW2), with Undo when there is one. */
|
|
972
|
+
function toastSay(me, text, undo) { me.ui.msg = { text, undo, toast: true }; repaint(me); clearTimeout(me.msgT); me.msgT = setTimeout(() => { if (me.ui.msg && me.ui.msg.text === text) { me.ui.msg = null; repaint(me); } }, 8000); }
|
|
973
|
+
/** The one-time hint under the kept points, "Select a word …": shown until the first fix, then never again on this machine. */
|
|
974
|
+
const HINT_KEY = 'pmos.mtg.wordHint';
|
|
975
|
+
function hintSeen(me) { me.ui.hint = false; try { localStorage.setItem(HINT_KEY, '1'); } catch { /* no storage: it shows again next time */ } }
|
|
976
|
+
const hintOn = () => { try { return localStorage.getItem(HINT_KEY) !== '1'; } catch { return false; } };
|
|
977
|
+
/** A capture Jev boxed shows only its box (MpJ): the image is scaled and moved inside its frame so the box fills it. */
|
|
978
|
+
function crop(body) {
|
|
979
|
+
body.querySelectorAll('img[data-box]').forEach((im) => {
|
|
980
|
+
const [x, y, w, h] = im.dataset.box.split(',').map(Number), [fw, fh] = String(im.dataset.wh || '16,9').split(',').map(Number);
|
|
981
|
+
if (![x, y, w, h].every(Number.isFinite) || !(w > 0) || !(h > 0)) return;
|
|
982
|
+
const fr = im.closest('.vzr-crop'); if (fr) { const ar = String((w * (fw || 16)) / (h * (fh || 9))); fr.classList.add('boxed'); fr.style.setProperty('aspect-ratio', ar); fr.style.setProperty('--ar', ar); }
|
|
983
|
+
im.style.setProperty('width', (100 / w) + '%'); im.style.setProperty('height', (100 / h) + '%');
|
|
984
|
+
im.style.setProperty('left', (-x / w * 100) + '%'); im.style.setProperty('top', (-y / h * 100) + '%');
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
/** Hovering the bar (MpD1): a small preview of the capture there, the chapter's name and the time. */
|
|
988
|
+
function hoverBar(me, body) {
|
|
989
|
+
const bar = body.querySelector('.vzr-np:not(.vzr-ph1) .vzr-bar') || null, V = me.model && me.model.notes.replay; if (!bar || !V) return;
|
|
990
|
+
const hov = bar.parentElement.querySelector('.vzr-hov'); if (!hov) return;
|
|
991
|
+
bar.onmousemove = (e) => {
|
|
992
|
+
const r = bar.getBoundingClientRect(), f = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)), t = f * V.len, ci = V.chapterAt(t), si = V.shotAt(t);
|
|
993
|
+
const st = ci >= 0 ? V.strip[ci] : null, sh = si >= 0 ? V.shots[si] : null;
|
|
994
|
+
hov.querySelector('b').textContent = st ? st.text : ''; hov.querySelector('small').textContent = mmss(t);
|
|
995
|
+
const th = hov.querySelector('.vzr-hth');
|
|
996
|
+
if (sh && V.noteId) { if (th.dataset.t !== String(sh.t)) { th.dataset.t = String(sh.t); th.innerHTML = `<span class="vzr-crop"><img src="/api/notes/screen?id=${encodeURIComponent(V.noteId)}&t=${sh.t}" alt=""${sh.box ? ` data-box="${sh.box.join(',')}" data-wh="${sh.w || 16},${sh.h || 9}"` : ''}></span>`; crop(th); } th.hidden = false; } else th.hidden = true;
|
|
997
|
+
hov.hidden = false;
|
|
998
|
+
const w = hov.offsetWidth || 160; hov.style.setProperty('left', Math.max(0, Math.min(r.width - w, e.clientX - r.left - w / 2)) + 'px');
|
|
999
|
+
};
|
|
1000
|
+
bar.onmouseleave = () => { hov.hidden = true; };
|
|
1001
|
+
}
|
|
1002
|
+
/** ‹ and ›: the start of the chapter before or after; ‹ goes back to the start of this one first when more than 3 s of it played. */
|
|
1003
|
+
function stepChapter(me, d) {
|
|
1004
|
+
const V = me.model && me.model.notes.replay; if (!V || !V.strip.length) return;
|
|
1005
|
+
const pos = posOf(V, me.ui), ci = V.chapterAt(pos);
|
|
1006
|
+
const to = d > 0 ? V.strip[Math.min(V.strip.length - 1, ci + 1)] : pos - V.strip[ci].from > 3000 ? V.strip[ci] : V.strip[Math.max(0, ci - 1)];
|
|
1007
|
+
seek(me, to.from, false);
|
|
889
1008
|
}
|
|
890
1009
|
/** Opens the correction of one kept point where the PM pressed it: "In this part" or the All kept fold. */
|
|
891
1010
|
function startEdit(me, id, where) {
|
|
@@ -1062,11 +1181,15 @@ function ensurePlay(me) {
|
|
|
1062
1181
|
function playUi(me) {
|
|
1063
1182
|
const P = me.play, T = me.model.notes.replay; me.ui.pos = P.pos; me.ui.playing = P.on;
|
|
1064
1183
|
const b = bodyEl(); if (!b) return;
|
|
1065
|
-
const sh = T.shotAt(P.pos);
|
|
1066
|
-
|
|
1067
|
-
|
|
1184
|
+
const sh = T.shotAt(P.pos), ch = T.chapterAt(P.pos);
|
|
1185
|
+
// a new capture, a new chapter or Play/Pause: draw the card again (not while the word fix is open: it would close it)
|
|
1186
|
+
if ((sh !== me.shotNow || ch !== me.chapNow || P.on !== me.wasOn) && !me.ui.wpop) { me.shotNow = sh; me.chapNow = ch; me.wasOn = P.on; me.lineNow = lineAtPos(T, P.pos); return repaint(me); }
|
|
1187
|
+
const at = Math.min(100, P.pos / T.len * 100);
|
|
1188
|
+
b.querySelectorAll('[data-clk="head"]').forEach((h) => h.setAttribute('x', at.toFixed(2) + '%')); // an SVG attribute: where the replay is
|
|
1189
|
+
b.querySelectorAll('[data-clk="prog"]').forEach((r) => r.setAttribute('width', Math.max(0, Math.min(+r.dataset.w, at - +r.dataset.x)).toFixed(2) + '%'));
|
|
1068
1190
|
b.querySelectorAll('[data-clk="pos"]').forEach((p) => { p.textContent = mmss(P.pos); });
|
|
1069
|
-
|
|
1191
|
+
const li = lineAtPos(T, P.pos); // the caption follows the line now, as lyrics do
|
|
1192
|
+
if (li !== me.lineNow) { me.lineNow = li; b.querySelectorAll('[data-pl="cap"]').forEach((c) => { c.innerHTML = captionHtml(T, P.pos); }); }
|
|
1070
1193
|
}
|
|
1071
1194
|
function togglePlay(me) {
|
|
1072
1195
|
const P = ensurePlay(me); if (!P) return;
|
|
@@ -32,7 +32,8 @@ const at = (stage) => { const k = stageKey(stage); return k ? STEPS.findIndex((s
|
|
|
32
32
|
const DELIVER = STEPS.findIndex((s) => s.key === 'deliver');
|
|
33
33
|
const MONITOR = STEPS.findIndex((s) => s.key === 'monitor');
|
|
34
34
|
const dayOf = (iso) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
|
|
35
|
-
|
|
35
|
+
/** "Nov 3", and the short year when the date is not this year ("Jun 23 ’27", workspace grill W4). */
|
|
36
|
+
export const shortDay = (iso, now = new Date()) => { const d = dayOf(iso); return d ? MON[d.getMonth()] + ' ' + d.getDate() + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '') : ''; };
|
|
36
37
|
|
|
37
38
|
const ownWf = (row) => !!(row && row.wf && row.wf.id && row.wf.id !== 'product-feature' && Array.isArray(row.wfStages) && row.wfStage);
|
|
38
39
|
/** The ◆ stage a milestone waits on for one initiative: the charter's pick, else its workflow's first ◆ stage (Product feature: Deliver). */
|
|
@@ -66,6 +66,7 @@ export function cleanBlock(b) {
|
|
|
66
66
|
if (b.schedule !== undefined) { const s = cleanSchedule(b.schedule); if (s) out.schedule = s; }
|
|
67
67
|
for (const k of ['structure', 'design', 'change']) { const v = clip(b[k], 600); if (v) out[k] = v; }
|
|
68
68
|
const secs = cleanSections(b.sections); if (secs) out.sections = secs;
|
|
69
|
+
const sm = out.design ? cleanSample(b.sample) : null; if (sm) out.sample = sm; // the file a look change follows (a deck, a screenshot)
|
|
69
70
|
return out;
|
|
70
71
|
}
|
|
71
72
|
const p = b.proposal && typeof b.proposal === 'object' ? b.proposal : null;
|
|
@@ -37,8 +37,9 @@ export function chatOf(st = {}, id = '') {
|
|
|
37
37
|
intro: 'Tell me what to change in ' + name + '. I pass it to the report\'s own session, and the draft beside us changes. Nothing is issued.',
|
|
38
38
|
placeholder: 'Change the report',
|
|
39
39
|
context: { title: name, subject: 'A report set up in PMOS (report id ' + id + ')' + (settings ? ': ' + settings : ''),
|
|
40
|
-
say: '
|
|
41
|
-
+ '","change":"the change,
|
|
40
|
+
say: 'A change to the draft text (a date, a status, a wording): end with ONE pmos-report block {"kind":"report","id":"' + id
|
|
41
|
+
+ '","change":"the change, complete"}; it passes by itself. A look change (palette, tables, Gantt, a file to follow): {"kind":"report","id":"' + id
|
|
42
|
+
+ '","design":"the look","sample":"<attached path>"}; the next look starts, and when the PM keeps it the draft is redrawn. Say which in one line.',
|
|
42
43
|
facts: [['Report id', id]].concat(draft ? [['Latest draft', draft]] : []), files: draft ? [draft] : [] } };
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -228,7 +229,10 @@ export function waitHtml(F = {}, face = '') {
|
|
|
228
229
|
const d = F.drafting;
|
|
229
230
|
if (d && d.state === 'held') return `<div class="rf-wait">${face}<p><b>The draft waits for a check of ${esc((d.sources || []).join(', '))}.</b><span>It starts by itself when the check passes.</span></p></div>`;
|
|
230
231
|
if (d && d.state === 'ended') return `<div class="rf-wait" data-k="bad">${face}<p><b>The drafting session ended before a draft was written.</b><span>${esc(d.reason || '')}</span></p><button type="button" class="rf-ib pri vz-hv" data-rt="redraft" aria-label="Draft it again" title="Draft it again">${SV(ICON.go)}</button></div>`;
|
|
231
|
-
if (
|
|
232
|
+
if (d && d.state === 'drafting') return `<div class="rf-wait" role="status">${face}<p><b>The report's own session is drafting it.</b><span>It opens here when it lands. Keep talking to Claude in the chat.</span></p></div>`;
|
|
233
|
+
// an open session is not a draft being written (Wawan, 2026-11-04: "drafting it" stayed after the session declined): say what it is doing
|
|
234
|
+
if (F.asking) return `<div class="rf-wait" role="status">${face}<p><b>Your change is with the report's session.</b><span>Its answer shows here when it is done.</span></p></div>`;
|
|
235
|
+
if (F.answer) return `<div class="rf-wait">${face}<p><b>${esc(F.answer.text)}</b><span>No draft is open. Draft it now, or it drafts on its day.</span></p><button type="button" class="rf-ib pri vz-hv" data-rt="redraft" aria-label="Draft it now" title="Draft it now">${SV(ICON.go)}</button></div>`;
|
|
232
236
|
return `<div class="rf-wait">${face}<p><b>No draft yet.</b><span>Draft it now, or it drafts on its day.</span></p><button type="button" class="rf-ib pri vz-hv" data-rt="redraft" aria-label="Draft it now" title="Draft it now">${SV(ICON.go)}</button></div>`;
|
|
233
237
|
}
|
|
234
238
|
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
<span class="bd-title" aria-hidden="true">Board</span>
|
|
32
32
|
<div class="bd-tools">
|
|
33
33
|
<div class="bd-seg bd-views" id="ibTabs" role="group" aria-label="View">
|
|
34
|
-
<button type="button" class="bd-ib" data-v="stages" aria-pressed="false" aria-label="By stage" title="By stage"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h4v16H4zM10 4h4v10h-4zM16 4h4v13h-4z"/></svg></button>
|
|
35
|
-
<button type="button" class="bd-ib" data-v="track" aria-pressed="true" aria-label="Stage track" title="Stage track"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5h18v4H3zM3 11h18v4H3zM3 17h12v3H3z"/></svg></button>
|
|
36
|
-
<button type="button" class="bd-ib" data-v="time" aria-pressed="false" aria-label="Timeline" title="Timeline"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h9M8 12h11M6 18h7"/></svg></button>
|
|
37
|
-
<button type="button" class="bd-ib" data-v="list" aria-pressed="false" aria-label="List" title="List"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/></svg></button>
|
|
34
|
+
<button type="button" class="bd-ib" data-v="stages" aria-pressed="false" aria-label="By stage: a column for each stage" title="By stage: a column for each stage"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h4v16H4zM10 4h4v10h-4zM16 4h4v13h-4z"/></svg></button>
|
|
35
|
+
<button type="button" class="bd-ib" data-v="track" aria-pressed="true" aria-label="Stage track: each initiative on its stages" title="Stage track: each initiative on its stages"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5h18v4H3zM3 11h18v4H3zM3 17h12v3H3z"/></svg></button>
|
|
36
|
+
<button type="button" class="bd-ib" data-v="time" aria-pressed="false" aria-label="Timeline: each initiative on its dates" title="Timeline: each initiative on its dates"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h9M8 12h11M6 18h7"/></svg></button>
|
|
37
|
+
<button type="button" class="bd-ib" data-v="list" aria-pressed="false" aria-label="List: every initiative, one row each" title="List: every initiative, one row each"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/></svg></button>
|
|
38
38
|
</div>
|
|
39
39
|
<!-- the grouping (PgBoard, PgBoardProduct): programs first, or By product; shown only when a program holds an initiative of this board -->
|
|
40
40
|
<span class="bd-grpw2 bd-hide-s" id="bdGrpw" hidden></span>
|
package/workspace/server.js
CHANGED
|
@@ -11098,7 +11098,7 @@ writeTmuxConf(); // must exist before restore may reattach or
|
|
|
11098
11098
|
// premium-session-end: asked only once the restore has FINISHED. The running server restores in slices after it listens, so
|
|
11099
11099
|
// checking right after the call saw no live pane on every launch and spawned one more `shell`, which tmux then kept: one idle
|
|
11100
11100
|
// shell per restart, piling up in the session list.
|
|
11101
|
-
restorePersisted({ progressive: !REQUIRED_AS_LIBRARY, onDone: () => firstShell(sessions.values(), { canHost: HOME_ENABLED || workspaces.size > 0, spawn: () => spawnSession({ name: 'shell', agent: 'shell' }) }) });
|
|
11101
|
+
restorePersisted({ progressive: !REQUIRED_AS_LIBRARY, onDone: () => firstShell(sessions.values(), { canHost: HOME_ENABLED || workspaces.size > 0, spawn: () => spawnSession({ name: 'shell', agent: 'shell', origin: 'first-shell' }) }) });
|
|
11102
11102
|
|
|
11103
11103
|
if (require.main === module) {
|
|
11104
11104
|
// pm-os-absorb Run 2 (R25, X11): one nonce per launch, held in memory only. Never in process.env (so no PTY inherits
|
|
@@ -100,8 +100,8 @@ const ITEMS = [
|
|
|
100
100
|
const { ctx, page: p, errors } = await open();
|
|
101
101
|
const sc = await rowOf(p, 'board:scarlet');
|
|
102
102
|
ok('B one ◇ per milestone: the done Pilot filled, Feature delivered open with "◆ Deliver" in its name; no ◇ for a plain task or one with no due date',
|
|
103
|
-
sc && sc.tm.length === 2 && /tg-tm dn\|Pilot with 2 newsrooms · Dec 11 · done/.test(sc.tm[0]) && /^tg-tm\|Feature delivered · Jan 29 · ◆ Deliver$/.test(sc.tm[1]), JSON.stringify(sc));
|
|
104
|
-
ok('B said once: the Evaluate task IS the end ◇ and takes its name; it is not drawn again', sc && /^tg-end\|KPI evaluated · Mar 31 · ◆ Evaluate$/.test(sc.end) && !sc.tm.some((x) => /KPI/.test(x)), JSON.stringify(sc));
|
|
103
|
+
sc && sc.tm.length === 2 && /tg-tm dn\|Pilot with 2 newsrooms · Dec 11 · done/.test(sc.tm[0]) && /^tg-tm\|Feature delivered · Jan 29(?: ’\d\d)? · ◆ Deliver$/.test(sc.tm[1]), JSON.stringify(sc));
|
|
104
|
+
ok('B said once: the Evaluate task IS the end ◇ and takes its name; it is not drawn again', sc && /^tg-end\|KPI evaluated · Mar 31(?: ’\d\d)? · ◆ Evaluate$/.test(sc.end) && !sc.tm.some((x) => /KPI/.test(x)), JSON.stringify(sc));
|
|
105
105
|
ok('S Year puts no date under a ◇: its hover names it (batch 13 item 4)', sc && !sc.tml.length, JSON.stringify(sc && sc.tml));
|
|
106
106
|
const pk = await rowOf(p, 'board:passkey');
|
|
107
107
|
ok('B a late milestone: an orange outline, and the row\'s dot turns orange', pk && /tg-tm late\|Beta to staff · Oct 16 · late/.test(pk.tm[0]) && !pk.tml.length && /cz-dot late/.test(pk.dot), JSON.stringify(pk));
|
|
@@ -102,7 +102,7 @@ const ROWS = [
|
|
|
102
102
|
ok('A the done part fills the pieces in order and skips the gap: the first piece fills before the second starts', otp && otp.bars[0].done > 0 && (otp.bars[0].done === 100 || otp.bars[1].done === 0), JSON.stringify(otp && otp.bars));
|
|
103
103
|
const rc = await rowOf(p, 'Recovery codes');
|
|
104
104
|
ok('A a plain gap in the dated work breaks the bar with a hairline that is no hold: no label (batch 11 item 13)', rc && rc.bars.length === 2 && !rc.ho.length && !rc.old
|
|
105
|
-
&& rc.ng.length === 1 && rc.ng[0].tag === 'BUTTON' &&
|
|
105
|
+
&& rc.ng.length === 1 && rc.ng[0].tag === 'BUTTON' && /^Nothing planned, Oct 21 to Jan 9(?: ’\d\d)? · 12 wk\. Plan skipped$/.test(rc.ng[0].label) && rc.ng[0].sk, JSON.stringify(rc));
|
|
106
106
|
const pk = await rowOf(p, 'Passkey sign-in');
|
|
107
107
|
ok('A a row with no hold and no gap keeps one whole bar', pk && pk.bars.length === 1 && !/x[lr]/.test(pk.bars[0].cls), JSON.stringify(pk));
|
|
108
108
|
const lg = await p.evaluate(() => (document.querySelector('#bdChipset .tg-lgd') || {}).textContent || '');
|
|
@@ -223,11 +223,11 @@ const ROWS = [
|
|
|
223
223
|
// the press: a future gap's card with its words and + New task with its dates
|
|
224
224
|
await p.click('#ibBody [data-row="board:recovery-codes"] .tg-ng');
|
|
225
225
|
const card = await p.evaluate(() => { const c = document.querySelector('.tg-gpc'); return c ? { words: [...c.querySelectorAll('p')].map((x) => x.textContent), add: (c.querySelector('.gp-add') || {}).getAttribute && c.querySelector('.gp-add').getAttribute('aria-label') } : null; });
|
|
226
|
-
ok('X a future gap opens a card with its words and + New task with the gap\'s dates', card &&
|
|
226
|
+
ok('X a future gap opens a card with its words and + New task with the gap\'s dates', card && /^Nothing planned, Oct 21 to Jan 9(?: ’\d\d)? · 12 wk\|Plan skipped$/.test(card.words.join('|')) && /^New task, Oct 21 to Jan 9(?: ’\d\d)?$/.test(card.add), JSON.stringify(card));
|
|
227
227
|
await shot(p, 'timeline-gap-card-1440');
|
|
228
228
|
await p.click('.tg-gpc .gp-add'); await p.waitForTimeout(400);
|
|
229
229
|
const add = await p.evaluate(() => { const f = document.querySelector('#ivAddTask'); return f ? f.placeholder : null; });
|
|
230
|
-
ok('X + opens the initiative with New task ready, the gap\'s dates named',
|
|
230
|
+
ok('X + opens the initiative with New task ready, the gap\'s dates named', /^A task for Recovery codes, Oct 21 to Jan 9(?: ’\d\d)?$/.test(String(add)), String(add));
|
|
231
231
|
ok('X the gap press threw nothing', !errors.length, errors.join('\n'));
|
|
232
232
|
await ctx.close();
|
|
233
233
|
}
|