create-pmos 0.116.0 → 0.118.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 +1 -1
- 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/guide/notes.md +43 -3
- package/workspace/guide/today.md +22 -13
- package/workspace/lib/brief.js +2 -2
- package/workspace/lib/day-marks.js +29 -9
- package/workspace/lib/goals.js +68 -10
- package/workspace/lib/notes.js +139 -17
- package/workspace/lib/routes-notes.js +68 -11
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +289 -91
- package/workspace/public/pm/dest-notes.js +136 -29
- package/workspace/public/pm/note-dock.js +204 -164
- package/workspace/public/pm/notes-context.js +60 -0
- package/workspace/public/pm/notes-links.js +32 -6
- package/workspace/public/pm/primitives.js +1 -1
- package/workspace/public/pm/today-agenda.js +27 -53
- package/workspace/public/pm/today-copy.js +34 -15
- package/workspace/public/pm/today-day.js +98 -76
- package/workspace/public/pm/today-goals.js +159 -15
- package/workspace/public/pm/today-jobs.js +84 -0
- package/workspace/public/pm/today-mentions.js +50 -0
- package/workspace/public/pm/topbar.js +71 -32
- package/workspace/public/pm/view-notes.js +142 -73
- package/workspace/server.js +10 -8
- package/workspace/test-files.json +4 -0
- package/workspace/test-lists/today-goals-jobs-ui.json +5 -0
- package/workspace/test-lists/today-goals-jobs.json +5 -0
- package/workspace/test-lists/today-mentions-cards.json +5 -0
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* Layout, as the mockup: the page title and a search field, then two cards side by side.
|
|
9
9
|
* left BY INITIATIVE · n notes. One group per initiative, then "No initiative"; each row is the meeting time or day,
|
|
10
10
|
* the title, and an amber dot when a statement in the note differs from a file in the repository.
|
|
11
|
-
* right the note
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* right THE FULL NOTE (meeting-notes Run 13: the pop-up note is gone). One meta line (Meeting note · day time · length ·
|
|
12
|
+
* initiative) with an icon that shows the exact times and dates, the title with its icons (Resume and Stop, or Stop
|
|
13
|
+
* while recording; Copy as Markdown; ⋯), the Meeting context card (people, agenda, what it is for, details), the live
|
|
14
|
+
* block while the meeting is recording or paused (status, Your notes, the live transcript), then only what is there:
|
|
15
|
+
* Conflicts with other files, Highlights, the Overview, Decisions, Next steps (owner · due, and Make task), Open
|
|
16
|
+
* questions, and the discussion and transcript folded at the end. Every button is an icon with a tooltip.
|
|
14
17
|
*
|
|
15
18
|
* WHERE EACH PART COMES FROM. After Stop and the final pass, the agent's final notes (note.final_notes: grounded in quotes);
|
|
16
19
|
* before that, what was caught live (view-notes.js caughtItems: the agent's minute bullets, else the labelled lines) and
|
|
@@ -20,9 +23,11 @@
|
|
|
20
23
|
* to the note's initiative as a plan item in its first stage (POST /api/plan-items, the Breakdown's own writer), with Undo.
|
|
21
24
|
*/
|
|
22
25
|
|
|
23
|
-
import { el } from './dom.js';
|
|
26
|
+
import { el, patchChildren } from './dom.js';
|
|
24
27
|
import { button, iconButton, field, row, menu, toast, empty, ensureComponentsCss } from './primitives.js';
|
|
25
|
-
import {
|
|
28
|
+
import { onNotes } from './notes-links.js';
|
|
29
|
+
import { agendaHeard, peopleOf } from './notes-context.js';
|
|
30
|
+
import { ensureSprite, icon } from './icons.js';
|
|
26
31
|
import { pmGet, pmPost } from './http.js';
|
|
27
32
|
import { registerCommand, openDestination } from './registry.js';
|
|
28
33
|
import { askConfirm } from './dialogs.js';
|
|
@@ -43,10 +48,14 @@ export const COPY = Object.freeze({
|
|
|
43
48
|
reading: 'Reading…',
|
|
44
49
|
readError: 'The notes could not be read.',
|
|
45
50
|
meetingNote: 'Meeting note',
|
|
46
|
-
record: 'Record', resume: 'Resume', stop: 'Stop',
|
|
51
|
+
record: 'Record this note', resume: 'Resume', stop: 'Stop for good (cannot be resumed)', copy: 'Copy as Markdown (with the meeting context)', timeInfo: 'Exact times and dates',
|
|
47
52
|
conflicts: 'Conflicts with other files',
|
|
48
53
|
conflictWith: 'conflicts with',
|
|
49
54
|
openFile: 'Open file',
|
|
55
|
+
linkMeeting: 'Link to a meeting', linkInitiative: 'Link to an initiative',
|
|
56
|
+
context: 'Meeting context', people: 'People', agenda: 'Agenda', about: 'What it is for', details: 'Details',
|
|
57
|
+
noMeeting: 'Not linked to a meeting. Link it to add the meeting’s people, agenda and details to the note.',
|
|
58
|
+
noContext: 'The calendar gave no more about this meeting.',
|
|
50
59
|
highlights: 'Highlights',
|
|
51
60
|
overview: 'Overview',
|
|
52
61
|
noOverview: 'No overview yet. It is written when you stop recording.',
|
|
@@ -58,7 +67,6 @@ export const COPY = Object.freeze({
|
|
|
58
67
|
undo: 'Undo',
|
|
59
68
|
discussion: 'Discussion',
|
|
60
69
|
transcript: (n) => `${n} transcript ${n === 1 ? 'line' : 'lines'}`,
|
|
61
|
-
openFull: 'Open the full note',
|
|
62
70
|
del: 'Delete note',
|
|
63
71
|
delAsk: (t) => `Delete “${t}”?`,
|
|
64
72
|
delBody: 'The note, its transcript and its audio are deleted from this machine.',
|
|
@@ -174,9 +182,10 @@ export function mountDestination(host, ctx) {
|
|
|
174
182
|
inst.head = el('div', { class: 'rp-head nt-pg-head' }, el('h1', { class: 'rp-head__title' }, COPY.title), find);
|
|
175
183
|
inst.root.replaceChildren(inst.head, el('div', { class: 'nt-pg-grid' }, inst.left, inst.right));
|
|
176
184
|
}
|
|
185
|
+
if (!inst.unsub) inst.unsub = onNotes(() => { if (insts.get(host) === inst) refresh(inst); });
|
|
177
186
|
inst.repo = (ctx && ctx.tab && ctx.tab.repo) || (ctx && ctx.state && ctx.state.product) || null;
|
|
178
187
|
const slug = (ctx && ctx.tab && ctx.tab.slug) || null;
|
|
179
|
-
if (slug !== inst.slug) { inst.slug = slug; if (slug) { inst.open = slug; inst.note = null; } }
|
|
188
|
+
if (slug !== inst.slug) { inst.slug = slug; if (slug) { inst.open = slug; inst.note = null; inst.ctxOpen = null; inst.info = false; loadOpen(inst); } }
|
|
180
189
|
if (!inst.rows || Date.now() - inst.readAt > REREAD_MS) refresh(inst);
|
|
181
190
|
drawLeft(inst);
|
|
182
191
|
drawRight(inst);
|
|
@@ -186,6 +195,7 @@ export function disposeDestination(host) {
|
|
|
186
195
|
const inst = insts.get(host);
|
|
187
196
|
if (!inst) return;
|
|
188
197
|
inst.seq++;
|
|
198
|
+
if (inst.unsub) inst.unsub();
|
|
189
199
|
inst.root.remove();
|
|
190
200
|
insts.delete(host);
|
|
191
201
|
}
|
|
@@ -211,7 +221,7 @@ async function loadOpen(inst) {
|
|
|
211
221
|
}
|
|
212
222
|
function choose(inst, id) {
|
|
213
223
|
if (inst.open === id) return;
|
|
214
|
-
inst.open = id; inst.note = null;
|
|
224
|
+
inst.open = id; inst.note = null; inst.ctxOpen = null; inst.info = false;
|
|
215
225
|
drawLeft(inst, true); drawRight(inst);
|
|
216
226
|
loadOpen(inst);
|
|
217
227
|
}
|
|
@@ -238,7 +248,7 @@ function drawLeft(inst, force) {
|
|
|
238
248
|
trail: r.conflicts ? [el('span', { class: 'nt-pg-warn-dot', 'data-ntm-conflict-dot': '', title: r.conflicts + (r.conflicts === 1 ? ' conflict' : ' conflicts') })] : null,
|
|
239
249
|
onOpen: () => choose(inst, r.id), openLabel: 'Open ' + (r.title || 'Untitled note'),
|
|
240
250
|
openAttrs: { 'aria-current': r.id === inst.open ? 'true' : 'false' },
|
|
241
|
-
attrs: { class: 'nt-pg-row', 'data-ntm-row': r.id } })))));
|
|
251
|
+
attrs: { class: 'nt-pg-row', 'data-ntm-row': r.id, title: 'Started ' + fullAt((r.sessions && r.sessions[0] && r.sessions[0].from) || startOf(r)) } })))));
|
|
242
252
|
}
|
|
243
253
|
inst.left.replaceChildren(head, body);
|
|
244
254
|
}
|
|
@@ -248,42 +258,134 @@ function drawLeft(inst, force) {
|
|
|
248
258
|
function drawRight(inst, force) {
|
|
249
259
|
const n = inst.note;
|
|
250
260
|
const live = dock.recordState();
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
// a
|
|
254
|
-
if (
|
|
261
|
+
const held = !!n && live.mode !== 'idle' && live.id === n.id;
|
|
262
|
+
const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
|
|
263
|
+
// a held note draws again on every render: the clock, the lines and the paused minutes move. Nothing else draws without a change.
|
|
264
|
+
if (!force && !held && sig === inst.rightSig) return;
|
|
265
|
+
// a field being typed in is not redrawn under the cursor, except in the live block, whose fields are kept across redraws
|
|
266
|
+
const active = typeof document !== 'undefined' ? document.activeElement : null;
|
|
267
|
+
if (active && inst.right.contains(active) && active.matches('input, textarea, [contenteditable="true"]') && !active.closest('[data-nt-part="live"]')) return;
|
|
255
268
|
inst.rightSig = sig;
|
|
256
|
-
|
|
257
|
-
if (!
|
|
258
|
-
|
|
269
|
+
const fresh = el('div');
|
|
270
|
+
if (!inst.open) fresh.appendChild(empty({ text: inst.rows && inst.rows.length ? COPY.pick : COPY.none }));
|
|
271
|
+
else if (!n) fresh.appendChild(el('p', { class: 'nt-hint' }, COPY.reading));
|
|
272
|
+
else for (const part of noteParts(inst, n, live)) fresh.appendChild(part);
|
|
273
|
+
// a live transcript read to its end follows the new lines; one the PM scrolled up in stays where they left it
|
|
274
|
+
const was = inst.right.querySelector('[data-nt-transcript]');
|
|
275
|
+
const atEnd = !!was && was.scrollTop + was.clientHeight >= was.scrollHeight - 8;
|
|
276
|
+
patchChildren(inst.right, fresh);
|
|
277
|
+
const box = inst.right.querySelector('[data-nt-transcript]');
|
|
278
|
+
if (atEnd && box && box.isConnected) box.scrollTop = box.scrollHeight;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const pad2 = (v) => String(v).padStart(2, '0');
|
|
282
|
+
const fullAt = (t) => { const d = new Date(t); return Number.isFinite(d.getTime()) ? d.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' }) + ', ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds()) : ''; };
|
|
283
|
+
const hmAt = (t) => { const d = new Date(t); return Number.isFinite(d.getTime()) ? pad2(d.getHours()) + ':' + pad2(d.getMinutes()) : ''; };
|
|
284
|
+
const TZ = (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { return ''; } })();
|
|
285
|
+
|
|
286
|
+
/** The exact times and dates of a note (R56): the calendar event, each recording stretch, the total, the status, the resume deadline. */
|
|
287
|
+
function infoPanel(n) {
|
|
288
|
+
const rows = [];
|
|
289
|
+
const m = n.meeting && n.meeting.kind === 'event' ? n.meeting : null;
|
|
290
|
+
if (m && m.start) rows.push(['Calendar event', fullAt(m.start).replace(/:\d\d$/, '') + (m.end ? '–' + hmAt(m.end) : '')]);
|
|
291
|
+
(n.sessions || []).forEach((x, i) => rows.push(['Recording ' + (i + 1), fullAt(x.from) + (x.to ? ' → ' + fullAt(x.to) : ' → now')]));
|
|
292
|
+
const total = (n.sessions || []).reduce((a, x) => a + Math.max(0, (x.to ? Date.parse(x.to) : Date.now()) - Date.parse(x.from)), 0);
|
|
293
|
+
if (total) rows.push(['Total recorded', V.fmtTime(total)]);
|
|
294
|
+
rows.push(['Status', ({ recording: 'Recording', paused: 'Paused', ended: 'Ended', idle: 'Not recorded' })[n.state] || '']);
|
|
295
|
+
if (n.state === 'paused' && n.resume_until) rows.push(['Can be resumed', 'yes, until ' + fullAt(n.resume_until)]);
|
|
296
|
+
else if (n.state === 'ended') rows.push(['Can be resumed', 'no: ' + (n.ended_by === 'stop' ? 'stopped for good' : 'paused over an hour, so it ended') + (n.ended_at ? ' at ' + fullAt(n.ended_at) : '')]);
|
|
297
|
+
if (TZ) rows.push(['Time zone', TZ]);
|
|
298
|
+
return el('dl', { class: 'nt-pg-info', 'data-ntm': 'info' }, rows.map(([k, v]) => [el('dt', null, k), el('dd', null, v)]));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The Meeting context card (R59): what the note kept of its meeting. Only what the calendar and the brief gave; nothing is made up. */
|
|
302
|
+
function contextCard(inst, n, held) {
|
|
303
|
+
const m = n.meeting && n.meeting.kind === 'event' ? n.meeting : null;
|
|
304
|
+
if (!m) {
|
|
305
|
+
return el('section', { class: 'nt-pg-ctx', 'data-ntm': 'context', 'data-ntm-context': 'none' },
|
|
306
|
+
el('div', { class: 'nt-pg-ctx__h' }, el('h3', { class: 'nt-pg-sec__h' }, COPY.context), el('span', { class: 'nt-pg-ctx__sum' }, COPY.noMeeting),
|
|
307
|
+
iconButton({ icon: 'link', ariaLabel: COPY.linkMeeting, onClick: () => V.linkMeeting({ state: inst.ctx.state, setState: inst.ctx.setState }, n.id), attrs: { 'data-ntm-link-meeting': '' } })));
|
|
308
|
+
}
|
|
309
|
+
const c = m.context || null;
|
|
310
|
+
const lines = (n.final && n.final.segments && n.final.segments.length ? n.final.segments : (n.transcript || []).filter((x) => !x.dropped)).map((x) => ({ t: x.t, text: x.text }));
|
|
311
|
+
const heard = c ? agendaHeard(c, lines) : [];
|
|
312
|
+
const names = peopleOf(c);
|
|
313
|
+
const open = inst.ctxOpen == null ? (held || !(n.transcript || []).length) : inst.ctxOpen;
|
|
314
|
+
const sum = [names.length ? names.length + (names.length === 1 ? ' person' : ' people') : c && c.attendees ? c.attendees + ' attendees' : null,
|
|
315
|
+
c && c.agenda && c.agenda.length ? heard.filter((x) => x !== null).length + ' of ' + c.agenda.length + ' agenda items heard' : null].filter(Boolean).join(' · ') || m.title;
|
|
316
|
+
const head = el('div', { class: 'nt-pg-ctx__h' }, el('h3', { class: 'nt-pg-sec__h' }, COPY.context), el('span', { class: 'nt-pg-ctx__sum' }, sum),
|
|
317
|
+
iconButton({ icon: 'chevron-down', ariaLabel: open ? 'Hide the meeting context' : 'Show the meeting context', expanded: open,
|
|
318
|
+
onClick: () => { inst.ctxOpen = !open; drawRight(inst, true); }, attrs: { class: 'nt-pg-ctx__toggle' + (open ? ' nt-pg-ctx__toggle--open' : ''), 'data-ntm-context-toggle': '' } }));
|
|
319
|
+
if (!open) return el('section', { class: 'nt-pg-ctx', 'data-ntm': 'context', 'data-ntm-context': 'folded' }, head);
|
|
320
|
+
const cols = [];
|
|
321
|
+
if (names.length) cols.push(el('div', { class: 'nt-pg-ctx__col', 'data-ntm-ctx': 'people' }, el('h4', null, COPY.people),
|
|
322
|
+
el('ul', null, names.map((x) => el('li', null, el('span', { class: 'nt-pg-av', 'aria-hidden': 'true' }, x.split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase()), el('span', null, x))))));
|
|
323
|
+
else if (c && c.attendees) cols.push(el('div', { class: 'nt-pg-ctx__col', 'data-ntm-ctx': 'people' }, el('h4', null, COPY.people), el('p', null, c.attendees + ' attendees. Names are not kept while calendar names are off.')));
|
|
324
|
+
if (c && c.agenda && c.agenda.length) cols.push(el('div', { class: 'nt-pg-ctx__col', 'data-ntm-ctx': 'agenda' }, el('h4', null, COPY.agenda),
|
|
325
|
+
el('ul', null, c.agenda.map((a, i) => el('li', { class: heard[i] !== null ? 'nt-pg-ag nt-pg-ag--heard' : 'nt-pg-ag', 'data-ntm-agenda': heard[i] !== null ? 'heard' : 'open' },
|
|
326
|
+
el('span', { class: 'nt-pg-ag__mk', 'aria-hidden': 'true' }, heard[i] !== null ? icon('check') : null), el('span', null, a),
|
|
327
|
+
heard[i] !== null ? el('span', { class: 'nt-pg-li__at' }, V.fmtTime(heard[i])) : null)))));
|
|
328
|
+
const about = [];
|
|
329
|
+
if (c && c.purpose) about.push(el('p', null, c.purpose));
|
|
330
|
+
if (c && c.prep) about.push(el('p', { class: 'nt-pg-dim' }, 'To be ready: ' + c.prep));
|
|
331
|
+
const tags = [];
|
|
332
|
+
if (n.initiative) tags.push(el('span', { class: 'nt-pg-chip' }, 'Initiative · ' + iniName(n.initiative)));
|
|
333
|
+
for (const r of (c && c.refs) || []) tags.push(r.link ? el('a', { class: 'nt-pg-chip', href: r.link, target: '_blank', rel: 'noopener noreferrer' }, r.label) : el('span', { class: 'nt-pg-chip' }, r.label));
|
|
334
|
+
if (about.length || tags.length) cols.push(el('div', { class: 'nt-pg-ctx__col', 'data-ntm-ctx': 'about' }, el('h4', null, COPY.about), ...about, tags.length ? el('div', { class: 'nt-pg-chips' }, tags) : null));
|
|
335
|
+
const det = [['When', fullAt(m.start).replace(/:\d\d$/, '') + (m.end ? '–' + hmAt(m.end) : '')]];
|
|
336
|
+
if (c && c.series) det.push(['Series', c.series]);
|
|
337
|
+
if (c && c.role) det.push(['Your role', c.role]);
|
|
338
|
+
if (c && c.external) det.push(['Outside', 'someone from outside the company attends']);
|
|
339
|
+
const links = ((c && c.links) || []).map((l) => el('a', { class: 'nt-pg-chip', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, l.label));
|
|
340
|
+
cols.push(el('div', { class: 'nt-pg-ctx__col', 'data-ntm-ctx': 'details' }, el('h4', null, COPY.details),
|
|
341
|
+
el('dl', { class: 'nt-pg-dl' }, det.map(([k, v]) => [el('dt', null, k), el('dd', null, v)])), links.length ? el('div', { class: 'nt-pg-chips' }, links) : null));
|
|
342
|
+
return el('section', { class: 'nt-pg-ctx nt-pg-ctx--open', 'data-ntm': 'context', 'data-ntm-context': 'open' }, head,
|
|
343
|
+
el('div', { class: 'nt-pg-ctx__b' }, cols, c ? null : el('p', { class: 'nt-pg-dim nt-pg-ctx__note' }, COPY.noContext)));
|
|
259
344
|
}
|
|
260
345
|
|
|
261
346
|
function noteParts(inst, n, live) {
|
|
262
347
|
const p = partsOf(n);
|
|
348
|
+
const held = live.mode !== 'idle' && live.id === n.id;
|
|
349
|
+
const recording = held && live.mode === 'recording';
|
|
263
350
|
const meta = [COPY.meetingNote, whenLong(startOf(n)), minutes(lengthOf(n)), iniName(n.initiative)].filter(Boolean).join(' · ');
|
|
264
351
|
const title = field({ label: 'Title', kind: 'title', hideLabel: true, value: n.title || '', placeholder: 'Untitled note',
|
|
265
352
|
attrs: { 'data-ntm': 'title', class: 'nt-pg-title' },
|
|
266
|
-
onCommit: async (v) => { const r = await pmPost('/api/notes/save', { id: n.id, title: v }); if (r && r.ok) { n.title = v; refresh(inst); } else toast({ text: COPY.notSaved, tone: 'destructive' }); } });
|
|
267
|
-
|
|
353
|
+
onCommit: async (v) => { const r = await pmPost('/api/notes/save', { id: n.id, title: v }); if (r && r.ok) { n.title = v; V.noteTitled(n.id, v); refresh(inst); } else toast({ text: COPY.notSaved, tone: 'destructive' }); } });
|
|
354
|
+
// the icons (R54): recording shows Stop only (Highlight and Pause are in the top bar); paused shows Resume and Stop; a stopped
|
|
355
|
+
// meeting shows neither; a note never recorded shows Record
|
|
268
356
|
const acts = [];
|
|
269
357
|
if (!n.private) {
|
|
270
|
-
acts.push(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
358
|
+
if (held && !recording) acts.push(iconButton({ icon: 'play', ariaLabel: 'Resume ' + (n.title || 'the note'), onClick: () => dock.resumeNow(), attrs: { class: 'nt-pg-resume', 'data-ntm': 'resume' } }));
|
|
359
|
+
if (held) acts.push(iconButton({ icon: 'square', ariaLabel: COPY.stop, onClick: () => dock.stopNow(), attrs: { class: 'nt-pg-stop', 'data-ntm': 'stop' } }));
|
|
360
|
+
else if (n.state === 'idle' || (n.state === undefined && !(n.transcript || []).length)) {
|
|
361
|
+
const busy = live.mode === 'recording';
|
|
362
|
+
const b = iconButton({ icon: 'mic', ariaLabel: busy ? 'Pause or stop the meeting being recorded first' : COPY.record, onClick: () => { if (!busy) dock.recordNote(n.id); }, attrs: { 'data-ntm': 'record' } });
|
|
363
|
+
if (busy) b.setAttribute('aria-disabled', 'true');
|
|
364
|
+
acts.push(b);
|
|
365
|
+
}
|
|
274
366
|
}
|
|
367
|
+
acts.push(iconButton({ icon: 'file-text', ariaLabel: COPY.copy, onClick: () => copyMarkdown(n), attrs: { 'data-ntm': 'copy' } }));
|
|
275
368
|
const more = iconButton({ icon: 'ellipsis', ariaLabel: 'More', attrs: { 'data-ntm': 'more' },
|
|
276
369
|
onClick: () => menu({ anchor: more, ariaLabel: 'Note actions', items: [
|
|
277
|
-
{ label: COPY.
|
|
278
|
-
{ label: COPY.
|
|
370
|
+
{ label: COPY.linkMeeting, icon: 'link', onSelect: () => V.linkMeeting({ state: inst.ctx.state, setState: inst.ctx.setState }, n.id), attrs: { 'data-ntm-more-meeting': '' } },
|
|
371
|
+
{ label: COPY.linkInitiative, icon: 'target', onSelect: () => V.linkInitiative({ state: inst.ctx.state, setState: inst.ctx.setState }, n.id), attrs: { 'data-ntm-more-initiative': '' } },
|
|
372
|
+
{ sep: true },
|
|
373
|
+
{ label: COPY.del, danger: true, onSelect: () => deleteNote(inst, n) },
|
|
279
374
|
] }) });
|
|
280
375
|
acts.push(more);
|
|
281
376
|
const out = [
|
|
282
377
|
el('div', { class: 'nt-pg-top' },
|
|
283
|
-
el('span', { class: 'nt-pg-meta', 'data-ntm': 'meta' }, meta),
|
|
378
|
+
el('div', { class: 'nt-pg-metarow' }, el('span', { class: 'nt-pg-meta', 'data-ntm': 'meta' }, meta),
|
|
379
|
+
iconButton({ icon: 'clock', ariaLabel: COPY.timeInfo, expanded: !!inst.info, size: 'sm', onClick: () => { inst.info = !inst.info; drawRight(inst, true); }, attrs: { 'data-ntm': 'time-info' } })),
|
|
380
|
+
inst.info ? infoPanel(n) : null,
|
|
284
381
|
el('div', { class: 'nt-pg-titlerow' }, el('div', { class: 'nt-pg-titlewrap' }, title), el('div', { class: 'nt-pg-acts' }, acts))),
|
|
285
382
|
];
|
|
286
383
|
if (n.private) out.push(el('p', { class: 'nt-hint' }, 'This note is private: it is not recorded, and nothing from it goes to Jev or to any agent.'));
|
|
384
|
+
out.push(contextCard(inst, n, held));
|
|
385
|
+
if (held) {
|
|
386
|
+
const liveHost = el('div', { class: 'nt-pg-live', 'data-ntm': 'live' });
|
|
387
|
+
if (V.renderLive(liveHost, { state: inst.ctx.state, setState: inst.ctx.setState, FIXTURE: inst.ctx.FIXTURE }, n.id)) out.push(liveHost);
|
|
388
|
+
}
|
|
287
389
|
|
|
288
390
|
const cf = conflictsOf(n);
|
|
289
391
|
if (cf.length) {
|
|
@@ -292,7 +394,7 @@ function noteParts(inst, n, live) {
|
|
|
292
394
|
cf.map((c) => el('div', { class: 'nt-pg-li', 'data-ntm-conflict': c.doc },
|
|
293
395
|
el('span', { class: 'nt-pg-warn-dot' }),
|
|
294
396
|
el('span', { class: 'nt-pg-li__text' }, '“' + c.text + '” ', el('span', { class: 'nt-pg-dim' }, COPY.conflictWith + ' '), el('span', { class: 'nt-pg-mono' }, c.doc + (c.line ? ':' + c.line : ''))),
|
|
295
|
-
|
|
397
|
+
iconButton({ icon: 'file-text', ariaLabel: COPY.openFile + ': ' + c.doc, onClick: () => openFile(inst, c.doc), attrs: { 'data-ntm-open-file': c.doc } })))));
|
|
296
398
|
}
|
|
297
399
|
|
|
298
400
|
const hl = n.highlights || [];
|
|
@@ -325,12 +427,13 @@ function noteParts(inst, n, live) {
|
|
|
325
427
|
out.push(section('decision', COPY.decisions, p.decisions, '—'));
|
|
326
428
|
out.push(section('action', COPY.actions, p.actions, '○', (x) => [
|
|
327
429
|
x.owner || x.due ? el('span', { class: 'nt-pg-li__who' }, [x.owner, x.due].filter(Boolean).join(' · ')) : null,
|
|
328
|
-
|
|
430
|
+
iconButton({ icon: 'list-checks', ariaLabel: COPY.makeTask, onClick: (e) => makeTaskFrom(n, x, { repo: inst.repo, btn: e && e.currentTarget }), attrs: { 'data-ntm-make-task': '' } }),
|
|
329
431
|
]));
|
|
330
432
|
out.push(section('question', COPY.questions, p.questions, '?'));
|
|
331
433
|
|
|
332
434
|
const tr = (n.final && n.final.segments && n.final.segments.length ? n.final.segments.map((g) => ({ t: g.t, text: g.text })) : (n.transcript || []).filter((x) => !x.dropped));
|
|
333
|
-
|
|
435
|
+
// while the meeting is held the live block already shows the transcript, so the fold waits for the end
|
|
436
|
+
if (!held && (p.topics.length || tr.length)) {
|
|
334
437
|
const bits = [p.topics.length ? p.topics.length + (p.topics.length === 1 ? ' topic' : ' topics') : null, tr.length ? COPY.transcript(tr.length) : null].filter(Boolean).join(' · ');
|
|
335
438
|
out.push(el('details', { class: 'nt-pg-sec nt-pg-fold', 'data-ntm': 'discussion' },
|
|
336
439
|
el('summary', { class: 'nt-pg-fold__sum' }, COPY.discussion + ' · ' + bits),
|
|
@@ -364,6 +467,10 @@ async function saveFinal(inst, n, sec, i, text) {
|
|
|
364
467
|
return true;
|
|
365
468
|
}
|
|
366
469
|
|
|
470
|
+
async function copyMarkdown(n) {
|
|
471
|
+
try { await navigator.clipboard.writeText(V.noteMarkdown(n)); toast({ text: 'The note is on the clipboard as Markdown, with its meeting context.', timeout: 2500 }); }
|
|
472
|
+
catch { toast({ text: 'The clipboard was not available.', tone: 'destructive' }); }
|
|
473
|
+
}
|
|
367
474
|
function openFile(inst, doc) {
|
|
368
475
|
if (!inst.repo) return;
|
|
369
476
|
import('./panel-tabs.js').then((m) => m.openDoc(inst.repo, doc, 'notes')).catch(() => toast({ text: 'The file could not be opened.', tone: 'destructive' }));
|