create-pmos 0.67.1 → 0.69.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/command-palette.md +1 -1
- package/workspace/guide/initiatives-screen.md +1 -1
- package/workspace/guide/layout.md +35 -8
- package/workspace/guide/notes.md +12 -1
- package/workspace/guide/today.md +3 -1
- package/workspace/lib/notes-links.js +56 -0
- package/workspace/lib/notes.js +87 -5
- package/workspace/lib/routes-notes.js +47 -6
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +697 -3
- package/workspace/public/pm/doc-editor.js +473 -0
- package/workspace/public/pm/doc-rail.js +161 -0
- package/workspace/public/pm/doc-remote.js +180 -0
- package/workspace/public/pm/dock-sessions.js +57 -13
- package/workspace/public/pm/frame-copy.js +2 -0
- package/workspace/public/pm/icons.js +1 -1
- package/workspace/public/pm/initiative-sheet.js +10 -2
- package/workspace/public/pm/initiatives-copy.js +1 -0
- package/workspace/public/pm/notes-links.js +110 -0
- package/workspace/public/pm/panel-tabs.js +85 -3
- package/workspace/public/pm/primitives.js +8 -4
- package/workspace/public/pm/state.js +3 -0
- package/workspace/public/pm/tab-doc.js +175 -170
- package/workspace/public/pm/today-agenda.js +6 -1
- package/workspace/public/pm/today-day.js +6 -0
- package/workspace/public/pm/view-notes.js +113 -6
- package/workspace/public/vendor/lucide-sprite.js +15 -2
- package/workspace/replaced-tests.json +43 -0
- package/workspace/server.js +30 -0
- package/workspace/test-lists/workspace-tabs-declutter.json +13 -0
- package/workspace/third-party/README.md +1 -1
|
@@ -25,6 +25,7 @@ export const SEL = Object.freeze({
|
|
|
25
25
|
sprite: '.pmc-sprite',
|
|
26
26
|
icon: 'svg.pmc-icon',
|
|
27
27
|
btn: '.pmc-btn',
|
|
28
|
+
btnLabel: '.pmc-btn__label',
|
|
28
29
|
primary: '.pmc-btn--primary',
|
|
29
30
|
row: '.pmc-row',
|
|
30
31
|
owed: '.pmc-row--owed',
|
|
@@ -256,16 +257,17 @@ function metaLine(cls, meta, signals) {
|
|
|
256
257
|
|
|
257
258
|
/**
|
|
258
259
|
* row({ title, titleIcon?, meta?: string[], signals?: Node[] (2 at most), lead?: Node[], trail?: Node[], onOpen?, openLabel?,
|
|
259
|
-
* owed?: boolean, done?: boolean, attrs? })
|
|
260
|
+
* owed?: boolean, done?: boolean, compact?: boolean, attrs? })
|
|
261
|
+
* `compact` keeps the title and the meta line on one line each, cut with an ellipsis: for a narrow list such as the dock's.
|
|
260
262
|
* One title, one meta line holding at most two signals. With `onOpen` the title and meta are one button (the row's
|
|
261
263
|
* focus target); without it they are plain text.
|
|
262
264
|
*/
|
|
263
|
-
export function row({ title, titleIcon, meta, signals, lead, trail, onOpen, openLabel, owed, done, proposed, attrs, openAttrs } = {}) {
|
|
265
|
+
export function row({ title, titleIcon, meta, signals, lead, trail, onOpen, openLabel, owed, done, proposed, compact, attrs, openAttrs } = {}) {
|
|
264
266
|
const body = [titleLine('pmc-row__title', title, titleIcon), metaLine('pmc-row__meta', meta, signals)];
|
|
265
267
|
const main = onOpen
|
|
266
268
|
? el('button', { type: 'button', class: 'pmc-row__open', 'aria-label': openLabel || null, onClick: onOpen, ...(openAttrs || {}) }, body)
|
|
267
269
|
: el('span', { class: 'pmc-row__main' }, body);
|
|
268
|
-
return el('div', withClass(cx('pmc-row', owed && 'pmc-row--owed', done && 'pmc-row--done', proposed && 'pmc-row--proposed'), attrs),
|
|
270
|
+
return el('div', withClass(cx('pmc-row', owed && 'pmc-row--owed', done && 'pmc-row--done', proposed && 'pmc-row--proposed', compact && 'pmc-row--compact'), attrs),
|
|
269
271
|
lead && lead.length ? el('span', { class: 'pmc-row__lead' }, lead) : null,
|
|
270
272
|
main,
|
|
271
273
|
trail && trail.length ? el('span', { class: 'pmc-row__trail' }, trail) : null);
|
|
@@ -325,7 +327,8 @@ let openMenu = null;
|
|
|
325
327
|
let lastMenuClose = { anchor: null, at: 0 };
|
|
326
328
|
|
|
327
329
|
/**
|
|
328
|
-
* menu({ anchor, items: [{ label, onSelect, danger?, sep? }], ariaLabel }): opens a popover menu under `anchor`.
|
|
330
|
+
* menu({ anchor, items: [{ label, onSelect, danger?, sep?, icon?, attrs? }], ariaLabel }): opens a popover menu under `anchor`.
|
|
331
|
+
* An item's `attrs` are data hooks for the suites (a class or a role is the menu's own).
|
|
329
332
|
* Arrow keys move, Enter or Space chooses, Escape closes and returns focus to the anchor. Pressing the anchor again
|
|
330
333
|
* closes it.
|
|
331
334
|
*/
|
|
@@ -349,6 +352,7 @@ export function menu({ anchor, items, ariaLabel } = {}) {
|
|
|
349
352
|
for (const it of items || []) {
|
|
350
353
|
if (it.sep) { node.appendChild(el('div', { class: 'pmc-menu__sep', role: 'separator' })); continue; }
|
|
351
354
|
const b = el('button', {
|
|
355
|
+
...(it.attrs || {}),
|
|
352
356
|
type: 'button', role: 'menuitem', tabindex: '-1', class: cx('pmc-menu__item', it.danger && 'pmc-menu__item--danger'),
|
|
353
357
|
onClick: (e) => { e.stopPropagation(); close(false); if (it.onSelect) it.onSelect(); },
|
|
354
358
|
}, it.icon ? icon(it.icon) : null, el('span', null, it.label));
|
|
@@ -129,6 +129,7 @@ function initialBase() {
|
|
|
129
129
|
newSessIsolate: null, newSessError: null, newSessBusy: false,
|
|
130
130
|
newSessBranches: null, preflightProfile: null, profiles: null, branchInfo: null,
|
|
131
131
|
col0: true, col1: true, col2: true, scratchOpen: false, scratchText: '', copied: false,
|
|
132
|
+
dsFolded: {}, // the dock's session groups the PM folded (true) or opened (false), by group key — persisted
|
|
132
133
|
tabsVertical: false, // the cockpit's tabs as a vertical list instead of a strip — persisted, per browser
|
|
133
134
|
/* The scratchpad drawer's height in px (panel-tabs.js). 96 is the prototype's literal and stays
|
|
134
135
|
* the default; the PM can drag it, and the drag is clamped there rather than here — this store
|
|
@@ -238,6 +239,8 @@ const PERSIST = {
|
|
|
238
239
|
scratchOpen: isBool,
|
|
239
240
|
// the cockpit's tabs as a vertical list (true) or the horizontal strip (false, the default)
|
|
240
241
|
tabsVertical: isBool,
|
|
242
|
+
// the dock's session groups, folded (true) or open (false) by the PM; a group with no entry takes its default
|
|
243
|
+
dsFolded: isFlagMap,
|
|
241
244
|
/* A restored height is re-validated on the way IN, not merely on the way out: a corrupted or
|
|
242
245
|
* hand-edited record must not be able to render a drawer with no textarea in it, or one taller
|
|
243
246
|
* than any window it could be restored into. The upper bound here is deliberately generous (the
|
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
* 4-second session poll included — took a half-read document back to the top. `scrollPane` keeps one
|
|
33
33
|
* element per tab and mode; see the block comment above it.
|
|
34
34
|
*
|
|
35
|
+
* ── In live mode, a markdown file the PM can edit is edited in place (workspace-tabs-declutter) ─────────────
|
|
36
|
+
* The four modes are the prototype's, and the fixture keeps them. In live mode a markdown file the PM can write is
|
|
37
|
+
* drawn by doc-editor.js: read and edited in one surface, block by block, in the layout it is read in. `edit` stays as
|
|
38
|
+
* the whole file as markdown (the Markdown toggle) and `diff` as the Changes toggle; `raw` is the Source toggle of a
|
|
39
|
+
* file that cannot be edited. Beside the document, doc-rail.js holds its outline and its Connections.
|
|
40
|
+
*
|
|
35
41
|
* ── What `view` renders it through ──────────────────────────────────────────────────────────
|
|
36
42
|
* `markdown.js`, not `fixture.js`'s `mdParse`. The latter is a transcription of the design
|
|
37
43
|
* prototype's four-branch stand-in (`# `/`## `/`- `/paragraph) pinned byte-for-byte by
|
|
@@ -43,7 +49,10 @@ import { state, setState, FIXTURE } from './state.js';
|
|
|
43
49
|
import * as FX from './fixture.js';
|
|
44
50
|
import { mdRender } from './markdown.js';
|
|
45
51
|
import { openDoc } from './panel-initiatives.js';
|
|
46
|
-
import { button } from './primitives.js';
|
|
52
|
+
import { button, iconButton, menu } from './primitives.js';
|
|
53
|
+
import { editorNode, disposeEditor, headsOf, goToBlock, isEditing } from './doc-editor.js';
|
|
54
|
+
import { railNode, disposeRail, refreshGraph } from './doc-rail.js';
|
|
55
|
+
import { copyChip, refreshCopies } from './doc-remote.js';
|
|
47
56
|
|
|
48
57
|
/** `{{ expr }}` — one interpolated value in its own span, as the prototype's runtime emits it. */
|
|
49
58
|
const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
|
|
@@ -165,6 +174,8 @@ function save(tab, text) {
|
|
|
165
174
|
const final = out && typeof out.text === 'string' ? out.text : text;
|
|
166
175
|
disk.set(path, final);
|
|
167
176
|
claims.delete(wsOf(tab)); // the consistency record may move with the file; read it again
|
|
177
|
+
refreshCopies(wsOf(tab)); // …and so may the copy's state, and the file's ties
|
|
178
|
+
refreshGraph(wsOf(tab), tab.path);
|
|
168
179
|
commit(final);
|
|
169
180
|
},
|
|
170
181
|
(e) => { saveErr.set(path, (e && e.message) || 'could not reach the workspace server'); setState({}); },
|
|
@@ -388,35 +399,26 @@ function editBox(tab) {
|
|
|
388
399
|
/** Release what a closed doc tab held. Called by panel-tabs.js when the tab goes away. */
|
|
389
400
|
export function disposeDoc(id) {
|
|
390
401
|
boxes.delete(id);
|
|
402
|
+
docBars.delete(id);
|
|
391
403
|
normNote.delete(id);
|
|
392
404
|
frames.delete(id);
|
|
393
405
|
scrollers.delete(id);
|
|
394
406
|
running.delete(id); // the grant belonged to this open document, and it is closed
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
disposeEditor(id);
|
|
408
|
+
disposeRail(id);
|
|
409
|
+
places.delete(id);
|
|
410
|
+
railHidden.delete(id);
|
|
398
411
|
}
|
|
399
412
|
|
|
400
413
|
|
|
401
|
-
/* ── the
|
|
414
|
+
/* ── the findings beside a document (was the Alignment drawer) ─────────────────────────────────────────────────────
|
|
402
415
|
*
|
|
403
|
-
* The
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* · the board's "Apply" (a proposed edit to the other file, with its diff) has no backing — PMOS
|
|
409
|
-
* drafts no edit to another file — so it is not drawn, and the drawer says so;
|
|
410
|
-
* · a file the last check did not read is said to be unread, not shown as clean.
|
|
411
|
-
* The drawer is closed by default, and closed it adds nothing to the pane: the pane is exactly
|
|
412
|
-
* [mode bar, body] as before. Open, it is a third child laid over the right of the pane, and the
|
|
413
|
-
* mode bar and body give it room with a margin. No node is re-parented, so a textarea keeps its
|
|
414
|
-
* caret, a scroller its offset and a sandboxed frame its document. */
|
|
415
|
-
const alignOpen = new Set(); // tab ids whose drawer is open
|
|
416
|
-
const alignSkip = new Map(); // tab id -> Set of rels the PM skipped for this tab
|
|
416
|
+
* The Alignment drawer (workspace-cockpit-nav Run 3) listed the documents with a concrete tie to this one, from the
|
|
417
|
+
* consistency record the Validate view reads (`/api/claims`). The Connections rail (doc-rail.js) now draws every stated
|
|
418
|
+
* tie, so what the record adds is its FINDINGS: the ones in this file, and the ones in another file that name this
|
|
419
|
+
* file as their source. A tie is still never drawn from wording, and a file the last check did not read is not shown as
|
|
420
|
+
* clean: it has no findings to show, and the rail says only what was read. */
|
|
417
421
|
const claims = new Map(); // ws -> { state: 'loading' | 'ok' | 'error', data?, error? }
|
|
418
|
-
const ALIGN_W = 'min(420px, 50%)';
|
|
419
|
-
const alignNodes = new Map(); // tab id -> the drawer element, kept so its scroll survives a repaint
|
|
420
422
|
|
|
421
423
|
function readClaims(ws) {
|
|
422
424
|
let c = claims.get(ws);
|
|
@@ -430,132 +432,24 @@ function readClaims(ws) {
|
|
|
430
432
|
return c;
|
|
431
433
|
}
|
|
432
434
|
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
const STATUS_TONE = { contradicted: 'var(--destructive)', stale: 'var(--warning)', suspected: 'var(--muted-foreground)', undecided: 'var(--muted-foreground)' };
|
|
442
|
-
const STATUS_WORD = { contradicted: 'contradicted', stale: 'stale', suspected: 'suspected by Jev', undecided: 'worth a look' };
|
|
443
|
-
|
|
444
|
-
const statusBadge = (st) => el('span', {
|
|
445
|
-
style: A_BADGE + ';border-color:transparent;color:' + (STATUS_TONE[st] || 'var(--muted-foreground)')
|
|
446
|
-
+ ';background:color-mix(in oklab, ' + (STATUS_TONE[st] || 'var(--muted-foreground)') + ' 14%, transparent)',
|
|
447
|
-
}, iv(STATUS_WORD[st] || st));
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* The documents tied to `rel`, each with the ties that put it there. Three kinds, all read:
|
|
451
|
-
* · this file is compared with it (this file's `compared_with`)
|
|
452
|
-
* · it is compared with this file (its `compared_with` names this file)
|
|
453
|
-
* · a finding in it names this file (a finding whose `source` is this file)
|
|
454
|
-
*/
|
|
455
|
-
function tiesFor(rel, docs) {
|
|
456
|
-
const out = new Map();
|
|
457
|
-
const add = (r, tie, finding) => {
|
|
458
|
-
if (!r || r === rel) return;
|
|
459
|
-
let c = out.get(r);
|
|
460
|
-
if (!c) { c = { rel: r, ties: new Set(), findings: [] }; out.set(r, c); }
|
|
461
|
-
c.ties.add(tie);
|
|
462
|
-
if (finding) c.findings.push(finding);
|
|
463
|
-
};
|
|
464
|
-
const self = docs.find((x) => x.rel === rel) || null;
|
|
465
|
-
if (self) for (const r of self.compared_with || []) add(r, 'this file checks against it');
|
|
435
|
+
/** The findings that concern `rel`: its own, then each other file's finding whose source is `rel` (with that file). */
|
|
436
|
+
function findingsFor(ws, rel) {
|
|
437
|
+
const c = readClaims(ws);
|
|
438
|
+
if (c.state !== 'ok') return [];
|
|
439
|
+
const docs = Array.isArray(c.data && c.data.docs) ? c.data.docs : [];
|
|
440
|
+
const out = [];
|
|
441
|
+
const self = docs.find((x) => x.rel === rel);
|
|
442
|
+
if (self) for (const f of self.findings || []) out.push({ status: f.status, sentence: f.sentence });
|
|
466
443
|
for (const x of docs) {
|
|
467
444
|
if (x.rel === rel) continue;
|
|
468
|
-
|
|
469
|
-
for (const f of x.findings || []) if (f && f.source === rel) add(x.rel, 'a finding names this file', f);
|
|
470
|
-
}
|
|
471
|
-
return { self, list: [...out.values()] };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function alignDrawer(tab) {
|
|
475
|
-
const ws = wsOf(tab);
|
|
476
|
-
const rel = String(tab.path || '');
|
|
477
|
-
const close = () => { alignOpen.delete(tab.id); setState({}); };
|
|
478
|
-
const kids = [
|
|
479
|
-
el('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:8px' },
|
|
480
|
-
el('span', { style: 'font-weight:600;font-size:15px;font-family:var(--sans)', 'data-r29': 'align-title' }, 'Alignment'),
|
|
481
|
-
el('button', { onClick: close, 'data-r29': 'align-close', 'aria-label': 'close alignment', title: 'close the drawer',
|
|
482
|
-
style: 'background:transparent;border:0;color:var(--muted-foreground);font-size:15px;padding:2px 6px', hover: 'color:var(--foreground)' }, '⟩')),
|
|
483
|
-
el('div', { style: A_NOTE, 'data-r29': 'align-rule' }, 'Only documents with a concrete tie to this file are listed. No similarity guesses.'),
|
|
484
|
-
];
|
|
485
|
-
const c = readClaims(ws);
|
|
486
|
-
if (c.state === 'loading') {
|
|
487
|
-
kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': 'loading' }, 'Reading the consistency record…'));
|
|
488
|
-
} else if (c.state === 'error') {
|
|
489
|
-
kids.push(el('div', { style: A_NOTE + ';color:var(--destructive)', 'data-r29': 'align-status', 'data-state': 'error' },
|
|
490
|
-
iv('The consistency record could not be read: ' + c.error + '. Nothing is listed.')));
|
|
491
|
-
} else {
|
|
492
|
-
const d = c.data || {};
|
|
493
|
-
const docs = Array.isArray(d.docs) ? d.docs : [];
|
|
494
|
-
const openValidate = () => import('./panel-cockpit.js').then((m) => m.openView('validate')).catch(() => {});
|
|
495
|
-
if (d.verdict === 'not-checked' || !docs.length) {
|
|
496
|
-
kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': 'not-checked' },
|
|
497
|
-
'This repository has not been checked yet, so no tie is known. Open Validate and press “check now”.'));
|
|
498
|
-
kids.push(el('div', null, el('button', { style: A_BTN, hover: 'border-color:var(--ring)', onClick: openValidate, 'data-r29': 'align-validate' }, 'open Validate')));
|
|
499
|
-
} else {
|
|
500
|
-
const { self, list } = tiesFor(rel, docs);
|
|
501
|
-
const skipped = alignSkip.get(tab.id) || new Set();
|
|
502
|
-
const shown = list.filter((x) => !skipped.has(x.rel));
|
|
503
|
-
kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': self ? 'read' : 'unread' },
|
|
504
|
-
iv(self
|
|
505
|
-
? 'This file: ' + (self.classified ? self.type : 'not classified') + ' · ' + self.verified + ' statement(s) verified'
|
|
506
|
-
+ (self.findings.length ? ' · ' + self.findings.length + ' finding(s)' : ' · no open findings')
|
|
507
|
-
: 'The last check did not read this file, so its own findings are unknown. Ties from other files are still listed.')));
|
|
508
|
-
if (self && self.findings.length) {
|
|
509
|
-
kids.push(el('div', { style: A_LBL }, 'findings in this file'));
|
|
510
|
-
kids.push(el('div', { style: 'display:flex;flex-direction:column;gap:6px', 'data-r29': 'align-self' },
|
|
511
|
-
self.findings.map((f) => el('div', { 'data-r29': 'align-finding', 'data-status': f.status, style: 'display:flex;flex-direction:column;gap:3px' },
|
|
512
|
-
el('span', null, statusBadge(f.status)),
|
|
513
|
-
el('span', { style: 'font-size:12px;line-height:1.5;color:var(--foreground)' }, iv(f.sentence))))));
|
|
514
|
-
}
|
|
515
|
-
if (state.savedDocs[docKey(tab)] != null && list.length) {
|
|
516
|
-
kids.push(el('div', { style: A_NOTE + ';color:var(--warning)', 'data-r29': 'align-saved' },
|
|
517
|
-
iv('Saved. ' + list.length + ' other document' + (list.length === 1 ? '' : 's') + ' may now disagree with this change.')));
|
|
518
|
-
}
|
|
519
|
-
kids.push(el('div', { style: A_LBL }, iv('tied documents · ' + list.length)));
|
|
520
|
-
if (!list.length) kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-none' }, 'No other document has a concrete tie to this file.'));
|
|
521
|
-
for (const x of shown) {
|
|
522
|
-
kids.push(el('div', { style: A_CARD, 'data-r29': 'align-cand', 'data-rel': x.rel },
|
|
523
|
-
el('b', { style: 'font-family:var(--mono);font-size:12px;font-weight:600;overflow-wrap:anywhere' }, iv(x.rel)),
|
|
524
|
-
el('div', { style: 'display:flex;gap:6px;flex-wrap:wrap' }, [...x.ties].map((t) => el('span', { style: A_BADGE, 'data-r29': 'align-tie' }, iv(t)))),
|
|
525
|
-
x.findings.map((f) => el('div', { style: 'display:flex;flex-direction:column;gap:3px' },
|
|
526
|
-
el('span', null, statusBadge(f.status)),
|
|
527
|
-
el('span', { style: A_NOTE }, iv(f.sentence)))),
|
|
528
|
-
el('div', { style: 'display:flex;gap:6px' },
|
|
529
|
-
el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-open', onClick: () => openDoc(tab.slug, x.rel, 'FILE', tab.ws ? { ws: tab.ws, branch: tab.branch || null, linked: true } : undefined) }, 'Open to edit'),
|
|
530
|
-
el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-skip',
|
|
531
|
-
onClick: () => { const s2 = new Set(skipped); s2.add(x.rel); alignSkip.set(tab.id, s2); setState({}); } }, 'Skip'))));
|
|
532
|
-
}
|
|
533
|
-
if (skipped.size) {
|
|
534
|
-
kids.push(el('button', { style: A_BTN + ';align-self:flex-start', hover: 'border-color:var(--ring)', 'data-r29': 'align-unskip',
|
|
535
|
-
onClick: () => { alignSkip.delete(tab.id); setState({}); } }, iv('show ' + skipped.size + ' skipped')));
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
kids.push(el('div', { style: 'display:flex;gap:6px' },
|
|
539
|
-
el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-reread', onClick: () => { claims.delete(ws); setState({}); } }, 're-read')));
|
|
445
|
+
for (const f of x.findings || []) if (f && f.source === rel) out.push({ status: f.status, sentence: f.sentence, rel: x.rel });
|
|
540
446
|
}
|
|
541
|
-
|
|
542
|
-
'Ties come from the last consistency check. PMOS drafts no edit to another file: open it to change it. Git keeps the history.'));
|
|
543
|
-
/* One node per tab, kept like the scrollers above, so a repaint does not take the drawer's own
|
|
544
|
-
* scroll back to the top. */
|
|
545
|
-
let node = alignNodes.get(tab.id);
|
|
546
|
-
if (!node) { node = makeDrawerNode(); alignNodes.set(tab.id, node); }
|
|
547
|
-
setChildren(node, kids);
|
|
548
|
-
return node;
|
|
447
|
+
return out;
|
|
549
448
|
}
|
|
550
449
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
style: 'position:absolute;top:0;right:0;bottom:0;width:' + ALIGN_W + ';box-sizing:border-box;overflow-y:auto;'
|
|
555
|
-
+ 'border-left:1px solid var(--border);background:var(--sidebar);color:var(--foreground);padding:16px;'
|
|
556
|
-
+ 'display:flex;flex-direction:column;gap:12px;font-family:var(--sans);font-size:13px;line-height:1.5',
|
|
557
|
-
});
|
|
558
|
-
}
|
|
450
|
+
/** Whether a tab can have the rail at all: a file in the checked-out tree, in real mode. */
|
|
451
|
+
const canRail = (tab) => !FIXTURE && !tab.guide && !tab.ref && !isReference(tab.path);
|
|
452
|
+
const railHidden = new Set(); // tab ids whose outline and connections the PM put away
|
|
559
453
|
|
|
560
454
|
/* ── mount ─────────────────────────────────────────────────────────────────────────────────── */
|
|
561
455
|
|
|
@@ -724,7 +618,10 @@ export function mountDoc(host, tab) {
|
|
|
724
618
|
const raw = docText(tab);
|
|
725
619
|
const text = state.docEdits[docKey(tab)] ?? raw;
|
|
726
620
|
const nonMd = !FIXTURE && isSourcePath(tab.path); // file-viewer-any-text: read-only, shown as source
|
|
727
|
-
const
|
|
621
|
+
const writable = canWrite(tab, nonMd);
|
|
622
|
+
let dm = ((tab.readonly || nonMd) && (state.docModes[tab.id] === 'edit' || state.docModes[tab.id] === 'diff')) ? 'view' : (state.docModes[tab.id] ?? 'view');
|
|
623
|
+
if (writable && dm === 'raw') dm = 'view'; // in place, the file you can write IS its source: no read-only copy of it
|
|
624
|
+
if (!FIXTURE && !writable && (dm === 'edit' || dm === 'diff')) dm = 'view'; // only a markdown file can be saved
|
|
728
625
|
const dirty = text !== raw;
|
|
729
626
|
const err = saveErr.get(docKey(tab));
|
|
730
627
|
|
|
@@ -780,14 +677,7 @@ export function mountDoc(host, tab) {
|
|
|
780
677
|
const canNormalize = !!(norm && typeof norm.normalize === 'function')
|
|
781
678
|
&& !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab)) && !nonMd;
|
|
782
679
|
const normBtn = canNormalize ? el('button', {
|
|
783
|
-
onClick: () =>
|
|
784
|
-
const r = norm.normalize(text);
|
|
785
|
-
normNote.set(tab.id, r.changed
|
|
786
|
-
? r.joins + ' line(s) joined' + (r.blanks ? ', ' + r.blanks + ' blank inserted' : '')
|
|
787
|
-
: 'already normalized — every paragraph is one line');
|
|
788
|
-
if (!r.changed) { setState({}); return; }
|
|
789
|
-
setState((s) => ({ docEdits: { ...s.docEdits, [docKey(tab)]: r.text } }));
|
|
790
|
-
},
|
|
680
|
+
onClick: () => normalizeBuffer(tab, norm),
|
|
791
681
|
style: 'margin-left:8px;font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:var(--secondary);border:1px solid var(--border);color:var(--muted-foreground)',
|
|
792
682
|
hover: 'border-color:var(--ring)',
|
|
793
683
|
title: 'Join hard-wrapped paragraphs into one line each — what a Jira issue, a Google Doc or any'
|
|
@@ -812,21 +702,19 @@ export function mountDoc(host, tab) {
|
|
|
812
702
|
hover: 'color:var(--foreground)', 'data-r1': 'measure',
|
|
813
703
|
title: narrowOf(tab) ? 'use the full width of the tab' : 'limit lines to a 72-character reading measure',
|
|
814
704
|
}, iv(narrowOf(tab) ? '↔ full width' : '⇥ 72ch')),
|
|
815
|
-
canAlign(tab) ? el('button', {
|
|
816
|
-
onClick: () => {
|
|
817
|
-
if (alignOpen.has(tab.id)) alignOpen.delete(tab.id);
|
|
818
|
-
else { alignOpen.add(tab.id); claims.delete(wsOf(tab)); } // opening reads the record afresh
|
|
819
|
-
setState({});
|
|
820
|
-
},
|
|
821
|
-
style: 'margin-left:8px;font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:'
|
|
822
|
-
+ (alignOpen.has(tab.id) ? 'var(--secondary)' : 'transparent') + ';border:1px solid '
|
|
823
|
-
+ (alignOpen.has(tab.id) ? 'var(--brand)' : 'var(--border)') + ';color:var(--foreground)',
|
|
824
|
-
hover: 'border-color:var(--ring)', 'data-r29': 'align-toggle', 'data-open': alignOpen.has(tab.id) ? 'yes' : 'no',
|
|
825
|
-
title: 'Show the documents with a concrete tie to this one, from the last consistency check.',
|
|
826
|
-
}, 'alignment') : null,
|
|
827
705
|
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:' + dirtyColor }, iv(dirtyLabel)),
|
|
828
706
|
);
|
|
829
707
|
|
|
708
|
+
/* Live mode draws the bar from the design system (workspace-tabs-declutter): the four modes as one segmented control,
|
|
709
|
+
* the save state beside them, save and revert, and one ⋯ menu for what is used now and then (the reading width,
|
|
710
|
+
* normalize, alignment). Run scripts stays on the bar while a page can run: it grants a privilege, so its state is
|
|
711
|
+
* never one click away. The fixture keeps the prototype's bar above, which the parity suites pin. */
|
|
712
|
+
const bar = FIXTURE ? modeBar : docBar(tab, {
|
|
713
|
+
dm, readonly, writable, dirty, err, text, asPage, live, canNormalize, norm,
|
|
714
|
+
statusText: dirtyLabel,
|
|
715
|
+
tone: err ? 'error' : (dirty ? 'dirty' : 'clean'),
|
|
716
|
+
});
|
|
717
|
+
|
|
830
718
|
let body;
|
|
831
719
|
if (dm === 'raw') body = rawBody(tab, text);
|
|
832
720
|
else if (dm === 'diff') body = diffBody(tab, raw, text, dirty);
|
|
@@ -839,16 +727,133 @@ export function mountDoc(host, tab) {
|
|
|
839
727
|
if (body.defaultValue !== text) body.defaultValue = text;
|
|
840
728
|
if (body.value !== text) body.value = text;
|
|
841
729
|
} else if (!FIXTURE && (extOfPath(tab.path) === 'csv' || extOfPath(tab.path) === 'tsv')) body = gridBody(tab, text);
|
|
730
|
+
else if (writable) body = inPlace(tab, text, raw);
|
|
842
731
|
else body = asPage ? htmlFrame(tab, text, live) : (nonMd && !unread.has(docKey(tab)) ? sourceBody(tab, text) : viewBody(tab, text));
|
|
843
732
|
|
|
844
|
-
|
|
845
|
-
* margin is written on every paint so a closed drawer leaves no gap behind on a kept node. */
|
|
846
|
-
const drawerOn = canAlign(tab) && alignOpen.has(tab.id);
|
|
847
|
-
host.style.position = drawerOn ? 'relative' : '';
|
|
848
|
-
modeBar.style.marginRight = drawerOn ? ALIGN_W : '';
|
|
849
|
-
body.style.marginRight = drawerOn ? ALIGN_W : '';
|
|
850
|
-
setChildren(host, drawerOn ? [modeBar, body, alignDrawer(tab)] : [modeBar, body]);
|
|
733
|
+
setChildren(host, [bar, body]);
|
|
851
734
|
return host;
|
|
852
735
|
}
|
|
853
736
|
|
|
737
|
+
/* A markdown file the PM can write, in live mode: edited in place. A guide page ships with the app, a file from git is
|
|
738
|
+
* evidence, a reference names someone else's record, and a file that could not be read is a stub: none of those. */
|
|
739
|
+
function canWrite(tab, nonMd) {
|
|
740
|
+
if (FIXTURE || tab.readonly || tab.guide || tab.ref || nonMd || isReference(tab.path) || isHtmlDoc(tab.path)) return false;
|
|
741
|
+
const PK = typeof window !== 'undefined' ? window.PreviewKind : null;
|
|
742
|
+
if (!PK || typeof PK.previewKind !== 'function' || PK.previewKind(String(tab.path || '')) !== 'markdown') return false;
|
|
743
|
+
const k = docKey(tab);
|
|
744
|
+
return state.savedDocs[k] != null || (disk.has(k) && !unread.has(k));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/* The in-place surface: the editor and, beside it, the rail. One scrolling node per tab, kept like the scrollers above,
|
|
748
|
+
* so neither a repaint nor a keystroke moves the page. */
|
|
749
|
+
const places = new Map(); // tab id -> { node, grid }
|
|
750
|
+
function inPlace(tab, text, raw) {
|
|
751
|
+
let pl = places.get(tab.id);
|
|
752
|
+
if (!pl) {
|
|
753
|
+
const grid = el('div', { class: 'fr-ed__grid' });
|
|
754
|
+
pl = { node: el('div', { class: 'fr-ed', 'data-ed': 'scroll' }, grid), grid };
|
|
755
|
+
places.set(tab.id, pl);
|
|
756
|
+
}
|
|
757
|
+
const ws = wsOf(tab);
|
|
758
|
+
const main = editorNode(tab, {
|
|
759
|
+
text, ctx: linkCtx(tab),
|
|
760
|
+
onChange: (t) => { saveErr.delete(docKey(tab)); setState((s) => { const de = { ...s.docEdits }; if (t === raw) delete de[docKey(tab)]; else de[docKey(tab)] = t; return { docEdits: de }; }); },
|
|
761
|
+
onSave: () => { const cur = state.docEdits[docKey(tab)]; if (cur != null && cur !== raw) save(tab, cur); },
|
|
762
|
+
});
|
|
763
|
+
const showRail = canRail(tab) && !railHidden.has(tab.id);
|
|
764
|
+
const rail = showRail ? railNode(tab, {
|
|
765
|
+
ws, rel: String(tab.path || ''), heads: headsOf(tab.id), findings: findingsFor(ws, String(tab.path || '')),
|
|
766
|
+
onGoto: (i) => goToBlock(tab.id, i),
|
|
767
|
+
onOpen: (rel) => openDoc(tab.slug, rel, 'FILE', tab.ws ? { ws: tab.ws, branch: tab.branch || null, linked: true } : undefined),
|
|
768
|
+
}) : null;
|
|
769
|
+
pl.grid.setAttribute('data-measure', narrowOf(tab) ? '72ch' : 'full');
|
|
770
|
+
pl.grid.toggleAttribute('data-rail', !!rail);
|
|
771
|
+
setChildren(pl.grid, [main, rail]);
|
|
772
|
+
if (isEditing(tab.id)) pl.node.setAttribute('data-editing', ''); else pl.node.removeAttribute('data-editing');
|
|
773
|
+
return pl.node;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** normalize: the buffer's paragraphs joined into one line each. The buffer, never the file; a clean document says so. */
|
|
777
|
+
function normalizeBuffer(tab, norm) {
|
|
778
|
+
const raw = docText(tab);
|
|
779
|
+
const text = state.docEdits[docKey(tab)] ?? raw;
|
|
780
|
+
const r = norm.normalize(text);
|
|
781
|
+
normNote.set(tab.id, r.changed
|
|
782
|
+
? r.joins + ' line(s) joined' + (r.blanks ? ', ' + r.blanks + ' blank inserted' : '')
|
|
783
|
+
: 'already normalized — every paragraph is one line');
|
|
784
|
+
if (!r.changed) { setState({}); return; }
|
|
785
|
+
setState((s) => ({ docEdits: { ...s.docEdits, [docKey(tab)]: r.text } }));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* ── the document bar in live mode ──────────────────────────────────────────────────────────────────────────────────
|
|
789
|
+
* One bar per tab, kept so the ⋯ control keeps its focus across a repaint; its children are rebuilt. The bar says whether
|
|
790
|
+
* the file is saved and what its copy holds, and carries what changes the view: Changes while there is something to
|
|
791
|
+
* compare, the whole file as markdown (or, for a file that cannot be edited, its source), and one ⋯ menu for what is used
|
|
792
|
+
* now and then. Run scripts stays on the bar while a page can run: it grants a privilege, so its state is never one
|
|
793
|
+
* click away. Every action is the prototype's: the same state keys, the same buffer, the same save path. */
|
|
794
|
+
const docBars = new Map(); // tab id -> { node, more }
|
|
795
|
+
|
|
796
|
+
function docBar(tab, o) {
|
|
797
|
+
let kept = docBars.get(tab.id);
|
|
798
|
+
if (!kept) {
|
|
799
|
+
const more = iconButton({ icon: 'ellipsis', ariaLabel: 'Document options', size: 'sm', attrs: { 'data-doc': 'more', class: 'fr-doc-bar__more' },
|
|
800
|
+
onClick: (e) => menu({ anchor: e.currentTarget, ariaLabel: 'Document options', items: docMenu(tab) }) });
|
|
801
|
+
kept = { node: el('div', { class: 'fr-doc-bar', 'data-doc': 'bar' }), more };
|
|
802
|
+
docBars.set(tab.id, kept);
|
|
803
|
+
}
|
|
804
|
+
const setMode = (m) => setState((s) => ({ docModes: { ...s.docModes, [tab.id]: m } }));
|
|
805
|
+
const status = el('span', { class: 'fr-doc-bar__status', 'data-doc': 'status', 'data-tone': o.tone }, o.statusText);
|
|
806
|
+
const copy = canRail(tab) && !o.readonly ? copyChip(wsOf(tab), String(tab.path || '')) : null;
|
|
807
|
+
const inDiff = o.dm === 'diff';
|
|
808
|
+
const changes = o.writable && (o.dirty || inDiff) ? button({
|
|
809
|
+
label: 'changes', variant: inDiff ? 'outline' : 'ghost', size: 'sm', icon: 'git-compare',
|
|
810
|
+
attrs: { 'data-doc': 'changes', 'aria-pressed': inDiff ? 'true' : 'false', title: 'What saving would change on disk, line by line' },
|
|
811
|
+
onClick: () => setMode(inDiff ? 'view' : 'diff'),
|
|
812
|
+
}) : null;
|
|
813
|
+
const srcMode = o.writable ? 'edit' : 'raw';
|
|
814
|
+
const inSrc = o.dm === srcMode;
|
|
815
|
+
const source = button({
|
|
816
|
+
label: o.writable ? 'markdown' : 'source', variant: inSrc ? 'outline' : 'ghost', size: 'sm', icon: 'file-code',
|
|
817
|
+
attrs: { 'data-doc': 'source', 'aria-pressed': inSrc ? 'true' : 'false',
|
|
818
|
+
title: o.writable ? 'Edit the whole file as markdown' : 'Show the file as it is written' },
|
|
819
|
+
onClick: () => setMode(inSrc ? 'view' : srcMode),
|
|
820
|
+
});
|
|
821
|
+
const saveB = o.dirty ? button({ label: 'save', variant: 'primary', size: 'sm', attrs: { 'data-doc': 'save', title: 'Save (Ctrl or ⌘ S)' }, onClick: () => save(tab, o.text) }) : null;
|
|
822
|
+
const revertB = o.dirty ? button({ label: 'revert', variant: 'ghost', size: 'sm', attrs: { 'data-doc': 'revert' }, onClick: () => revert(tab) }) : null;
|
|
823
|
+
const run = o.asPage && o.dm === 'view' ? button({
|
|
824
|
+
label: o.live ? 'scripts running — stop' : 'run scripts', variant: o.live ? 'danger' : 'outline', size: 'sm',
|
|
825
|
+
onClick: () => { if (o.live) running.delete(tab.id); else running.add(tab.id); setState({}); },
|
|
826
|
+
attrs: { 'data-doc': 'run', 'aria-pressed': o.live ? 'true' : 'false',
|
|
827
|
+
title: o.live
|
|
828
|
+
? 'This document is running its own scripts, in a frame with no origin and no network. Stop discards it.'
|
|
829
|
+
: "Run this document's own scripts. It stays sandboxed: no origin, no network, nothing loaded from anywhere." },
|
|
830
|
+
}) : null;
|
|
831
|
+
const hasMenu = docMenu(tab).length > 0;
|
|
832
|
+
setChildren(kept.node, [status, copy, el('span', { class: 'fr-doc-bar__end' }, run, changes, revertB, saveB, source, hasMenu ? kept.more : null)]);
|
|
833
|
+
return kept.node;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** The ⋯ menu's items, read afresh when it opens, so each one says what choosing it will do now. */
|
|
837
|
+
function docMenu(tab) {
|
|
838
|
+
const items = [];
|
|
839
|
+
const dm = state.docModes[tab.id] ?? 'view';
|
|
840
|
+
if (dm === 'view' || dm === 'raw') {
|
|
841
|
+
const narrow = narrowOf(tab);
|
|
842
|
+
items.push({ label: narrow ? 'Use the full width' : 'Limit lines to 72 characters', icon: 'arrow-left-right', attrs: { 'data-r1': 'measure' },
|
|
843
|
+
onSelect: () => setState((s) => ({ docNarrow: { ...(s.docNarrow || {}), [tab.id]: !narrowOf(tab) } })) });
|
|
844
|
+
}
|
|
845
|
+
const norm = typeof window !== 'undefined' ? window.MdNormalize : null;
|
|
846
|
+
const nonMd = isSourcePath(tab.path);
|
|
847
|
+
if (norm && typeof norm.normalize === 'function' && !tab.readonly && !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab)) && !nonMd) {
|
|
848
|
+
items.push({ label: 'Normalize line breaks', icon: 'refresh-cw', attrs: { 'data-doc': 'normalize', title: 'Join hard-wrapped paragraphs into one line each. Writes the buffer, not the file: read it in Changes, then save.' },
|
|
849
|
+
onSelect: () => normalizeBuffer(tab, norm) });
|
|
850
|
+
}
|
|
851
|
+
if (canRail(tab) && canWrite(tab, nonMd)) {
|
|
852
|
+
const hidden = railHidden.has(tab.id);
|
|
853
|
+
items.push({ label: hidden ? 'Show the outline and connections' : 'Hide the outline and connections', icon: 'share-2', attrs: { 'data-doc': 'rail' },
|
|
854
|
+
onSelect: () => { if (hidden) railHidden.delete(tab.id); else railHidden.add(tab.id); setState({}); } });
|
|
855
|
+
}
|
|
856
|
+
return items;
|
|
857
|
+
}
|
|
858
|
+
|
|
854
859
|
export default mountDoc;
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* picks the week. The lens switch says which answer the cells show; the agenda under the month always shows them all.
|
|
13
13
|
* The calendar memory changes only through the morning-brief session's brief. The one write here is the PM's key-meeting
|
|
14
14
|
* mark (POST /api/proactive/agenda/mark): this time or every time, key or not key, over what the calendar says.
|
|
15
|
+
* A meeting's detail shows its meeting notes, and an event row a notes chip, from pm/notes-links.js (meeting-notes Run 4);
|
|
16
|
+
* Take notes writes there.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { peopleLinks } from './people-link.js';
|
|
@@ -19,6 +21,7 @@ import { el } from './dom.js';
|
|
|
19
21
|
import { button, iconButton, badge, tabs, toast } from './primitives.js';
|
|
20
22
|
import { pmGet, pmPost } from './http.js';
|
|
21
23
|
import { COPY } from './today-copy.js';
|
|
24
|
+
import { notesBlock, notesFor } from './notes-links.js';
|
|
22
25
|
|
|
23
26
|
const A = COPY.day.ag;
|
|
24
27
|
const DAY0 = 9 * 60;
|
|
@@ -203,7 +206,8 @@ function eventRow(h, e) {
|
|
|
203
206
|
const chips = el('span', { class: 'td-day__chips' },
|
|
204
207
|
e.imp ? badge({ text: A.star, tone: 'outline', attrs: { title: A.means } }) : null,
|
|
205
208
|
e.prep ? el('span', { class: 'td-ag__ready', 'data-ag-ready': e.ready || 'ok' }, A.readyBadge(e.prep_done || 0, e.prep.length)) : null,
|
|
206
|
-
e.
|
|
209
|
+
notesFor(e).length ? badge({ text: 'notes', tone: 'outline', attrs: { 'data-ag-notes': e.id, title: 'Meeting notes are linked to this event' } })
|
|
210
|
+
: e.ended && !e.notes && (e.imp || e.kind === 'one') ? el('span', { class: 'td-ag__nonotes' }, A.noNotes(1)) : null);
|
|
207
211
|
return h.rowBtn(type, e.id, { lead: e.time, title: e.title, meta: A.kindWord[e.kind] + (e.attendees ? ' · ' + A.people(e.attendees) : ''), trail: chips },
|
|
208
212
|
{ 'data-past': e.ended ? 'true' : null, 'data-ag-event': e.id });
|
|
209
213
|
}
|
|
@@ -327,5 +331,6 @@ export function meetingDetail(ctx, id, h) {
|
|
|
327
331
|
e.ended ? el('div', { class: 'td-day__phase', 'data-ag': 'notes' }, el('h4', null, A.afterNotes),
|
|
328
332
|
e.notes ? el('p', null, e.notes) : el('p', { class: 'td-day__note' }, A.noNotesYet),
|
|
329
333
|
e.open && e.open.length ? [el('h4', null, A.openLines), el('ul', { class: 'td-day__chk' }, ...e.open.map((x) => el('li', null, x)))] : null) : null,
|
|
334
|
+
notesBlock(e, { ws: ctx && ctx.ws }),
|
|
330
335
|
];
|
|
331
336
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* - on the left, the calendar (with a line at the current time), the jobs (needs you, agent working, done), the goals
|
|
11
11
|
* and the mentions with the watched channels, projects and spaces;
|
|
12
12
|
* - on the right, the detail of what is selected: a meeting, a job, a goal, a mention, a step, the sources or a place.
|
|
13
|
+
* A meeting's detail ends with its meeting notes: the summary and Open notes, or Take notes (pm/notes-links.js,
|
|
14
|
+
* meeting-notes Run 4).
|
|
13
15
|
* The presses go to the routes that own them: the goals route (add, done, drop, reopen, remove), /goals/plan (Prepare my
|
|
14
16
|
* day), /day/decide (approve, revise, run again), /day/check (Check now) and the settings route (sources, levels, the
|
|
15
17
|
* daytime check, websites). Nothing here sends anything outward: the agent sessions do that, after the PM approves.
|
|
@@ -30,6 +32,7 @@ import { COPY } from './today-copy.js';
|
|
|
30
32
|
import { goalRows, goalAdd, goalDetail, addGoal, goalsSig } from './today-goals.js';
|
|
31
33
|
import { peopleLinks } from './people-link.js';
|
|
32
34
|
import { agendaPanel, meetingDetail, memoryBlock, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
|
|
35
|
+
import { notesBlock, onNotes } from './notes-links.js';
|
|
33
36
|
|
|
34
37
|
const D = COPY.day;
|
|
35
38
|
const SLOW_MS = 30000;
|
|
@@ -57,6 +60,7 @@ const ui = {
|
|
|
57
60
|
revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
|
|
58
61
|
};
|
|
59
62
|
let rerender = () => {};
|
|
63
|
+
let notesSub = null; // the redraw when the meeting notes list is read (pm/notes-links.js)
|
|
60
64
|
let timer = null;
|
|
61
65
|
|
|
62
66
|
/* ── the read ─────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -566,6 +570,7 @@ function eventDetail(ctx, e) {
|
|
|
566
570
|
memoryBlock(e.id),
|
|
567
571
|
!e.ended ? el('div', { class: 'td-day__acts' }, e.prep ? pill(D.needPrep, 'warn') : steps.before && steps.before.length ? pill(D.nothingToPrep, 'ok') : null) : null,
|
|
568
572
|
after,
|
|
573
|
+
notesBlock(e, { ws: ctx.ws, initiative: ini }),
|
|
569
574
|
!e.ended ? [list(D.phases.before, (steps.before || []).map(stepItem)), list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), mailBlock] : null,
|
|
570
575
|
Array.isArray(e.links) && e.links.length ? el('div', { class: 'td-day__acts', 'data-day': 'links' }, ...e.links.map((l) => extLink((l.label || l.url) + ' ↗', l.url))) : null,
|
|
571
576
|
];
|
|
@@ -856,6 +861,7 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
856
861
|
// cause a whole-page render under whatever the PM is doing elsewhere
|
|
857
862
|
if (setState) rerender = () => { if (document.querySelector('[data-today="day"]')) setState({}); };
|
|
858
863
|
onAgenda(() => rerender());
|
|
864
|
+
if (!notesSub) notesSub = onNotes(() => rerender());
|
|
859
865
|
ensure(ws);
|
|
860
866
|
const c = cache.get(ws) || {};
|
|
861
867
|
const m = model(c.data, { here: here || null, all: !!all });
|