create-pmos 0.179.0 → 0.180.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/c3-report-setup-test.js +190 -0
- package/workspace/c3-reports-test.js +30 -97
- package/workspace/guide/document-sheet.md +14 -0
- package/workspace/guide/index.md +3 -2
- package/workspace/guide/reports-dashboards.md +17 -28
- package/workspace/guide/reports-page.md +10 -5
- package/workspace/guide/reports-set-up.md +56 -0
- package/workspace/guide/reports.md +13 -1
- package/workspace/guide/settings.md +11 -2
- package/workspace/lib/doc-sync.js +28 -6
- package/workspace/lib/docs-cli.js +7 -11
- package/workspace/lib/four-panel.js +1 -1
- package/workspace/lib/headless.js +21 -2
- package/workspace/lib/report-checks.js +103 -0
- package/workspace/lib/report-cli.js +89 -4
- package/workspace/lib/report-export.js +347 -0
- package/workspace/lib/report-finder.js +156 -0
- package/workspace/lib/report-needs.js +1 -1
- package/workspace/lib/report-setups.js +743 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-c3-reports.js +35 -17
- package/workspace/lib/routes-doc-health.js +15 -1
- package/workspace/lib/routes-report-setups.js +368 -0
- package/workspace/lib/schedule-logic.js +5 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-rep2.js +15 -0
- package/workspace/public/pm/c3-demo-rs.js +281 -0
- package/workspace/public/pm/c3-demo.js +1 -1
- package/workspace/public/pm/c3-doc-formats.js +152 -0
- package/workspace/public/pm/c3-doc-sheet.js +115 -25
- package/workspace/public/pm/c3-quiet.js +2 -2
- package/workspace/public/pm/c3-report-print.css +20 -0
- package/workspace/public/pm/c3-report-setup.css +62 -0
- package/workspace/public/pm/c3-report-setup.js +260 -0
- package/workspace/public/pm/c3-report-store.js +208 -0
- package/workspace/public/pm/c3-reports.css +4 -0
- package/workspace/public/pm/c3-reports.js +47 -181
- package/workspace/public/pm/c3-settings.js +4 -3
- package/workspace/public/pm/c3-shell.js +15 -31
- package/workspace/server.js +66 -5
- package/workspace/skills/doc-sync.skill +17 -8
- package/workspace/skills/report-find.skill +75 -0
- package/workspace/skills/report-setup.skill +67 -0
- package/workspace/test-files.json +10 -0
- package/workspace/test-lists/report-workspace-ui.json +5 -0
|
@@ -40,8 +40,10 @@ import { c3ic } from './pm-work-view.js';
|
|
|
40
40
|
import { openSheet } from './c3-sheet.js';
|
|
41
41
|
import { say } from './c3-init-extras.js';
|
|
42
42
|
import { c3btn } from './page-agent.js';
|
|
43
|
+
import { button } from './primitives.js';
|
|
43
44
|
import { mountSlide } from './slide-confirm.js';
|
|
44
45
|
import { parseMd, toMd, canonMd, diffMd, toDom, fromDom, wordCounts, safeHref } from './c3-md.js';
|
|
46
|
+
import { ensureReportCss, formatOf, formatView, outlineLabels, downloadsOf, printPdf, copyKindFor } from './c3-doc-formats.js';
|
|
45
47
|
|
|
46
48
|
/* ── pure: sections, round trip, word diff ── */
|
|
47
49
|
|
|
@@ -179,7 +181,7 @@ export function composeSections(base, groups, intro, front) {
|
|
|
179
181
|
/* ── the sheet ── */
|
|
180
182
|
|
|
181
183
|
const NAMES = { fresh: 'Fresh', decisions: 'Matches decisions', data: 'Matches the data', complete: 'Complete', traced: 'Traced to a goal and tickets', readers: 'Readers have this version' };
|
|
182
|
-
const STATE = { current: ['Current', 'ds-good', 'check'], stale: ['Stale', 'ds-warn', 'alert'], draft: ['Draft', 'ds-pe', 'clock'], missing: ['Missing', 'ds-bad', 'plus'] };
|
|
184
|
+
const STATE = { issued: ['Issued', 'ds-good', 'check'], current: ['Current', 'ds-good', 'check'], stale: ['Stale', 'ds-warn', 'alert'], draft: ['Draft', 'ds-pe', 'clock'], missing: ['Missing', 'ds-bad', 'plus'] };
|
|
183
185
|
const TABS = [['read', 'eye', 'Read'], ['edit', 'edit', 'Edit'], ['ask', 'spark', 'Proposals'], ['hist', 'clock', 'Edit log'], ['health', 'shield', 'Health'], ['links', 'init', 'Relations']];
|
|
184
186
|
const MODE = { proposals: 'ask', props: 'ask', log: 'hist', history: 'hist' };
|
|
185
187
|
const isDemo = () => !!window.__c3demo;
|
|
@@ -190,6 +192,8 @@ const hcls = (h) => (!h || h.total === 0 ? '' : h.passed === h.total ? 'ds-good'
|
|
|
190
192
|
const isAgentRev = (r) => !!r && (r.text_by ? r.text_by === 'agent' : byName(r.by) === 'Agent');
|
|
191
193
|
const md = (text, extra) => el('div', { class: 'ds-md' + (extra ? ' ' + extra : '') }, toDom(parseMd(text), document));
|
|
192
194
|
const btn = (cls, attrs, kids, onClick) => c3btn(cls, attrs, kids, onClick);
|
|
195
|
+
/* a console3 primitive button (the Issue press and the menu: the mockup's UI.btn) */
|
|
196
|
+
const pb = (variant, attrs, kids, onClick) => { const b = button({ label: '', variant, size: 'sm', onClick, attrs: attrs || {} }); b.replaceChildren(...[].concat(kids).filter((k) => k != null && k !== false)); return b; };
|
|
193
197
|
const headLevel = (s) => (/^(#+)/.exec(s.prefix || '') || ['', ''])[1].length;
|
|
194
198
|
|
|
195
199
|
/* ── v2 (c3-doc-sheet-v2): the copies in other tools, the outline, the graph ── */
|
|
@@ -201,16 +205,19 @@ const SIG = {
|
|
|
201
205
|
diverged: ['Both changed', 'alert', 'ds-bad', 'Both changed since they last matched.', 'both changed'],
|
|
202
206
|
unchecked: ['Not checked', 'info', '', 'No check has read this copy yet.', 'not checked'],
|
|
203
207
|
unreadable: ['Cannot open', 'alert', 'ds-bad', 'The last check could not open the copy.', 'cannot open'],
|
|
208
|
+
none: ['Not created', 'info', '', 'No copy exists yet.', 'not created'],
|
|
204
209
|
'missing-remote': ['Not found', 'alert', 'ds-bad', 'The copy is not there any more.', 'not found'],
|
|
205
210
|
'missing-repo': ['Not in PMOS', 'alert', 'ds-bad', 'This file is not in the repository any more.', 'not in PMOS'] };
|
|
206
211
|
const SYNCING = ['Updating', 'clock', 'ds-acc', 'A session is updating this copy.', 'updating'];
|
|
207
|
-
const KLABEL = { google: 'Google Doc', confluence: 'Confluence' };
|
|
212
|
+
const KLABEL = { google: 'Google Doc', 'google-slides': 'Google Slides', 'google-sheets': 'Google Sheets', confluence: 'Confluence' };
|
|
208
213
|
const sigOf = (l) => (l.syncing ? SYNCING : SIG[l.signal] || SIG.unchecked);
|
|
209
214
|
const labelOf = (l) => KLABEL[l.kind] || l.label || l.kind;
|
|
210
215
|
/** Which tool a pasted address names (the server checks it again): 'confluence', 'google' or null. */
|
|
211
216
|
export function copyKindOf(target) {
|
|
212
217
|
const t = String(target || '');
|
|
213
218
|
if (/atlassian|confluence|\/wiki\/|pageId=/i.test(t)) return 'confluence';
|
|
219
|
+
if (/docs\.google\.com\/presentation|slides\.google|\/presentation\/d\//i.test(t)) return 'google-slides';
|
|
220
|
+
if (/docs\.google\.com\/spreadsheets|sheets\.google|\/spreadsheets\/d\//i.test(t)) return 'google-sheets';
|
|
214
221
|
if (/google|docs\.|drive\./i.test(t)) return 'google';
|
|
215
222
|
return null;
|
|
216
223
|
}
|
|
@@ -244,13 +251,20 @@ const olGet = () => { try { return localStorage.getItem(OL_KEY) !== 'off'; } cat
|
|
|
244
251
|
const olSet = (on) => { try { localStorage.setItem(OL_KEY, on ? 'on' : 'off'); } catch { /* no storage here */ } };
|
|
245
252
|
const ESTATE = { ok: 'in step', drift: 'out of step', conflict: 'conflict', missing: 'missing', ahead: 'PMOS ahead' };
|
|
246
253
|
|
|
247
|
-
export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra, library, openGraph }) {
|
|
254
|
+
export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra, library, openGraph, report }) {
|
|
248
255
|
const S = { mode: MODE[mode] || mode, prompt, text: null, err: '', revs: null, texts: {}, health: null, hAsk: false, busy: false, changed: false,
|
|
249
256
|
meta: null, metaFor: null, props: null, propsErr: '', log: null, logErr: '', asking: null, closed: false, who: false, src: false, preview: {},
|
|
250
257
|
pick: {}, rejFor: null, rejWhy: '', whyFor: null, whyText: '', staleWhy: null, logF: 'all', cmp: null, cmpNode: null, flashRev: null, ed: null, leave: null,
|
|
251
|
-
copies: null, copiesErr: '', syncOpen: false, linking: false, linkUrl: '', pend: {}, checkRun: null, lib: undefined, ol: olGet(), olOpen: false, olq: '', olShut: {}, cur: null };
|
|
258
|
+
copies: null, copiesErr: '', syncOpen: false, menu: false, linking: false, linkUrl: '', pend: {}, checkRun: null, lib: undefined, ol: olGet(), olOpen: false, olq: '', olShut: {}, cur: null };
|
|
252
259
|
const demo = isDemo();
|
|
253
260
|
const base = { ws, path: doc.path };
|
|
261
|
+
/* a report cycle (initiative report-workspace): report = { output, state: 'draft'|'issued', name, cycle, foot, onIssue(doc), onNext(doc), onState(state) }.
|
|
262
|
+
* It is a document with an output format: Read draws it as slides, a grid, a page or A4 pages, an issued one is frozen, and the copy is
|
|
263
|
+
* created by the PM's press. Everything else in the sheet is the ordinary document sheet. */
|
|
264
|
+
const R = report || null;
|
|
265
|
+
if (R) ensureReportCss();
|
|
266
|
+
const frozen = () => !!R && R.state === 'issued';
|
|
267
|
+
const fmt = () => formatOf(R && R.output);
|
|
254
268
|
let ui = null;
|
|
255
269
|
|
|
256
270
|
/* ── data ── */
|
|
@@ -324,11 +338,12 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
324
338
|
const goEl = (sel) => { const n = ui && ui.body.querySelector(sel); if (n) { n.scrollIntoView({ block: 'start' }); n.classList.remove('flash'); void n.offsetWidth; n.classList.add('flash'); } };
|
|
325
339
|
|
|
326
340
|
function head() {
|
|
327
|
-
const st = STATE[fresh() ? 'stale' : doc.st] || STATE.current; const h = S.health; const last = lastRev();
|
|
341
|
+
const st = STATE[R ? R.state : fresh() ? 'stale' : doc.st] || STATE.current; const h = S.health; const last = lastRev();
|
|
328
342
|
return el('div', { class: 'chips2 ds-meta', 'data-doc-meta': '' },
|
|
329
343
|
el('span', { class: 'chip2 ' + st[1] }, c3ic(st[2], 'sm'), st[0]), el('span', { class: 'chip2' }, doc.ty || 'Document'), doc.v ? el('span', { class: 'chip2 mono' }, doc.v) : null,
|
|
330
344
|
h && h.total ? btn('chip2 lk ' + hcls(h), { 'data-doc-hchip': '' }, [c3ic('shield', 'sm'), h.passed + ' of ' + h.total + ' checks'], () => go('health')) : null,
|
|
331
345
|
el('span', { class: 'chip2' }, c3ic('init', 'sm'), doc.iniName || doc.ini || 'Portfolio'), copyChips(),
|
|
346
|
+
R ? el('span', { class: 'chip2', 'data-doc-format': R.output }, c3ic(fmt().ic, 'sm'), fmt().l) : null,
|
|
332
347
|
last && !generated ? btn('chip2 lk', { 'data-doc-last': '' }, [c3ic(isAgentRev(last) ? 'spark' : 'user', 'sm'), 'Last change: ', el('b', null, whoOf(last) + ','), ' ' + dayName(last.at)], () => go('hist')) : null);
|
|
333
348
|
}
|
|
334
349
|
function seg() {
|
|
@@ -389,10 +404,13 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
389
404
|
const d = (j.docs || []).find((x) => x.path === doc.path);
|
|
390
405
|
S.copies = d ? d.links || [] : []; S.copiesErr = ''; S.checkRun = j.session || j.open || null;
|
|
391
406
|
};
|
|
407
|
+
/* a report with no copy yet still shows the copy it will make: the tool its output keeps its copy in, "not created" */
|
|
408
|
+
const copyList = () => { const L = S.copies || []; return R && !L.length && fmt().copy ? [{ kind: fmt().copy, signal: 'none', synthetic: true, title: doc.name }] : L; };
|
|
392
409
|
const copyChips = () => {
|
|
393
410
|
if (generated || S.copies == null) return null;
|
|
394
|
-
if (!
|
|
395
|
-
|
|
411
|
+
if (R && !fmt().copy) return btn('chip2 lk', { 'data-doc-copychip': 'pdf', 'aria-expanded': String(S.syncOpen) }, [c3ic('pdf', 'sm'), 'PDF: no live copy'], () => toggleCopies());
|
|
412
|
+
if (!copyList().length) return btn('chip2 lk', { 'data-doc-copychip': '', 'aria-expanded': String(S.syncOpen) }, [c3ic('link', 'sm'), 'Link a Confluence or Google Doc copy'], () => toggleCopies());
|
|
413
|
+
return copyList().map((l) => { const g = sigOf(l); return btn('chip2 lk ' + g[2], { 'data-doc-copychip': l.kind, 'aria-expanded': String(S.syncOpen), title: l.words || g[3] }, [c3ic(g[1], 'sm'), labelOf(l) + ': ' + g[4]], () => toggleCopies()); });
|
|
396
414
|
};
|
|
397
415
|
function toggleCopies() {
|
|
398
416
|
S.syncOpen = !S.syncOpen;
|
|
@@ -401,23 +419,28 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
401
419
|
}
|
|
402
420
|
function copiesPanel() {
|
|
403
421
|
if (!S.syncOpen || generated) return null;
|
|
404
|
-
|
|
422
|
+
if (R && !fmt().copy) return el('section', { class: 'ds-sync', 'aria-label': 'Copies in other tools', 'data-doc-copies': '' }, el('div', { class: 'hd' }, el('b', null, 'Copies in other tools'),
|
|
423
|
+
btn('ds-btn', { 'aria-label': 'Close the copies', 'data-copy-close': '' }, c3ic('x', 'sm'), () => { S.syncOpen = false; draw(); })),
|
|
424
|
+
el('div', { class: 'fold' }, c3ic('info', 'sm'), 'A PDF has no live copy. Download it when you issue. Nothing goes to another tool.'));
|
|
425
|
+
const L = R ? copyList() : (S.copies || []);
|
|
405
426
|
const rows = L.map((l) => {
|
|
406
427
|
const g = sigOf(l); const name = labelOf(l); const pend = S.pend[l.kind];
|
|
407
428
|
const acts = [btn('ds-btn', { 'data-copy-check': l.kind, disabled: !!S.checkRun, 'aria-disabled': S.checkRun ? 'true' : null }, [c3ic(S.checkRun ? 'clock' : 'undo', 'sm'), S.checkRun ? 'Checking' : 'Check now'], () => checkNow()),
|
|
408
429
|
l.url ? el('a', { class: 'ds-btn', href: l.url, target: '_blank', rel: 'noopener', 'data-copy-open': l.kind }, c3ic('out', 'sm'), 'Open in ' + name) : null];
|
|
409
430
|
if (!l.syncing && (l.signal === 'behind' || l.signal === 'diverged')) acts.push(btn('ds-btn ag', { 'data-copy-take': l.kind }, [c3ic('spark', 'sm'), 'Bring their change in as a proposal'], () => takeCopy(l)));
|
|
410
431
|
if (!l.syncing && l.signal === 'ahead') acts.push(pend ? btn('ds-btn', { 'data-copy-undo': l.kind }, [c3ic('undo', 'sm'), 'Undo the update'], () => undoPush(l))
|
|
411
|
-
: btn('ds-btn pri', { 'data-copy-push': l.kind }, [c3ic('up', 'sm'), 'Update the ' + name + ' to this version'], () => pushCopy(l)));
|
|
432
|
+
: btn('ds-btn pri', { 'data-copy-push': l.kind }, [c3ic('up', 'sm'), R ? 'Update the ' + name + ' copy to ' + (doc.v || 'this version') : 'Update the ' + name + ' to this version'], () => pushCopy(l)));
|
|
433
|
+
if (R && !l.syncing && l.signal === 'none') acts.push(pend ? btn('ds-btn', { 'data-copy-undo': l.kind }, [c3ic('undo', 'sm'), 'Undo'], () => undoPush(l))
|
|
434
|
+
: btn('ds-btn pri', { 'data-copy-create': l.kind }, [c3ic('out', 'sm'), 'Create the ' + name + ' copy'], () => createCopy(l)));
|
|
412
435
|
const changed = l.remote_modified_at ? 'changed ' + whenOf(l.remote_modified_at) + (l.remote_modified_by ? ' by ' + l.remote_modified_by : '') : '';
|
|
413
436
|
return el('li', { 'data-copy': l.kind },
|
|
414
437
|
el('div', { class: 'l1' }, c3ic('doc', 'sm'), el('b', null, l.title || name), el('span', { class: 'tag ' + g[2], 'data-copy-state': l.syncing ? 'syncing' : l.signal || 'unchecked' }, c3ic(g[1], 'sm'), g[0])),
|
|
415
438
|
el('div', { class: 'ds-mono' }, [name, l.target, changed].filter(Boolean).join(' · ')),
|
|
416
|
-
el('p', null, l.words || g[3]),
|
|
439
|
+
el('p', null, l.words || (R && l.signal === 'none' ? 'No copy exists yet. PMOS writes to ' + name + ' only when you press Create the ' + name + ' copy.' : g[3])),
|
|
417
440
|
el('div', { class: 'ds-note' }, l.checked_at ? 'Last checked ' + whenOf(l.checked_at) + (l.checked_by ? ' by ' + byName(l.checked_by) : '') + '.' : 'Not checked yet.'),
|
|
418
441
|
l.syncing ? el('div', { class: 'fold', role: 'status', 'data-copy-syncing': l.kind }, c3ic('clock', 'sm'), 'A session is updating ' + (l.syncing.use === 'copy' ? 'PMOS from the ' + name : 'the ' + name + ' to this version') + (l.syncing.started_at ? '. It started ' + whenOf(l.syncing.started_at) + '.' : '.')) : null,
|
|
419
442
|
l.sync_failed ? el('div', { class: 'fold w', role: 'alert', 'data-copy-failed': l.kind }, c3ic('alert', 'sm'), 'The last update did not finish' + (l.sync_failed.reason ? ': ' + l.sync_failed.reason : '.')) : null,
|
|
420
|
-
pend ? el('div', { class: 'fold', role: 'status', 'data-copy-pending': l.kind }, c3ic('clock', 'sm'), 'The update starts in five seconds. Undo stops it.') : null,
|
|
443
|
+
pend ? el('div', { class: 'fold', role: 'status', 'data-copy-pending': l.kind }, c3ic('clock', 'sm'), (R && l.signal === 'none' ? 'The copy is created in five seconds' : 'The update starts in five seconds') + '. Undo stops it.') : null,
|
|
421
444
|
el('div', { class: 'ds-acts' }, acts));
|
|
422
445
|
});
|
|
423
446
|
const linkRow = S.linking ? el('div', { class: 'ds-whyadd ds-linkadd' },
|
|
@@ -425,11 +448,12 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
425
448
|
btn('ds-btn pri', { 'data-copy-linkok': '' }, 'Link', () => linkCopy()), btn('ds-btn', {}, 'Cancel', () => { S.linking = false; draw(); })) : null;
|
|
426
449
|
return el('section', { class: 'ds-sync', 'aria-label': 'Copies in other tools', 'data-doc-copies': '' },
|
|
427
450
|
el('div', { class: 'hd' }, el('b', null, 'Copies in other tools'), el('span', { class: 'ds-acts' },
|
|
428
|
-
btn('ds-btn', { 'data-copy-link': '' }, [c3ic('link', 'sm'), L.length ? 'Link another copy' : 'Link a copy'], () => { S.linking = true; S.linkUrl = ''; draw(); const i = ui.body.querySelector('#dcopyurl'); if (i) i.focus(); }),
|
|
451
|
+
R ? null : btn('ds-btn', { 'data-copy-link': '' }, [c3ic('link', 'sm'), L.length ? 'Link another copy' : 'Link a copy'], () => { S.linking = true; S.linkUrl = ''; draw(); const i = ui.body.querySelector('#dcopyurl'); if (i) i.focus(); }),
|
|
429
452
|
btn('ds-btn', { 'aria-label': 'Close the copies', 'data-copy-close': '' }, c3ic('x', 'sm'), () => { S.syncOpen = false; draw(); }))),
|
|
430
453
|
S.copiesErr ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.copiesErr) : null, linkRow,
|
|
431
454
|
L.length ? el('ul', null, rows) : el('p', { class: 'ds-note', 'data-copy-none': '' }, 'This document has no copy in Confluence or Google Docs yet. Link one to see whether it is in step.'),
|
|
432
|
-
el('p', { class: 'ds-note' }, 'PMOS never
|
|
455
|
+
el('p', { class: 'ds-note' }, R ? 'PMOS never writes to ' + fmt().app + ' on its own. Create and Update start one agent session through your own connector, and you can undo for 5 seconds. Nothing is sent to people.'
|
|
456
|
+
: 'PMOS never overwrites this document from a copy: a change in the copy comes in as a proposal you accept by section. Updating a copy starts one agent session, and you have five seconds to undo.'));
|
|
433
457
|
}
|
|
434
458
|
async function checkNow() {
|
|
435
459
|
const r = await pmPost('/api/docs/check', { ws }).catch(() => null);
|
|
@@ -459,6 +483,18 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
459
483
|
}, 5000);
|
|
460
484
|
draw();
|
|
461
485
|
}
|
|
486
|
+
function createCopy(l) {
|
|
487
|
+
const name = labelOf(l);
|
|
488
|
+
S.pend[l.kind] = setTimeout(async () => {
|
|
489
|
+
delete S.pend[l.kind];
|
|
490
|
+
const r = await pmPost('/api/docs/create-copy', { ws, path: doc.path, kind: l.kind }).catch(() => null);
|
|
491
|
+
if (!r || !r.ok) say((r && r.error) || 'The copy did not start.');
|
|
492
|
+
else say('Creating the ' + name + ' copy. A session writes it with your connector. Nothing is sent to people.');
|
|
493
|
+
await loadCopies(); drawBg();
|
|
494
|
+
}, 5000);
|
|
495
|
+
draw();
|
|
496
|
+
say('Creating the ' + name + ' copy in five seconds. Nothing is sent to people.', () => undoPush(l));
|
|
497
|
+
}
|
|
462
498
|
function undoPush(l) { clearTimeout(S.pend[l.kind]); delete S.pend[l.kind]; draw(); say('Stopped. The ' + labelOf(l) + ' is not updated.'); }
|
|
463
499
|
async function linkCopy() {
|
|
464
500
|
const t = String(S.linkUrl || '').trim(); const kind = copyKindOf(t);
|
|
@@ -488,12 +524,12 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
488
524
|
const on = S.cur === s.i;
|
|
489
525
|
rows.push(el('li', { class: 'l' + lv + (on ? ' on' : ''), 'data-ol-i': String(s.i) },
|
|
490
526
|
n && !qq ? btn('tw', { 'aria-label': (shut ? 'Show' : 'Hide') + ' the ' + plural(n, 'heading') + ' under ' + s.name, 'aria-expanded': String(!shut), 'data-ol-fold': String(s.i) }, c3ic('next', 'sm'), () => { S.olShut[s.i] = !shut; redrawOl(); }) : el('span', { class: 'tw' }),
|
|
491
|
-
btn('go', { 'data-ol-go': String(s.i), 'aria-current': on ? 'location' : null }, [el('span', null, s.name),
|
|
527
|
+
btn('go', { 'data-ol-go': String(s.i), 'aria-current': on ? 'location' : null }, [el('span', null, it.label || s.name),
|
|
492
528
|
stale.has(s.name) ? el('i', { class: 'mdot w', title: 'A source changed after this section', 'data-ol-stale': '' }) : null,
|
|
493
529
|
prop.has(s.name) ? el('i', { class: 'mdot p', title: 'The agent proposes a change here', 'data-ol-prop': '' }) : null], () => jumpTo(s.i))));
|
|
494
530
|
});
|
|
495
531
|
return el('nav', { class: 'ds-ol', 'aria-label': 'Outline', 'data-doc-outline': '' },
|
|
496
|
-
el('div', { class: 'hd' }, el('b', null, 'Outline'), el('span', { class: 'ds-mono' }, plural(items.length, 'heading'))),
|
|
532
|
+
el('div', { class: 'hd' }, el('b', null, 'Outline'), el('span', { class: 'ds-mono' }, plural(items.length, R ? fmt().word.replace(/s$/, '') : 'heading'))),
|
|
497
533
|
items.length > 12 ? el('input', { type: 'text', id: 'dolq', 'data-ol-find': '', placeholder: 'Find a heading', 'aria-label': 'Find a heading', value: S.olq, onInput: (e) => { S.olq = e.target.value; redrawOl(); } }) : null,
|
|
498
534
|
el('ul', null, rows.length ? rows : el('li', { class: 'none ds-note' }, 'No heading matches.')),
|
|
499
535
|
stale.size || prop.size ? el('div', { class: 'key ds-note' }, stale.size ? el('span', null, el('i', { class: 'mdot w' }), 'stale') : null, prop.size ? el('span', null, el('i', { class: 'mdot p' }), 'proposal') : null) : null);
|
|
@@ -634,7 +670,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
634
670
|
if (S.text == null) return S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the document.');
|
|
635
671
|
const secs = splitDoc(S.text);
|
|
636
672
|
if (!S.text.trim()) return el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The document is empty.');
|
|
637
|
-
const items = outlineOf(secs); const olOn = items.length >= 2 && !S.src;
|
|
673
|
+
const items = R ? outlineLabels(secs, R.output) : outlineOf(secs); const olOn = items.length >= 2 && !S.src;
|
|
638
674
|
if (S.cur == null && items.length) S.cur = items[0].s.i;
|
|
639
675
|
const curName = (items.find((x) => x.s.i === S.cur) || items[0] || { s: { name: '' } }).s.name;
|
|
640
676
|
const olCtl = olOn ? el('span', { class: 'ds-acts' },
|
|
@@ -652,23 +688,24 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
652
688
|
const meta = S.meta || {};
|
|
653
689
|
const pins = (name) => openProps().filter((p) => p.sections.some((c) => c.name === name && c.kind !== 'added'));
|
|
654
690
|
const flags = {}; failing().forEach((c) => { if (c.section && c.flag) flags[c.section] = c.flag; });
|
|
655
|
-
const
|
|
691
|
+
const secCell = (s, inner, label) => {
|
|
656
692
|
const fm = s.prefix === '' ? parseFront(s.text) : null;
|
|
657
693
|
const body = fm ? fm.rest.trim() : s.text;
|
|
658
694
|
if (!s.prefix && !body) return null;
|
|
659
695
|
const r = meta[s.name]; const P = pins(s.name);
|
|
660
696
|
const shown = P.find((p) => S.preview[p.id]);
|
|
661
697
|
const ch = shown && shown.sections.find((c) => c.name === s.name);
|
|
662
|
-
const content = ch ? md(diffMd(ch.stale ? s.text : ch.before, ch.after), 'ds-body') : body ? md(body, 'ds-body') : headLevel(s) === 1 ? null : el('p', { class: 'ds-note' }, 'This section is empty.');
|
|
698
|
+
const content = ch ? md(diffMd(ch.stale ? s.text : ch.before, ch.after), 'ds-body') : inner ? inner : body ? md(body, 'ds-body') : headLevel(s) === 1 ? null : el('p', { class: 'ds-note' }, 'This section is empty.');
|
|
663
699
|
const lvl = Math.min(3, Math.max(2, headLevel(s)));
|
|
664
700
|
return el('div', { class: 'ds-sec ' + (r ? (isAgentRev(r) ? 'a' : 'h') : '') + (ch ? ' prev' : ''), 'data-dsec': s.name, 'data-dsec-i': String(s.i) },
|
|
665
|
-
s.prefix ? el('div', { class: 'ds-sec-h' }, headLevel(s) === 1 ? el('div', { class: 'ds-md' }, el('h1', null, s.name)) : el('h' + lvl, null, s.name), generated ? null : byChip(r)) : null,
|
|
701
|
+
s.prefix ? el('div', { class: 'ds-sec-h' }, label ? el('h3', null, label) : headLevel(s) === 1 ? el('div', { class: 'ds-md' }, el('h1', null, s.name)) : el('h' + lvl, null, s.name), generated ? null : byChip(r)) : null,
|
|
666
702
|
P.map((p) => { const c = p.sections.find((x) => x.name === s.name); return el('div', { class: 'ds-pin', 'data-doc-pin': p.id }, el('span', null, c3ic('spark', 'sm'), (c.stale ? 'A proposal was drafted against older text here' : 'The agent proposes a change here') + ': “' + short(p.prompt) + '”'),
|
|
667
703
|
el('span', { class: 'ds-acts' }, btn('ds-btn', { 'data-doc-preview': p.id }, S.preview[p.id] ? 'Show current' : 'Show change', () => { S.preview[p.id] = !S.preview[p.id]; draw(); }),
|
|
668
704
|
btn('ds-btn ag', {}, 'Review', () => { S.openProp = p.id; go('ask'); }))); }),
|
|
669
705
|
flags[s.name] ? el('div', { class: 'fold w', role: 'note' }, c3ic('alert', 'sm'), flags[s.name]) : null,
|
|
670
706
|
content, secExtra ? secExtra(s.name) : null);
|
|
671
|
-
}
|
|
707
|
+
};
|
|
708
|
+
const cells = R && !S.src ? [formatView({ output: R.output, name: R.name || doc.name, cycle: R.cycle, foot: R.foot, secs, md: (t) => md(t, 'ds-body'), wrap: (s, inner, label) => secCell(s, inner, label) })] : secs.map((s) => secCell(s));
|
|
672
709
|
const adds = openProps().flatMap((p) => p.sections.filter((c) => c.kind === 'added' && S.preview[p.id]).map((c) => el('div', { class: 'ds-sec a prev', 'data-dsec-new': c.name },
|
|
673
710
|
el('div', { class: 'ds-pin' }, el('span', null, c3ic('spark', 'sm'), 'The proposal adds a section: ' + c.name)), el('div', { class: 'ds-sec-h' }, el('h2', null, c.name)), md(diffMd(null, c.after), 'ds-body'))));
|
|
674
711
|
const main = [generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, staleBanner(), propsBlock(secs, false), cells, adds];
|
|
@@ -679,7 +716,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
679
716
|
|
|
680
717
|
/* ── Edit ── */
|
|
681
718
|
function startEdit() {
|
|
682
|
-
if (generated || S.text == null) { S.ed = null; return; }
|
|
719
|
+
if (generated || frozen() || S.text == null) { S.ed = null; return; }
|
|
683
720
|
S.ed = { text: S.text, base: splitDoc(S.text), front: {}, kind: 'rich', why: '', whyBad: false, node: null, ta: null, range: null, link: false };
|
|
684
721
|
}
|
|
685
722
|
function edNode() {
|
|
@@ -777,6 +814,8 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
777
814
|
}
|
|
778
815
|
function editBody() {
|
|
779
816
|
if (generated) return el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'edit' }, c3ic('lock', 'sm'), generated.why);
|
|
817
|
+
if (frozen()) return el('div', { class: 'ds', 'data-doc-frozen': '' }, el('div', { class: 'fold w', role: 'status' }, c3ic('lock', 'sm'), 'Issued reports are frozen. Start the next cycle to change it.'),
|
|
818
|
+
el('div', { class: 'ds-acts' }, btn('ds-btn pri', { 'data-doc-next': '' }, [c3ic('plus', 'sm'), 'Start the next cycle'], () => nextCycle())));
|
|
780
819
|
if (S.text == null) return readBody();
|
|
781
820
|
if (!S.ed) startEdit();
|
|
782
821
|
const E = S.ed;
|
|
@@ -815,6 +854,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
815
854
|
|
|
816
855
|
/* ── Proposals ── */
|
|
817
856
|
function askBox() {
|
|
857
|
+
if (frozen()) return el('div', { class: 'fold w', role: 'status', 'data-doc-frozen': '' }, c3ic('lock', 'sm'), 'Issued reports are frozen. Start the next cycle to ask for a change.');
|
|
818
858
|
const A = S.asking;
|
|
819
859
|
const ta = el('textarea', { id: 'dpr', placeholder: 'Tell the agent what to change, for example: add a one-paragraph summary at the top', 'aria-label': 'Request to the agent', 'data-doc-prompt': '', onInput: (e) => { S.prompt = e.target.value; } }, S.prompt);
|
|
820
860
|
const sug = [...new Set(failing().map((c) => c.fix).filter(Boolean))];
|
|
@@ -876,8 +916,8 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
876
916
|
md(c.kind === 'added' ? diffMd(null, c.after) : diffMd(c.stale ? c.current || '' : c.before, c.after)));
|
|
877
917
|
});
|
|
878
918
|
const host = el('div', { class: 'ds-slide', 'data-doc-slide': p.id });
|
|
879
|
-
if (n) mountSlide(host, { label: 'Slide to accept ' + n + ' of ' + p.sections.length, hash: p.hash, onConfirm: (h) => acceptProp(p, h) });
|
|
880
|
-
else host.append(el('div', { class: 'slt-why', role: 'note' }, c3ic('lock', 'sm'), anyStale && p.sections.every((c) => c.stale) ? 'Every section changed since this draft. Ask the agent again.' : 'Tick at least one section to accept.'));
|
|
919
|
+
if (n && !frozen()) mountSlide(host, { label: 'Slide to accept ' + n + ' of ' + p.sections.length, hash: p.hash, onConfirm: (h) => acceptProp(p, h) });
|
|
920
|
+
else host.append(el('div', { class: 'slt-why', role: 'note' }, c3ic('lock', 'sm'), frozen() ? 'Issued reports are frozen. Start the next cycle to accept a change.' : anyStale && p.sections.every((c) => c.stale) ? 'Every section changed since this draft. Ask the agent again.' : 'Tick at least one section to accept.'));
|
|
881
921
|
const rej = S.rejFor === p.id
|
|
882
922
|
? el('div', { class: 'ds-whyadd' }, el('input', { type: 'text', id: 'drejwhy', 'data-doc-rejwhy': '', placeholder: 'Why are you rejecting it? Kept in the edit log.', 'aria-label': 'Why reject', value: S.rejWhy, onInput: (e) => { S.rejWhy = e.target.value; } }),
|
|
883
923
|
btn('ds-btn pri', { 'data-doc-rejok': p.id }, 'Reject', () => rejectProp(p)), btn('ds-btn', {}, 'Cancel', () => { S.rejFor = null; draw(); }))
|
|
@@ -1013,15 +1053,65 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
1013
1053
|
el('p', { class: 'ds-note' }, 'Health answers one question: can I trust this document without rereading it? The checks run again every time the text changes.'));
|
|
1014
1054
|
}
|
|
1015
1055
|
|
|
1056
|
+
/* ── a report cycle: Issue, the menu (Download, Start the next cycle) ── */
|
|
1057
|
+
const toolsNode = R ? el('span', { class: 'rs-tools' }) : null;
|
|
1058
|
+
const slugOf = (t) => String(t || 'report').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
1059
|
+
function menuRow() {
|
|
1060
|
+
const items = downloadsOf(R.output);
|
|
1061
|
+
return el('div', { class: 'chips2 rs-menu', role: 'menu', 'aria-label': 'Document actions', 'data-doc-menu': '' }, el('span', { class: 'dim mono ds-mono' }, 'DOWNLOAD'),
|
|
1062
|
+
items.map((it) => pb('outline', { 'data-doc-download': it.fmt, role: 'menuitem' }, [c3ic('drop', 'sm'), it.label], () => download(it))),
|
|
1063
|
+
frozen() ? pb('ghost', { 'data-doc-next': '', role: 'menuitem' }, [c3ic('plus', 'sm'), 'Start the next cycle'], () => nextCycle()) : null);
|
|
1064
|
+
}
|
|
1065
|
+
async function download(it) {
|
|
1066
|
+
S.menu = false; draw();
|
|
1067
|
+
const title = R.name || doc.name; const secs = S.text != null ? splitDoc(S.text) : [];
|
|
1068
|
+
if (it.fmt === 'pdf') {
|
|
1069
|
+
if (!printPdf({ output: R.output, name: title, cycle: R.cycle, secs })) say('The browser blocked the print window. Allow pop-ups for this page, then press Download again.');
|
|
1070
|
+
else say('The print view is open. Choose Save as PDF. Nothing is uploaded.');
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
const r = await fetch('/api/report-export?' + q({ ws, path: doc.path, fmt: it.fmt }));
|
|
1075
|
+
if (!r.ok) { let m = ''; try { m = (await r.json()).error; } catch { /* not JSON */ } throw new Error(m || 'the workspace did not make the file'); }
|
|
1076
|
+
const blob = await r.blob();
|
|
1077
|
+
const cd = /filename="?([^";]+)"?/.exec(r.headers.get('content-disposition') || '');
|
|
1078
|
+
const name = cd ? cd[1] : slugOf(title + ' ' + (R.cycle || '')) + '.' + it.fmt;
|
|
1079
|
+
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; document.body.appendChild(a); a.click(); a.remove();
|
|
1080
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 4000);
|
|
1081
|
+
say('Downloaded ' + name + '. Nothing went to another tool.');
|
|
1082
|
+
} catch (e) { say('The file was not made: ' + ((e && e.message) || 'no answer') + '.'); }
|
|
1083
|
+
}
|
|
1084
|
+
async function issue() {
|
|
1085
|
+
/* Issue is the PM's act on the existing route (routes-reports.js, requirePmPress). It freezes the bytes the PM read:
|
|
1086
|
+
the sha comes from the file as served, and the page refuses when that text is not the text on screen. */
|
|
1087
|
+
const m = /reports\/([a-z][a-z0-9-]{0,39})\/(\d{4}-\d{2}-\d{2})\.md$/.exec(doc.path || '');
|
|
1088
|
+
if (!m) { say('This document is not a report cycle, so it cannot be issued.'); return; }
|
|
1089
|
+
const cur = await pmGet('/api/report/draft?' + q({ ws, cadence: m[1], slot: m[2] })).catch(() => null);
|
|
1090
|
+
if (!cur || !cur.ok || !cur.exists || !cur.sha) { say((cur && cur.error) || 'The report was not issued: the file could not be read.'); return; }
|
|
1091
|
+
if (S.text != null && cur.markdown != null && cur.markdown !== S.text) { await reloadAll(); say('The file changed since you opened it. Read it again, then press Issue.'); return; }
|
|
1092
|
+
const r = await pmPost('/api/report/issue', { ws, cadence: m[1], slot: m[2], sha: cur.sha }).catch(() => null);
|
|
1093
|
+
if (!r || !r.ok) { say((r && r.error) || 'The report was not issued.'); return; }
|
|
1094
|
+
R.state = 'issued'; doc.st = 'issued'; S.changed = true; S.menu = false; if (S.mode === 'edit') { S.ed = null; S.mode = 'read'; }
|
|
1095
|
+
if (R.onIssue) R.onIssue(doc);
|
|
1096
|
+
await reloadAll(); say('Issued and frozen. PMOS sends nothing; share it yourself.');
|
|
1097
|
+
}
|
|
1098
|
+
function nextCycle() { S.menu = false; if (R.onNext) R.onNext(doc, ui); else say('The next cycle starts on its schedule. PMOS drafts it then.'); }
|
|
1099
|
+
function drawTools() {
|
|
1100
|
+
if (!toolsNode) return;
|
|
1101
|
+
toolsNode.replaceChildren(...[R.state === 'draft' ? pb('outline', { 'data-doc-issue': '' }, [c3ic('check', 'sm'), 'Issue'], () => issue()) : null,
|
|
1102
|
+
btn('ib sm', { 'aria-label': 'Download and more', title: 'Download and more', 'aria-expanded': String(S.menu), 'data-doc-more': '' }, c3ic('more', 'sm'), () => { S.menu = !S.menu; draw(); })].filter(Boolean));
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1016
1105
|
/* a load that finished in the background must not redraw under a hand that is typing */
|
|
1017
1106
|
const drawBg = () => { const a = document.activeElement; if (ui && a && ui.body.contains(a) && a.closest('input,textarea,[contenteditable=true]')) { refreshTop(); return; } draw(); };
|
|
1018
|
-
const top = () => el('div', { class: 'ds-top' }, head(), seg(), S.mode === 'edit' && S.ed ? toolbar() : null,
|
|
1107
|
+
const top = () => el('div', { class: 'ds-top' }, head(), seg(), R && S.menu ? menuRow() : null, S.mode === 'edit' && S.ed ? toolbar() : null,
|
|
1019
1108
|
S.leave ? el('div', { class: 'fold w', role: 'alert', 'data-doc-leave': '' }, c3ic('alert', 'sm'), 'You have unsaved changes in ' + plural(dirty().length, 'section') + '. ',
|
|
1020
1109
|
btn('ds-btn', {}, 'Keep editing', () => { S.leave = null; drawEdit(); }), btn('ds-btn', { 'data-doc-discard': '' }, 'Discard and leave', () => { const m = S.leave; S.ed = null; S.leave = null; go(m); })) : null);
|
|
1021
1110
|
function refreshTop() { const t = ui && ui.body.querySelector('.ds-top'); if (t) t.replaceWith(top()); }
|
|
1022
1111
|
function draw() {
|
|
1023
1112
|
if (!ui) return;
|
|
1024
1113
|
if (S.mode === 'edit' && S.ed && S.ed.kind === 'rich' && S.ed.node) keepRange();
|
|
1114
|
+
drawTools();
|
|
1025
1115
|
const body = S.mode === 'read' ? readBody() : S.mode === 'edit' ? editBody() : S.mode === 'ask' ? propsBody() : S.mode === 'hist' ? logBody() : S.mode === 'health' ? healthBody() : linksBody();
|
|
1026
1116
|
const keep = ui.body.querySelector('textarea:focus,input[type=text]:focus');
|
|
1027
1117
|
const had = keep ? { id: keep.id, s: keep.selectionStart } : null;
|
|
@@ -1036,7 +1126,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
1036
1126
|
}
|
|
1037
1127
|
|
|
1038
1128
|
document.addEventListener('selectionchange', keepRange);
|
|
1039
|
-
ui = openSheet({ title: doc.name, icon: 'doc', dw: true, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; document.removeEventListener('selectionchange', keepRange); if (S.changed && onChanged) onChanged(); } });
|
|
1129
|
+
ui = openSheet({ title: doc.name, icon: 'doc', dw: true, actions: toolsNode, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; document.removeEventListener('selectionchange', keepRange); if (S.changed && onChanged) onChanged(); } });
|
|
1040
1130
|
S.open = (d) => { ui.close(); if (S.openOther) S.openOther(d); else openDocSheet({ ws, doc: d, library, openGraph }); };
|
|
1041
1131
|
ui.setOpener = (fn) => { S.openOther = fn; };
|
|
1042
1132
|
draw();
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - the portfolio: the headline joins the breadcrumb, an empty triage card is not shown, the timeline folds;
|
|
9
9
|
* - empty chips ("No owner", "0 epics", ...) are not drawn;
|
|
10
10
|
* - Calendar: the layer chips sit behind one Layers button, the Detail card shows once a meeting is picked;
|
|
11
|
-
* - Work: the lenses sit beside Add work; Reports:
|
|
11
|
+
* - Work: the lenses sit beside Add work; Reports: Send this as a report and Edit dashboard join the tab row.
|
|
12
12
|
* Desktop only: below 601px the page keeps today's layout.
|
|
13
13
|
* The views draw their own markup; this module only moves and hides it, so every press still goes to the view's own handler.
|
|
14
14
|
* It runs again, debounced, whenever the views redraw (one MutationObserver on the root). */
|
|
@@ -144,7 +144,7 @@ function apply(root) {
|
|
|
144
144
|
const add = w.querySelector(':scope > .c3add'), bar = w.querySelector(':scope > .calbar'), l = bar && bar.querySelector('[data-pm-lenses]');
|
|
145
145
|
if (add && l && l.parentElement === bar) { for (const o of add.querySelectorAll('[data-pm-lenses]')) o.remove(); add.appendChild(l); bar.classList.add('qt-moved'); }
|
|
146
146
|
}
|
|
147
|
-
// Reports:
|
|
147
|
+
// Reports: Send this as a report and Edit dashboard on the tab row.
|
|
148
148
|
for (const r of root.querySelectorAll('[data-rp3]')) {
|
|
149
149
|
const scope = r.querySelector(':scope > .calbar .rp3-scope'), acts = r.querySelector('.rp3-head > .chips2');
|
|
150
150
|
if (scope && acts) { for (const o of scope.querySelectorAll('.qt-acts')) o.remove(); acts.classList.add('qt-acts'); scope.appendChild(acts); }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* The printable report (Download > PDF): its own window, printed by the browser. Colours are the slide tokens of
|
|
2
|
+
tokens.css (linked first), so the paper reads the same in either theme. Slides are 16:9 landscape cards (.rp-slide),
|
|
3
|
+
everything else is A4. */
|
|
4
|
+
@page { size: A4; margin: 16mm; }
|
|
5
|
+
.rp-print { margin: 0; font-family: var(--sans); font-size: 14px; line-height: 1.5; color: var(--slide-fg); background: var(--slide-bg); }
|
|
6
|
+
.rp-print h1 { font-size: 20px; margin: 0 0 4mm; }
|
|
7
|
+
.rp-print h2 { font-size: 16px; margin: 6mm 0 2mm; color: var(--slide-brand); }
|
|
8
|
+
.rp-print p, .rp-print ul { margin: 0 0 3mm; }
|
|
9
|
+
.rp-print table { border-collapse: collapse; width: 100%; margin: 0 0 3mm; }
|
|
10
|
+
.rp-print th, .rp-print td { border: 1px solid var(--slide-border); padding: 2mm 3mm; text-align: left; font-size: 12px; }
|
|
11
|
+
.rp-print th { background: var(--slide-secondary); }
|
|
12
|
+
.rp-print .rp-print-m { color: var(--slide-muted); }
|
|
13
|
+
.rp-print-slides { margin: 0; }
|
|
14
|
+
@page slides { size: 297mm 167mm; margin: 0; }
|
|
15
|
+
.rp-slide { page: slides; page-break-after: always; width: 297mm; height: 167mm; box-sizing: border-box; padding: 14mm; font-size: 20px; }
|
|
16
|
+
.rp-slide h2 { margin-top: 0; font-size: 24px; }
|
|
17
|
+
.rp-slide li { margin-bottom: 2mm; }
|
|
18
|
+
.rp-slide table { font-size: 16px; }
|
|
19
|
+
.rp-slide--title { background: var(--slide-brand); color: var(--slide-bg); }
|
|
20
|
+
.rp-slide--title h2 { color: var(--slide-bg); }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* pm/c3-report-setup.css — the Reports tab (Ask PMOS, Found in your work, Set up, Documents), the "Send this as a report" sheet, the Briefing card, the Settings
|
|
2
|
+
* block and the report formats of the document sheet (initiative report-workspace). The mockup's 41-reportsetup.css under real class names: .mk-rs is .rs,
|
|
3
|
+
* .mk-rs-* is .rs-*, .mk-kv is .rs-kv. Tokens only. Corners: 6px on a control or a card drawn as paper, 8px on a container, else 0.
|
|
4
|
+
* c3-report-setup.js and c3-doc-sheet.js link it once. The slide colours are the --slide-* tokens of tokens.css. */
|
|
5
|
+
/* in .brief a .nrow has two columns, so the row buttons sit under the text */
|
|
6
|
+
.c3 .rs .nrow > .acts { grid-column: 2; justify-content: flex-start; display: flex; gap: 6px; flex-wrap: wrap; }
|
|
7
|
+
.c3 .rs, .c3 .rs > * { min-width: 0; }
|
|
8
|
+
.c3 .rs .nrow .chips .chip2 { max-width: 100%; }
|
|
9
|
+
.c3 .rs .form { margin: 0; }
|
|
10
|
+
.c3 .rs .seg { max-width: 100%; overflow-x: auto; }
|
|
11
|
+
.c3 .rs .seg button { white-space: nowrap; }
|
|
12
|
+
.c3 [data-rs-finder] .nrow > .acts { grid-column: 2; justify-content: flex-start; display: flex; gap: 6px; flex-wrap: wrap; }
|
|
13
|
+
.c3 [data-rs-finder] .seg { max-width: 100%; overflow-x: auto; }
|
|
14
|
+
.c3 .rs .lh { grid-template-columns: auto minmax(0, 1fr); }
|
|
15
|
+
.c3 .rs .lh > .chips2 { grid-column: 1 / -1; }
|
|
16
|
+
.c3 .rs-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 12px; margin: 0; font-size: var(--fs-2); }
|
|
17
|
+
.c3 .rs-kv dt { font: var(--fs-1) var(--mono); color: var(--muted-foreground); text-transform: uppercase; letter-spacing: .06em; padding-top: 2px; }
|
|
18
|
+
.c3 .rs-kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
19
|
+
.c3 .rs-kv input[type="text"] { width: 100%; box-sizing: border-box; }
|
|
20
|
+
.c3 .rs-kv ol { margin: 0; padding-left: 18px; }
|
|
21
|
+
.c3 .rs-lbl { font: var(--fs-1) var(--mono); color: var(--muted-foreground); }
|
|
22
|
+
.c3 .rs-help { font-size: var(--fs-2); }
|
|
23
|
+
.c3 .rs-stack { display: grid; gap: var(--sp-2); }
|
|
24
|
+
.c3 .rs-tools { display: inline-flex; gap: 6px; align-items: center; }
|
|
25
|
+
.c3 .rs-menu { align-items: center; }
|
|
26
|
+
@media (max-width: 560px) { .c3 .rs-kv { grid-template-columns: minmax(0, 1fr); } }
|
|
27
|
+
|
|
28
|
+
/* Output formats: the document drawn as slides, a grid, a doc page or A4 pages. Paper colours are the slide tokens. */
|
|
29
|
+
.c3 .rs-slides { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: var(--sp-3); }
|
|
30
|
+
.c3 .rs-slides > .ds-sec { margin-left: 0; }
|
|
31
|
+
.c3 .rs-slide { aspect-ratio: 16 / 9; min-width: 0; display: grid; grid-template-rows: auto 1fr auto; gap: var(--sp-2); padding: var(--sp-3); background: var(--slide-bg); color: var(--slide-fg); border: 1px solid var(--slide-border); border-radius: 6px; font-size: var(--fs-2); overflow: hidden; }
|
|
32
|
+
.c3 .rs-sh { font-weight: 600; font-size: var(--fs-3); color: var(--slide-brand); overflow-wrap: anywhere; }
|
|
33
|
+
.c3 .rs-slide.title { background: var(--slide-brand); color: var(--slide-bg); }
|
|
34
|
+
.c3 .rs-slide.title .rs-sh { color: var(--slide-bg); font-size: var(--fs-4); }
|
|
35
|
+
.c3 .rs-slide .m, .c3 .rs-page .m { color: var(--slide-muted); margin: 0; }
|
|
36
|
+
.c3 .rs-slide.title .m { color: var(--slide-border); }
|
|
37
|
+
.c3 .rs-slide p, .c3 .rs-slide .ds-md { margin: 0; font-size: var(--fs-2); color: inherit; }
|
|
38
|
+
.c3 .rs-slide ul, .c3 .rs-page ul { margin: 0; padding-left: 18px; display: grid; gap: 4px; }
|
|
39
|
+
.c3 .rs-sf { display: flex; justify-content: space-between; font: var(--fs-1) var(--mono); color: var(--slide-foot); }
|
|
40
|
+
.c3 .rs-big { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
|
|
41
|
+
.c3 .rs-big b { display: block; font-size: var(--fs-4); color: var(--slide-primary); }
|
|
42
|
+
.c3 .rs-big span { color: var(--slide-muted); font-size: var(--fs-1); }
|
|
43
|
+
.c3 .rs-sheets { display: grid; gap: var(--sp-3); }
|
|
44
|
+
.c3 .rs-gridwrap { max-width: 100%; overflow-x: auto; border: 1px solid var(--slide-border); border-radius: 6px; background: var(--slide-bg); }
|
|
45
|
+
.c3 .rs-grid { border-collapse: collapse; min-width: 640px; width: 100%; color: var(--slide-fg); font-size: var(--fs-2); }
|
|
46
|
+
.c3 .rs-grid th, .c3 .rs-grid td { border: 1px solid var(--slide-border); padding: 4px 8px; text-align: left; }
|
|
47
|
+
.c3 .rs-grid thead th, .c3 .rs-grid tbody th { background: var(--slide-secondary); color: var(--slide-muted); font: var(--fs-1) var(--mono); text-align: center; }
|
|
48
|
+
.c3 .rs-grid tr.hd td { font-weight: 600; background: var(--slide-track); }
|
|
49
|
+
.c3 .rs-grid td.n { text-align: right; font-family: var(--mono); }
|
|
50
|
+
.c3 .rs-grid td.pos { color: var(--slide-done); }
|
|
51
|
+
.c3 .rs-grid td.neg { color: var(--slide-overdue); }
|
|
52
|
+
.c3 .rs-page { background: var(--slide-bg); color: var(--slide-fg); border: 1px solid var(--slide-border); border-radius: 6px; padding: var(--sp-4); display: grid; gap: var(--sp-2); align-content: start; min-width: 0; font-size: var(--fs-3); }
|
|
53
|
+
.c3 .rs-page h3 { margin: 0; font-size: var(--fs-5); }
|
|
54
|
+
.c3 .rs-page p { margin: 0; }
|
|
55
|
+
.c3 .rs-page .ds-md { color: inherit; }
|
|
56
|
+
.c3 .rs-page .ds-sec-h h3 { color: var(--slide-brand); }
|
|
57
|
+
.c3 .rs-pages { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
|
|
58
|
+
.c3 .rs-page.a4 { aspect-ratio: 1 / 1.414; grid-template-rows: auto; align-content: start; position: relative; }
|
|
59
|
+
.c3 .rs-pn { position: absolute; bottom: var(--sp-2); right: var(--sp-3); font: var(--fs-1) var(--mono); color: var(--slide-foot); }
|
|
60
|
+
|
|
61
|
+
.c3 .rs-min0 { min-width: 0; }
|
|
62
|
+
.c3 .rs-kv .chip2 + .rs-help { margin-left: 6px; }
|