create-pmos 0.196.0 → 0.198.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.
Files changed (50) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-ib-items-test.js +16 -8
  7. package/workspace/c3-ib-more-test.js +3 -2
  8. package/workspace/c3-ib-triage-backlog-test.js +1 -1
  9. package/workspace/c3-quiet-test.js +4 -3
  10. package/workspace/c3-report-design-real-test.js +225 -0
  11. package/workspace/c3-report-design-test.js +403 -0
  12. package/workspace/c3-report-setup-test.js +3 -2
  13. package/workspace/c3-work-test.js +1 -1
  14. package/workspace/guide/index.md +1 -0
  15. package/workspace/guide/initiative-boards.md +9 -5
  16. package/workspace/guide/report-design.md +104 -0
  17. package/workspace/guide/report-slides.md +3 -1
  18. package/workspace/guide/reports-set-up.md +2 -1
  19. package/workspace/lib/headless.js +24 -6
  20. package/workspace/lib/report-design.js +741 -0
  21. package/workspace/lib/report-draft.js +8 -3
  22. package/workspace/lib/report-setups.js +1 -1
  23. package/workspace/lib/route-modules.js +1 -0
  24. package/workspace/lib/routes-doc-sheet.js +8 -1
  25. package/workspace/lib/routes-report-design.js +481 -0
  26. package/workspace/lib/routes-reports.js +2 -1
  27. package/workspace/package-lock.json +2 -2
  28. package/workspace/package.json +1 -1
  29. package/workspace/public/pm/c3-demo-rd.js +426 -0
  30. package/workspace/public/pm/c3-demo-rep2.js +2 -0
  31. package/workspace/public/pm/c3-demo-rs.js +5 -1
  32. package/workspace/public/pm/c3-design-studio.js +308 -0
  33. package/workspace/public/pm/c3-doc-formats.js +3 -2
  34. package/workspace/public/pm/c3-doc-sheet.js +30 -5
  35. package/workspace/public/pm/c3-ib-items.js +26 -17
  36. package/workspace/public/pm/c3-ib-releases.js +4 -7
  37. package/workspace/public/pm/c3-ib-sprints.js +5 -8
  38. package/workspace/public/pm/c3-ib-triage.js +4 -9
  39. package/workspace/public/pm/c3-ini-life.css +2 -0
  40. package/workspace/public/pm/c3-initiatives.js +2 -1
  41. package/workspace/public/pm/c3-quiet.css +1 -1
  42. package/workspace/public/pm/c3-quiet.js +4 -5
  43. package/workspace/public/pm/c3-report-deck.css +139 -0
  44. package/workspace/public/pm/c3-report-deck.js +558 -0
  45. package/workspace/public/pm/c3-report-frame.css +20 -0
  46. package/workspace/public/pm/c3-report-setup.js +18 -6
  47. package/workspace/server.js +17 -5
  48. package/workspace/skills/report-design.skill +61 -0
  49. package/workspace/test-lists/report-design-backend.json +5 -0
  50. package/workspace/test-lists/report-design-studio-ui.json +5 -0
@@ -0,0 +1,308 @@
1
+ /* pm/c3-design-studio.js — the DESIGN STUDIO and the saved DESIGN BRIEF of a report (initiative report-design-studio; PRD sections 1 and 2;
2
+ * mockup 42-reportdeck.js, hashes #design, #design-point, #design-compare, #design-doc, #design-saved).
3
+ *
4
+ * openStudio({ ws, id, name, view, onClose }) the wide sheet: the conversation on the left (describe, add samples, turns, the PMOS lines
5
+ * that name each version), the live result on the right (the version strip, Compare two,
6
+ * go back to a version, point and say), the Deck / Document switch, This is it, Design brief
7
+ * openBrief({ ws, id, name }) the saved design in the report setup sheet: the description, the samples, the reference
8
+ * version, the rules PMOS wrote, and the cycles drafted to match it
9
+ * Entry points: "Design how it looks" on a setup row and "Refine the design" in a report document (c3-report-setup.js, c3-report-deck.js).
10
+ * A turn starts ONE report-design job on the server (POST /api/report-design/turn); the page shows it running and reads GET /api/report-design
11
+ * until the version lands. The page can be left: the job goes on. Nothing here sends anything outside the workspace. */
12
+ import { el } from './dom.js';
13
+ import { icon, pbtn, btn, chip, sect, put, lk } from './c3-ini-ui.js';
14
+ import { openSheet, closeSheet } from './c3-sheet.js';
15
+ import { toast } from './c3-toast.js';
16
+ import { RS, scheduleLabel } from './c3-report-store.js';
17
+ import { formatOf } from './c3-doc-formats.js';
18
+ import { RD, failed, splitRender, slideCard, frame, wireFrame, diffOutlines, ensureDeckCss, slotWord } from './c3-report-deck.js';
19
+
20
+ const MAX = 20 * 1024 * 1024;
21
+ const KIND_OF = (name) => { const e = String(name || '').toLowerCase().split('.').pop(); return e === 'pdf' ? 'pdf' : e === 'pptx' ? 'pptx' : e === 'docx' ? 'docx' : /^(png|jpe?g)$/.test(e) ? 'image' : ''; };
22
+ const SICON = { pdf: 'pdf', pptx: 'img', docx: 'doc', image: 'img', link: 'link', 'pmos-doc': 'doc' };
23
+ const SWORD = { pdf: 'Upload, PDF', pptx: 'Upload, .pptx', docx: 'Upload, .docx', image: 'Upload, image', link: 'Link', 'pmos-doc': 'Past PMOS document' };
24
+ const hhmm = (iso) => { const m = /T(\d{2}):(\d{2})/.exec(String(iso || '')); return m ? m[1] + ':' + m[2] : ''; };
25
+ const dayAt = (iso) => { const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})/.exec(String(iso || '')); return m ? slotWord(m[1]) + ', ' + m[2] + ':' + m[3] : String(iso || ''); };
26
+ const short = (t, n = 60) => { const s = String(t || '').replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
27
+ const hint = (k) => (/headline|metric/.test(k) ? 'Hover a value for its detail. Click a number to see its source.' : /list/.test(k) ? 'Click a status to see where it comes from.' : 'Click any part of the preview to attach a request to it.');
28
+
29
+ /* one studio session per report setup: what the PM points at waits here until the next turn */
30
+ const LOCAL = {};
31
+ const local = (id) => LOCAL[id] || (LOCAL[id] = { view: null, cmp: false, cmpA: null, sidx: 0, point: true, markers: [], draft: null, mOpen: null, panel: false, text: '', fmt: null });
32
+
33
+ function sampleChip(s) {
34
+ const th = s.thumb && /^data:image\//.test(s.thumb) ? el('img', { class: 'rd-dks-thumb img', src: s.thumb, alt: '' }) : el('span', { class: 'rd-dks-thumb ' + (s.kind === 'pptx' || s.kind === 'image' ? 'deck' : 'page'), 'aria-hidden': 'true' }, el('i'), el('i'), el('i'));
35
+ return el('div', { class: 'rd-dks-smp', 'data-rd-sample': s.id }, th, el('div', null, el('b', null, icon(SICON[s.kind] || 'doc', 'sm'), s.name), el('span', { class: 'dim mono' }, SWORD[s.kind] || s.kind),
36
+ s.readable === false ? el('p', { class: 'rd-warn' }, 'PMOS could not read it with your tools. It is kept, but it does not shape the design.')
37
+ : el('p', null, el('span', { class: 'dim' }, 'PMOS took: '), s.took || 'PMOS reads it on the next turn.')));
38
+ }
39
+
40
+ /* ── the studio ── */
41
+ export function openStudio({ ws, id, name, view, onClose } = {}) {
42
+ ensureDeckCss();
43
+ const K = local(id); if (view != null) { K.view = view; K.cmp = false; }
44
+ const S = { D: null, setup: null, err: '', cache: {}, timer: null, closed: false, busy: false };
45
+ const body = el('div', { class: 'brief rs rd-dk rd-dks-wrap', 'data-rd-studio': id });
46
+ const ui = openSheet({ title: 'Design studio', icon: 'spark', wide: true, dw: true, cls: 'rd-wide', body, onClose: () => { S.closed = true; clearTimeout(S.timer); if (onClose) onClose(); } });
47
+
48
+ async function load(keepView) {
49
+ const r = await RD.design(ws, id);
50
+ if (!r || r.ok === false) { S.err = (r && r.error) || 'The design could not be read.'; paint(); return; }
51
+ S.D = r.design; S.setup = r.setup || { id, name };
52
+ if (!K.fmt || !S.D.versions.length) K.fmt = S.D.format || 'deck';
53
+ if (!keepView || K.view == null || !S.D.versions.some((v) => v.n === K.view)) K.view = S.D.current;
54
+ if (S.D.running) poll(); else clearTimeout(S.timer);
55
+ if (K.view != null) await render(K.view);
56
+ if (K.cmp) await render(cmpA());
57
+ paint();
58
+ }
59
+ async function render(n) {
60
+ if (n == null || S.cache[n]) return;
61
+ try { S.cache[n] = splitRender(await RD.version(ws, id, n)); } catch (e) { const style = ''; S.cache[n] = { err: (e && e.message) || 'not readable', slides: [], secs: [], style, body: '' }; }
62
+ }
63
+ function poll() { clearTimeout(S.timer); S.timer = setTimeout(async () => { if (S.closed) return; const was = S.D && S.D.current; await load(false); if (S.D && !S.D.running && S.D.current !== was) { K.markers = []; K.draft = null; scrollLog(); } }, 1500); }
64
+ const ver = (n) => (S.D.versions || []).find((v) => v.n === n);
65
+ const cmpA = () => { const b = S.D.current; const a = K.cmpA != null && K.cmpA !== b && ver(K.cmpA) ? K.cmpA : (S.D.versions.filter((v) => v.n < b).pop() || S.D.versions.find((v) => v.n !== b) || {}).n; return a; };
66
+ const scrollLog = () => requestAnimationFrame(() => { const l = body.querySelector('.rd-dks-log'); if (l) l.scrollTop = l.scrollHeight; });
67
+ const vlabel = (v) => (v ? (v.turn ? short(v.turn, 48) : v.n === 1 ? 'From your description' : 'Your turn') : '');
68
+
69
+ /* ── the conversation ── */
70
+ function msg(l) {
71
+ const you = l.who === 'you';
72
+ const sm = l.kind === 'sample' ? (S.D.samples || []).filter((s) => (l.sample ? s.id === l.sample : String(l.text || '').includes(s.name))) : [];
73
+ const v = l.version != null ? ver(l.version) : null;
74
+ return el('li', { class: 'rd-dks-msg ' + (you ? 'you' : 'pmos') + (l.kind === 'error' ? ' err' : ''), 'data-rd-log': l.kind },
75
+ el('span', { class: 'ds-av ' + (you ? 'h' : l.kind === 'error' ? 'w' : 'a') }, you ? icon('user', 'sm') : icon(l.kind === 'error' ? 'alert' : 'spark', 'sm')),
76
+ el('div', { class: 'bub' }, el('div', { class: 'who' }, you ? 'You' : 'PMOS'), l.text && !(l.kind === 'sample' && sm.length) ? el('p', null, l.text) : null, sm.map(sampleChip),
77
+ l.points && l.points.length ? chip(icon('target', 'sm'), 'Pointed at: marker ' + l.points.map((p) => p.n).join(', ') + ' on the preview') : null,
78
+ v ? lk({ class: 'rd-dks-ver', 'data-rd-ver': String(v.n), 'aria-pressed': String(K.view === v.n && !K.cmp), onclick: () => look(v.n) }, icon('img', 'sm'), 'D' + v.n + ': ' + vlabel(v)) : null));
79
+ }
80
+ function runningMsg() {
81
+ const n = (S.D.versions.length ? Math.max(...S.D.versions.map((v) => v.n)) : 0) + 1;
82
+ return el('li', { class: 'rd-dks-msg pmos', 'data-rd-running': '' }, el('span', { class: 'ds-av a' }, icon('spark', 'sm')),
83
+ el('div', { class: 'bub' }, el('div', { class: 'who' }, 'PMOS'), el('p', null, 'Making D' + n + ' from this cycle\'s data. This takes a minute or two. You can close the studio; the version lands here when the job ends.')));
84
+ }
85
+ function panel() {
86
+ if (!K.panel) return null;
87
+ const file = el('input', { type: 'file', class: 'rd-hide', accept: '.pdf,.pptx,.docx,.png,.jpg,.jpeg', 'data-rd-file': '', onChange: (e) => upload(e.target.files && e.target.files[0]) });
88
+ const link = el('input', { type: 'text', 'data-rd-link': '', placeholder: 'Paste a Google Drive or Confluence link', 'aria-label': 'Link to a sample', onKeyDown: (e) => { if (e.key === 'Enter') addLink(); } });
89
+ const docs = (RS.docs || []).filter((d) => d.setup !== id || d.state === 'issued').slice(0, 3);
90
+ return el('div', { class: 'rd-dks-panel', 'data-rd-panel': '' }, el('div', { class: 'ds-h' }, 'Add a sample'),
91
+ btn({ class: 'rd-dks-opt', 'data-rd-smp': 'file', onclick: () => file.click() }, icon('drop', 'sm'), el('span', null, 'Upload a past deck or document', el('br'), el('span', { class: 'dim rd-small' }, 'PDF, .pptx, .docx or a screenshot, up to 20 MB'))), file,
92
+ el('div', { class: 'ds-whyadd' }, link, btn({ class: 'ds-btn', 'data-rd-smp': 'link', onclick: () => addLink() }, 'Add')),
93
+ docs.map((d) => btn({ class: 'rd-dks-opt', 'data-rd-smp': 'doc', 'data-v': d.path, onclick: () => addDoc(d) }, icon('doc', 'sm'), el('span', null, 'Pick a past PMOS document', el('br'), el('span', { class: 'dim rd-small' }, d.name + ', ' + slotWord(d.slot))))),
94
+ el('div', { class: 'dim rd-small' }, 'PMOS reads a sample to learn how it looks. It does not copy its numbers. A link is read only if your tools can open it.'));
95
+ }
96
+ async function sampleDone(r) {
97
+ if (!r || !r.ok) { failed(r, 'The sample was not added'); return; }
98
+ K.panel = false; await load(true); scrollLog(); toast('Added the sample. Say what to take from it, then make the next version.');
99
+ }
100
+ function upload(f) {
101
+ if (!f) return;
102
+ const kind = KIND_OF(f.name); if (!kind) { toast({ text: 'PMOS reads PDF, .pptx, .docx, PNG or JPG files.', tone: 'error' }); return; }
103
+ if (f.size > MAX) { toast({ text: 'The file is larger than 20 MB. Add a smaller copy, or a link.', tone: 'error' }); return; }
104
+ const rd = new FileReader();
105
+ rd.onload = async () => { const data = String(rd.result || '').replace(/^data:[^,]*,/, ''); sampleDone(await RD.sample({ ws, id, name: f.name, kind, data })); };
106
+ rd.readAsDataURL(f);
107
+ }
108
+ async function addLink() { const i = body.querySelector('[data-rd-link]'); const v = i && i.value.trim(); if (!v) { if (i) i.focus(); return; } sampleDone(await RD.sample({ ws, id, link: v })); }
109
+ async function addDoc(d) { sampleDone(await RD.sample({ ws, id, doc: d.path })); }
110
+
111
+ function chat() {
112
+ const D = S.D; const first = !D.versions.length;
113
+ const ta = el('textarea', { 'data-rd-in': '', 'aria-label': first ? 'Describe the report' : 'Tell PMOS what to change',
114
+ placeholder: first ? 'Describe the report in plain words: who reads it, what they need to see first, what must stand out.' : K.fmt === 'deck' ? 'Tell PMOS what to change, for example: denser, like slide 3 of my sample' : 'Tell PMOS what to change, for example: put the chart before the summary',
115
+ onInput: (e) => { K.text = e.target.value; }, onKeyDown: (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); send(); } } });
116
+ ta.value = K.text;
117
+ return el('div', { class: 'rd-dks-chat' }, el('div', { class: 'rd-dks-desc' }, el('span', { class: 'dim mono' }, 'DESCRIBE, ADD SAMPLES, REFINE'),
118
+ (D.samples || []).length ? el('div', { class: 'chips' }, D.samples.map((s) => chip(icon(SICON[s.kind] || 'doc', 'sm'), s.name))) : null),
119
+ el('ul', { class: 'rd-dks-log', 'data-rd-chat': '' }, (D.log || []).map(msg), D.running ? runningMsg() : null,
120
+ !(D.log || []).length && !D.running ? el('li', { class: 'fold' }, icon('info', 'sm'), ' Start with who reads the report and what they need. Add a past deck or document if you have one.') : null),
121
+ panel(),
122
+ el('div', { class: 'rd-dks-in' }, ta, K.markers.length ? el('div', { class: 'chips' }, chip(icon('target', 'sm'), K.markers.length + (K.markers.length === 1 ? ' request' : ' requests') + ' on the preview go with this turn')) : null,
123
+ el('div', { class: 'ds-acts rd-between' }, btn({ class: 'ds-btn', 'data-rd-act': 'panel', 'aria-expanded': String(K.panel), onclick: () => { K.panel = !K.panel; paint(); } }, icon('plus', 'sm'), 'Add a sample'),
124
+ btn({ class: 'ds-btn ag', 'data-rd-act': 'send', disabled: !!D.running || S.busy, onclick: () => send() }, icon('spark', 'sm'), first ? 'Make the first version' : 'Make the next version')),
125
+ el('div', { class: 'dim rd-small' }, 'Each turn makes a new version. The data stays this cycle\'s; only how it looks changes.')));
126
+ }
127
+ async function send(textIn) {
128
+ const D = S.D; const i = body.querySelector('[data-rd-in]');
129
+ let text = String(textIn != null ? textIn : (i ? i.value : K.text) || '').trim();
130
+ if (!text && K.markers.length) text = 'Do the requests I pointed at.';
131
+ if (!text) { if (i) i.focus(); return; }
132
+ if (D.running) { toast('A version is being made. Wait for it to land.'); return; }
133
+ S.busy = true; paint();
134
+ let r;
135
+ if (!D.description) { r = await RD.describe({ ws, id, text, format: K.fmt }); if (!r || !r.ok) { S.busy = false; paint(); failed(r, 'The description was not saved'); return; } }
136
+ const points = K.markers.map((m) => ({ n: m.n, slide: m.slide, x: m.x, y: m.y, text: m.text }));
137
+ r = await RD.turn({ ws, id, text: D.description ? text : 'Make the first version from my description.', points });
138
+ S.busy = false;
139
+ if (!r || !r.ok) { paint(); if (r && (r.status === 409 || /run/i.test(String(r.error || '')))) toast('A version is being made. Wait for it to land.'); else failed(r, 'PMOS did not start'); return; }
140
+ /* the preview follows the version this turn makes: a fast job can land it before this load, and keepView would hold the old one */
141
+ K.text = ''; K.markers = []; K.draft = null; K.mOpen = null; K.view = null; K.cmp = false;
142
+ await load(true); scrollLog();
143
+ }
144
+
145
+ /* ── the result ── */
146
+ function look(n) { K.view = n; K.cmp = false; render(n).then(paint); }
147
+ function strip() {
148
+ const D = S.D; const cur = D.current;
149
+ return el('div', { class: 'rd-dks-vers' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Versions', 'data-rd-versions': '' }, D.versions.map((v) => btn({ 'data-rd-v': String(v.n), 'aria-pressed': String(K.view === v.n && !K.cmp), title: vlabel(v), onclick: () => look(v.n) }, 'D' + v.n, D.saved && D.saved.version === v.n ? icon('check', 'sm') : null))),
150
+ el('span', { class: 'chips2' }, K.view !== cur && !K.cmp ? btn({ class: 'ds-btn', 'data-rd-act': 'revert', onclick: () => revert(K.view) }, icon('undo', 'sm'), 'Go back to D' + K.view) : null,
151
+ D.versions.length > 1 ? btn({ class: 'ds-btn', 'data-rd-act': 'cmp', 'aria-pressed': String(K.cmp), onclick: async () => { K.cmp = !K.cmp; if (K.cmp) { K.view = cur; await render(cur); await render(cmpA()); } paint(); } }, icon('cols', 'sm'), K.cmp ? 'Stop comparing' : 'Compare two') : null));
152
+ }
153
+ async function revert(n) {
154
+ const r = await RD.revert({ ws, id, n }); if (!r || !r.ok) { failed(r, 'It did not go back'); return; }
155
+ await load(true); toast('Back to D' + n + '. The next turn builds on it. The later versions stay in the strip.');
156
+ }
157
+ function markers(key, scaleBox) {
158
+ if (!K.point) return [];
159
+ const L = K.markers.filter((m) => m.slide === key).map((m) => { const b = btn({ class: 'rd-pindot a rd-dks-mk' + (K.mOpen === m.id ? ' on' : ''), 'data-rd-mk': String(m.n), 'aria-label': 'Request ' + m.n + ': ' + m.text, onclick: () => { K.mOpen = K.mOpen === m.id ? null : m.id; paint(); } }, String(m.n)); place(b, m, scaleBox); return b; });
160
+ if (K.draft && K.draft.slide === key) { const d = el('span', { class: 'rd-pindot a on rd-dks-mk', 'aria-hidden': 'true' }, String(K.markers.length + 1)); place(d, K.draft, scaleBox); L.push(d); }
161
+ return L;
162
+ }
163
+ const place = (n, m, box) => { if (box) { n.style.setProperty('--rp-x', Math.round(box.left + box.width * m.x / 100) + 'px'); n.style.setProperty('--rp-y', Math.round(box.top + box.height * m.y / 100) + 'px'); } else { n.style.setProperty('--rp-x', m.x + '%'); n.style.setProperty('--rp-y', m.y + '%'); } };
164
+ function pointSect(key) {
165
+ const ms = K.markers.filter((m) => m.slide === key);
166
+ const draft = K.draft && K.draft.slide === key ? el('div', { class: 'ds-whyadd' }, el('input', { type: 'text', 'data-rd-mkdraft': '', placeholder: 'What should change here? For example: make this a table', 'aria-label': 'Request for this spot', onKeyDown: (e) => { if (e.key === 'Enter') saveMk(); } }),
167
+ btn({ class: 'ds-btn pri', 'data-rd-act': 'mksave', onclick: () => saveMk() }, 'Attach'), btn({ class: 'ds-btn', 'data-rd-act': 'mkcancel', onclick: () => { K.draft = null; paint(); } }, 'Cancel')) : null;
168
+ return sect({ ic: 'target', title: 'Point and say', count: ms.length, attrs: { 'data-rd-point': key },
169
+ right: el('label', { class: 'ds-tg' }, el('input', { type: 'checkbox', checked: K.point, 'data-rd-ck': 'point', onChange: (e) => { K.point = e.target.checked; K.draft = null; paint(); } }), 'On') },
170
+ ms.length ? el('ul', { class: 'ds-log rd-notes' }, ms.map((m) => el('li', { class: 'rd-note' + (K.mOpen === m.id ? ' on' : ''), 'data-rd-mkrow': String(m.n) },
171
+ btn({ class: 'rd-pindot a', 'aria-label': 'Request ' + m.n, onclick: () => { K.mOpen = K.mOpen === m.id ? null : m.id; paint(); } }, String(m.n)),
172
+ el('div', { class: 'ds-le' }, el('div', { class: 'l1' }, el('b', null, m.text), el('span', { class: 'when' }, 'Waiting')),
173
+ K.mOpen === m.id ? [el('div', { class: 'who' }, el('span', null, 'PMOS makes it in the next version, with your other requests.')), el('div', { class: 'ds-acts' }, btn({ class: 'ds-btn ag', 'data-rd-act': 'mkgo', onclick: () => send(K.text.trim() || 'Do the requests I pointed at.') }, icon('spark', 'sm'), 'Make the next version'), btn({ class: 'ds-btn', 'data-rd-act': 'mkdrop', onclick: () => { K.markers = K.markers.filter((x) => x.id !== m.id).map((x, k) => ({ ...x, n: k + 1 })); K.mOpen = null; paint(); } }, icon('trash', 'sm'), 'Remove'))] : null)))) : null,
174
+ draft, el('div', { class: 'dim rd-small' }, K.point ? 'Click any part of the preview to attach a request to it.' : 'Point and say is off.'));
175
+ }
176
+ function saveMk() {
177
+ const i = body.querySelector('[data-rd-mkdraft]'); const t = i && i.value.trim(); if (!t) { if (i) i.focus(); return; }
178
+ const m = { id: 'm' + Date.now(), n: K.markers.length + 1, slide: K.draft.slide, x: K.draft.x, y: K.draft.y, text: t };
179
+ K.markers.push(m); K.draft = null; K.mOpen = m.id; paint();
180
+ }
181
+ const pointHands = (getBox) => ({ place: (e, doc) => { if (!K.point) return false; const b = getBox(e, doc); if (!b) return false; K.draft = { slide: b.id, x: Math.round(b.x), y: Math.round(b.y) }; paint(); const i = body.querySelector('[data-rd-mkdraft]'); if (i) i.focus(); return true; } });
182
+ function preview() {
183
+ const D = S.D;
184
+ if (!D.versions.length) return [D.running ? el('div', { class: 'fold a', role: 'status' }, icon('clock', 'sm'), ' PMOS is making the first version.') : null,
185
+ el('div', { class: 'fold rd-none', 'data-rd-noversion': '' }, icon('info', 'sm'), ' No version yet. Describe the report on the left, add a sample if you have one, then press Make the first version.')];
186
+ if (K.cmp) return [strip(), compare()];
187
+ const v = ver(K.view) || ver(D.current); const R = S.cache[v.n];
188
+ const capline = el('div', { class: 'dim rd-small', 'data-rd-vcap': String(v.n) }, el('b', { class: 'rd-fg' }, 'D' + v.n), ' · ' + vlabel(v) + ' · made ' + hhmm(v.made_at) + '. Built with this cycle\'s real data.' + (v.n !== D.current ? ' You are looking at an older version.' : ''));
189
+ const run = D.running ? el('div', { class: 'fold a', role: 'status' }, icon('clock', 'sm'), ' PMOS is making the next version. This one stays on screen until it lands.') : null;
190
+ if (!R) return [strip(), capline, el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading D' + v.n + '.')];
191
+ if (R.err) return [strip(), capline, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), ' D' + v.n + ' could not be read: ' + R.err + '.')];
192
+ if (R.format === 'doc') {
193
+ const card = el('div', { class: 'rd-docf rd-dks-doc' + (K.point ? ' placing' : ''), 'data-rd-sbig': 'doc' }); const layer = el('div', { class: 'rd-doclayer' });
194
+ const box = (e) => { const sec = e.target.closest && e.target.closest('section.pmos-sec'); if (!sec) return null; const r = sec.getBoundingClientRect(); return { id: sec.getAttribute('data-sec-id'), x: (e.clientX - r.left) / r.width * 100, y: (e.clientY - r.top) / r.height * 100 }; };
195
+ card.append(frame(R.style, R.body, { title: 'D' + v.n + ', the document', cls: 'pmos-doc', focusable: true, onLoad: (doc, fr) => {
196
+ const fit = () => { fr.style.setProperty('--rp-h', Math.max(200, doc.documentElement.scrollHeight) + 'px'); const L = []; for (const s of R.secs) { const n = doc.querySelector('section.pmos-sec[data-sec-id="' + s.id.replace(/"/g, '') + '"]'); if (n) L.push(...markers(s.id, n.getBoundingClientRect())); } put(layer, ...L); };
197
+ fit(); setTimeout(fit, 120); wireFrame(doc, { ...pointHands(box), more: fit });
198
+ } }), layer);
199
+ const key = K.draft ? K.draft.slide : (K.markers[0] || {}).slide || (R.secs[0] || {}).id || 'doc';
200
+ return [strip(), capline, run, card, pointSect(key)];
201
+ }
202
+ if (!R.slides.length) return [strip(), capline, el('div', { class: 'fold w' }, icon('alert', 'sm'), ' D' + v.n + ' has no slide.')];
203
+ K.sidx = Math.min(K.sidx, R.slides.length - 1); const s = R.slides[K.sidx];
204
+ const big = slideCard(R, s, { cls: 'rd-big' + (K.point ? ' placing' : ''), attrs: { 'data-rd-sbig': s.id }, focusable: true, over: markers(s.id), wire: (doc) => wireFrame(doc, pointHands((e) => ({ id: s.id, x: e.clientX / 1280 * 100, y: e.clientY / 720 * 100 }))) });
205
+ return [strip(), capline, run, thumbStrip(R, K.sidx, (i) => { K.sidx = i; K.draft = null; paint(); }),
206
+ el('div', { class: 'rd-dk-cap' }, el('b', { 'data-rd-scap': '' }, 'Slide ' + s.n + ' of ' + R.slides.length + ': ' + s.title), el('span', { class: 'dim rd-small' }, hint(s.kind))), big, pointSect(s.id)];
207
+ }
208
+ function thumbStrip(R, cur, pick) {
209
+ return el('nav', { class: 'rd-dk-strip h', 'aria-label': 'Slides', 'data-rd-sstrip': '' }, R.slides.map((s, i) => btn({ class: 'rd-dk-th', 'aria-current': String(cur === i), 'aria-label': 'Slide ' + s.n + ': ' + s.title, 'data-rd-sgo': String(i), onclick: () => pick(i) },
210
+ el('div', { inert: '' }, slideCard(R, s, { cls: 'rd-thumb' })), el('span', { class: 'rd-dk-thn' }, String(s.n)))));
211
+ }
212
+ function compare() {
213
+ const D = S.D; const b = D.current; const a = cmpA(); const RA = S.cache[a]; const RB = S.cache[b];
214
+ const seg = el('div', { class: 'rd-dk-cap' }, el('span', { class: 'chips2 rd-wrap' }, 'Compare ', el('div', { class: 'seg', role: 'group', 'aria-label': 'Compare with', 'data-rd-cmpa': '' }, D.versions.filter((v) => v.n !== b).map((v) => btn({ 'data-rd-a': String(v.n), 'aria-pressed': String(a === v.n), onclick: async () => { K.cmpA = v.n; await render(v.n); paint(); } }, 'D' + v.n))), ' with ', el('b', null, 'D' + b)));
215
+ if (!RA || !RB) return el('div', null, seg, el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading both versions.'));
216
+ const va = ver(a); const vb = ver(b);
217
+ const ch = diffOutlines(va.outline, vb.outline); if (vb.turn) ch.push('Your turn for D' + b + ': ' + short(vb.turn, 90)); if (va.format !== vb.format) ch.unshift('D' + a + ' is a ' + (va.format === 'deck' ? 'deck' : 'document') + ', D' + b + ' a ' + (vb.format === 'deck' ? 'deck' : 'document'));
218
+ ch.push('Same data in both: this cycle\'s draft');
219
+ const head = (n, v) => el('div', { class: 'chips2 rd-cmph' }, el('b', null, 'D' + n), el('span', { class: 'dim' }, vlabel(v)));
220
+ let cols; let tail = null;
221
+ if (RB.format === 'doc' || RA.format === 'doc') {
222
+ const docOf = (R) => (R.format === 'doc' ? el('div', { class: 'rd-docf sm' }, frame(R.style, R.body, { title: 'Version', cls: 'pmos-doc', onLoad: (doc, fr) => fr.style.setProperty('--rp-h', Math.max(200, doc.documentElement.scrollHeight) + 'px') })) : slideCard(R, R.slides[0] || { id: 'x', n: 1, title: '', html: '' }));
223
+ cols = [el('div', { class: 'rd-cmpc' }, head(a, va), docOf(RA)), el('div', { class: 'rd-cmpc' }, head(b, vb), docOf(RB))];
224
+ } else {
225
+ if (!RB.slides[K.sidx]) K.sidx = 0; const B = RB.slides[K.sidx]; const A = RA.slides.find((x) => x.id === B.id);
226
+ cols = [el('div', { class: 'rd-cmpc' }, head(a, va), A ? slideCard(RA, A) : el('div', { class: 'fold rd-none' }, icon('info', 'sm'), ' Not in D' + a + '.')), el('div', { class: 'rd-cmpc' }, head(b, vb), slideCard(RB, B))];
227
+ tail = thumbStrip(RB, K.sidx, (i) => { K.sidx = i; paint(); });
228
+ seg.append(el('span', { class: 'dim rd-small' }, 'Slide: ' + B.title + '. Pick another slide below.'));
229
+ }
230
+ return el('div', { class: 'rd-stack', 'data-rd-compare': '' }, seg, el('div', { class: 'rd-cmpg' }, ...cols),
231
+ sect({ ic: 'cols', title: 'What changed from D' + a + ' to D' + b, count: ch.length, attrs: { 'data-rd-changed': '' } }, el('ul', { class: 'chgl' }, ch.map((t) => el('li', null, icon('edit', 'sm'), el('span', null, t), el('span'))))), tail);
232
+ }
233
+
234
+ /* ── the bar ── */
235
+ async function setFmt(f) {
236
+ if (K.fmt === f) return; K.fmt = f;
237
+ if (S.D.description && S.D.format !== f) { const r = await RD.describe({ ws, id, text: S.D.description, format: f }); if (!r || !r.ok) { failed(r, 'The format was not changed'); return; } await load(true); toast('The next version is ' + (f === 'deck' ? 'a slide deck' : 'a document') + '. Make it from the conversation.'); return; }
238
+ paint();
239
+ }
240
+ async function accept() {
241
+ const n = K.view != null ? K.view : S.D.current; if (n == null) { toast('Make a version first.'); return; }
242
+ const r = await RD.accept({ ws, id, n }); if (!r || !r.ok) { failed(r, 'The design was not saved'); return; }
243
+ await load(true); toast('Saved D' + n + ' as the design of this report. PMOS drafts each cycle to match it. Nothing is sent.');
244
+ }
245
+ function paint() {
246
+ if (S.closed) return;
247
+ if (!S.D) { put(body, S.err ? el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), ' ' + S.err) : el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading the design.')); return; }
248
+ const D = S.D; const nm = (S.setup && S.setup.name) || name || 'Report';
249
+ const sb = body.closest('.sb'); const y = sb ? sb.scrollTop : 0; const log = body.querySelector('.rd-dks-log'); const ly = log ? log.scrollTop : null;
250
+ const focus = document.activeElement && body.contains(document.activeElement) && document.activeElement.matches('[data-rd-in],[data-rd-link]') ? document.activeElement.getAttribute('data-rd-in') != null ? '[data-rd-in]' : '[data-rd-link]' : null;
251
+ put(body, el('div', { class: 'rd-dk-bar' }, el('div', { class: 'chips2 rd-wrap' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Report type' },
252
+ btn({ 'data-rd-fmt': 'deck', 'aria-pressed': String(K.fmt === 'deck'), onclick: () => setFmt('deck') }, icon('img', 'sm'), 'Deck'), btn({ 'data-rd-fmt': 'doc', 'aria-pressed': String(K.fmt === 'doc'), onclick: () => setFmt('doc') }, icon('doc', 'sm'), 'Document')),
253
+ el('b', null, nm), D.saved ? el('span', { class: 'chip2 ds-good', 'data-rd-saved': String(D.saved.version) }, icon('check', 'sm'), 'Saved design: D' + D.saved.version) : el('span', { class: 'chip2', 'data-rd-saved': '' }, 'Not saved yet')),
254
+ el('div', { class: 'chips2' }, pbtn('primary', 'This is it', 'check', { 'data-rd-act': 'accept', disabled: !D.versions.length, onclick: () => accept() }),
255
+ D.saved ? pbtn('ghost', 'Design brief', 'doc', { 'data-rd-act': 'brief', onclick: () => openBrief({ ws, id, name: nm }) }) : null)),
256
+ el('div', { class: 'fold' }, icon('info', 'sm'), ' No template to fill in. Describe the report, add samples, then refine until it looks right. PMOS keeps what you settle on and drafts every cycle to match it. Nothing is sent.'),
257
+ el('div', { class: 'rd-dks' }, chat(), el('div', { class: 'rd-dks-out', 'data-rd-preview': '' }, ...[].concat(preview()))));
258
+ if (sb) sb.scrollTop = y; const l2 = body.querySelector('.rd-dks-log'); if (l2) l2.scrollTop = ly == null ? l2.scrollHeight : ly;
259
+ if (focus) { const n = body.querySelector(focus); if (n) n.focus(); }
260
+ }
261
+ paint(); load(false).then(scrollLog);
262
+ return { close: () => ui.close(), reload: () => load(true) };
263
+ }
264
+
265
+ /* ── the saved design: a brief, not a template ── */
266
+ export function openBrief({ ws, id, name } = {}) {
267
+ ensureDeckCss();
268
+ const body = el('div', { class: 'calbody brief rs rd-dk', 'data-rd-brief': id });
269
+ openSheet({ title: 'Report setup: ' + (name || id), icon: 'report', wide: true, cls: 'rd-wide', body });
270
+ put(body, el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading the design.'));
271
+ (async () => {
272
+ const r = await RD.design(ws, id);
273
+ if (!r || r.ok === false) { put(body, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), ' The design could not be read: ' + ((r && r.error) || 'no answer') + '.')); return; }
274
+ const D = r.design; const setup = RS.setups.find((x) => x.id === id) || r.setup || {};
275
+ const nm = setup.name || name || id; const out = formatOf(setup.output);
276
+ const lh = el('div', { class: 'lh' }, el('span', { class: 'ki' }, icon('report')), el('div', { class: 'rs-min0' }, el('b', null, nm),
277
+ el('div', { class: 'dim' }, (setup.label || scheduleLabel(setup.schedule)) + '. Output: ' + (setup.output === 'google-slides' ? 'a slide deck in PMOS. A Google Slides copy is optional.' : out.l + '.'))));
278
+ if (!D.saved) {
279
+ put(body, lh, sect({ ic: 'spark', title: 'Design', attrs: { 'data-rd-design': '' } }, el('div', { class: 'fold', 'data-rd-nosaved': '' }, icon('info', 'sm'), ' No saved design yet. Describe the report and refine it in the Design studio, then press This is it.'),
280
+ el('div', { class: 'ds-acts' }, pbtn('primary', 'Design how it looks', 'spark', { 'data-rd-act': 'studio', onclick: () => openStudio({ ws, id, name: nm }) }))));
281
+ return;
282
+ }
283
+ const n = D.saved.version; let R = null; try { R = splitRender(await RD.version(ws, id, n)); } catch { R = null; }
284
+ const rule = (h, xs, attr) => (xs && xs.length ? el('div', { class: 'rd-dks-rule', 'data-rd-rule': attr }, el('b', null, h), el('ul', null, xs.map((x) => el('li', null, x)))) : null);
285
+ const ref = !R ? el('div', { class: 'fold w' }, icon('alert', 'sm'), ' The reference version could not be read.')
286
+ : R.format === 'deck' ? [el('div', { class: 'rd-dks-ref', 'data-rd-ref': String(n) }, R.slides.slice(0, 6).map((s) => el('div', { inert: '' }, slideCard(R, s, { cls: 'rd-thumb' })))), el('div', { class: 'dim rd-small' }, R.slides.length + ' slides with this cycle\'s data.' + (R.slides.length > 6 ? ' Showing the first 6.' : ''))]
287
+ : el('div', { class: 'rd-docf sm', 'data-rd-ref': String(n) }, frame(R.style, R.body, { title: 'The reference version', cls: 'pmos-doc', onLoad: (doc, fr) => fr.style.setProperty('--rp-h', Math.min(520, doc.documentElement.scrollHeight) + 'px') }));
288
+ const rules = D.rules || {};
289
+ const cycHost = el('ul', { class: 'chgl', 'data-rd-briefcycles': '' });
290
+ const docs = (RS.docs || []).filter((d) => d.setup === id).sort((a, b) => String(b.slot).localeCompare(String(a.slot)));
291
+ put(body, lh,
292
+ sect({ ic: 'spark', title: 'Design', right: el('span', { class: 'tag acc' }, 'Design D' + n), attrs: { 'data-rd-design': '' } },
293
+ el('div', { class: 'dim rd-small' }, 'Written by PMOS from your studio session. It is not a template: PMOS reads it each cycle and drafts to match it. Saved ' + dayAt(D.saved.at) + ', version D' + n + '.'),
294
+ el('h3', { class: 'ds-h' }, 'Your description'), el('blockquote', { class: 'rd-dks-q', 'data-rd-desc': '' }, D.description || 'No description.'),
295
+ el('h3', { class: 'ds-h' }, 'Your samples'), (D.samples || []).length ? D.samples.map(sampleChip) : el('div', { class: 'dim rd-small' }, 'No sample.'),
296
+ el('h3', { class: 'ds-h' }, 'Reference version · D' + n), ref,
297
+ el('h3', { class: 'ds-h' }, 'What PMOS learned'), rule('Keeps the same each cycle', rules.same, 'same'), rule('Changes each cycle', rules.changes, 'changes'), rule('Learned from your turns', rules.learned, 'learned'),
298
+ el('div', { class: 'ds-acts' }, pbtn('primary', 'Refine the design', 'spark', { 'data-rd-act': 'studio', onclick: () => openStudio({ ws, id, name: nm }) }), pbtn('outline', 'See the reference version', 'eye', { 'data-rd-act': 'refver', onclick: () => openStudio({ ws, id, name: nm, view: n }) }))),
299
+ sect({ ic: 'cal', title: 'Cycles drafted to match it', count: docs.length }, docs.length ? cycHost : el('div', { class: 'fold' }, icon('info', 'sm'), ' No cycle yet. The next one is drafted to match D' + n + '.')),
300
+ el('div', { class: 'fold' }, icon('shield', 'sm'), ' PMOS drafts each cycle. Nothing is sent: you present the deck or share the PDF yourself.'));
301
+ for (const d of docs) {
302
+ const c = await RD.cycle(ws, d.path);
303
+ const word = !c || c.ok === false || !c.designed ? el('span', { class: 'chip2' }, 'Before the design') : c.matches ? el('span', { class: 'chip2 ds-good' }, icon('check', 'sm'), 'Matches the design') : el('span', { class: 'chip2 ds-warn' }, icon('alert', 'sm'), (c.differences || []).length + ' difference' + ((c.differences || []).length === 1 ? '' : 's') + ' from the design');
304
+ cycHost.append(el('li', { 'data-rd-briefcycle': d.path }, icon('doc', 'sm'), el('span', null, slotWord(d.slot) + ' ', el('span', { class: 'dim rd-small' }, d.state === 'issued' ? 'Issued' : 'Draft')),
305
+ el('span', { class: 'chips2' }, word, pbtn('ghost', 'Open', 'out', { onclick: () => { closeSheet(); if (typeof RS.openDoc === 'function') RS.openDoc(d); } }))));
306
+ }
307
+ })();
308
+ }
@@ -17,9 +17,10 @@ export function ensureReportCss() {
17
17
  document.head.appendChild(l);
18
18
  }
19
19
 
20
- /** The five outputs of a recurring report. l: the name the page shows; ic: its icon; app: the tool a copy lives in; dl: the Download menu, [label, fmt]. */
20
+ /** The five outputs of a recurring report. l: the name the page shows; ic: its icon; app: the tool a copy lives in; dl: the Download menu, [label, fmt].
21
+ * The stored value 'google-slides' is kept for compatibility; the page names it "Slide deck": the deck lives in PMOS, a Google Slides copy is optional (report-design-studio). */
21
22
  export const FORMATS = {
22
- 'google-slides': { l: 'Google Slides', ic: 'img', app: 'Google Slides', copy: 'google-slides', word: 'slides', dl: [['PDF', 'pdf'], ['PowerPoint .pptx', 'pptx']] },
23
+ 'google-slides': { l: 'Slide deck', ic: 'img', app: 'Google Slides', copy: 'google-slides', word: 'slides', dl: [['PDF', 'pdf'], ['PowerPoint .pptx', 'pptx']] },
23
24
  'google-doc': { l: 'Google Doc', ic: 'doc', app: 'Google Doc', copy: 'google', word: 'headings', dl: [['PDF', 'pdf'], ['Word .docx', 'docx']] },
24
25
  'google-sheets': { l: 'Google Sheets', ic: 'sheet', app: 'Google Sheets', copy: 'google-sheets', word: 'sheets', dl: [['Excel .xlsx', 'xlsx'], ['CSV', 'csv']] },
25
26
  confluence: { l: 'Confluence', ic: 'notes', app: 'Confluence', copy: 'confluence', word: 'headings', dl: [['PDF', 'pdf'], ['Word .docx', 'docx']] },
@@ -671,8 +671,24 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
671
671
  return el('dl', { class: 'ds-props', 'data-doc-props': '' }, fm.fields.map((f) => [el('dt', null, f.key), el('dd', null, f.value)]),
672
672
  el('div', { class: 'sub' }, c3ic('doc', 'sm'), 'Front matter', r ? [' · ', byChip(r)] : null));
673
673
  }
674
+ /* a designed report cycle (initiative report-design-studio): c3-report-deck.js draws the deck or the interactive document in place of the
675
+ format view, and reaches back through this api (the Edit tab at a section, the Markdown source, the copies, the download, the edit log) */
676
+ const deckApi = { redraw: () => drawBg(), setSrc: (v) => { S.src = !!v; draw(); }, toggleCopies: () => toggleCopies(), download: (fmt) => download({ fmt }), editAt: (name) => editAt(name), logChanged: () => { S.log = null; }, go: (m) => go(m) };
677
+ function editAt(name) {
678
+ if (frozen()) { say('Issued reports are frozen. Start the next cycle to change the words.'); return; }
679
+ go('edit');
680
+ setTimeout(() => {
681
+ const k = S.ed ? S.ed.base.findIndex((x) => x.name === name) : -1;
682
+ const h = k >= 0 && ui ? ui.body.querySelector('[data-sid="' + k + '"]') : null;
683
+ if (!h) return;
684
+ h.scrollIntoView({ block: 'start' }); h.setAttribute('data-doc-editat', '');
685
+ try { const r = document.createRange(); r.selectNodeContents(h); r.collapse(false); const sel = getSelection(); sel.removeAllRanges(); sel.addRange(r); const ed = ui.body.querySelector('[data-doc-editor]'); if (ed) ed.focus({ preventScroll: true }); } catch { /* the caret stays where it is */ }
686
+ }, 30);
687
+ }
674
688
  function readBody() {
675
689
  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.');
690
+ const deck = R && R.deck && !S.src ? R.deck(deckApi) : null;
691
+ if (deck) return el('div', { class: 'ds' }, staleBanner(), deck);
676
692
  const secs = splitDoc(S.text);
677
693
  if (!S.text.trim()) return el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The document is empty.');
678
694
  const items = R ? outlineLabels(secs, R.output) : outlineOf(secs); const olOn = items.length >= 2 && !S.src;
@@ -713,7 +729,8 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
713
729
  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));
714
730
  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 },
715
731
  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'))));
716
- const main = [generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, staleBanner(), propsBlock(secs, false), cells, adds];
732
+ const copyNote = R && R.copyNotice ? el('div', { class: 'fold', 'data-rd-copynote': '' }, c3ic('info', 'sm'), ' The deck now lives in PMOS. The Google Slides copy of this report is optional. ', btn('chip2 lk', {}, 'Copies in other tools', () => toggleCopies())) : null;
733
+ const main = [generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, copyNote, staleBanner(), propsBlock(secs, false), cells, adds];
717
734
  return el('div', { class: 'ds' + (S.who ? ' who' : '') }, bar, legend,
718
735
  olOn ? el('div', { class: 'ds-rlay' + (S.ol ? '' : ' nool') + (S.olOpen ? ' olopen' : '') }, outlineNav(items), el('div', { class: 'ds-rmain' }, main)) : el('div', { class: 'ds-rmain' }, main));
719
736
  }
@@ -962,6 +979,13 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
962
979
  }
963
980
 
964
981
  /* ── Edit log ── */
982
+ /* notes on the slide and the cycle's design decisions (report-design-studio): the server appends them to the document's log */
983
+ const DESIGN_T = { keep: 'Kept a difference from the design for this cycle', adopt: 'Made a difference part of the design', match: 'Matched the design' };
984
+ function pinTitle(e) {
985
+ const on = e.slide ? ' (' + e.slide + ')' : '';
986
+ if (e.op === 'add') return (e.by === 'agent' ? 'PMOS proposed a note on the slide' : 'Note pinned on the slide') + on;
987
+ return ({ edit: 'Changed a note on the slide', remove: 'Removed a note on the slide', keep: 'Kept the note PMOS proposed', dismiss: 'Dismissed the note PMOS proposed' }[e.op] || 'Note on the slide') + on;
988
+ }
965
989
  function logEntries() {
966
990
  const L = S.log || { revisions: [], events: [] };
967
991
  const out = [];
@@ -971,7 +995,8 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
971
995
  out.push({ kind: 'rev', at: r.at, r, ag, outside, people: !ag, title: (r.v ? r.v + ' · ' : 'Revision ' + r.rev + ' · ') + title });
972
996
  }
973
997
  for (const e of L.events) {
974
- const t = { proposed: 'The agent drafted a proposal', revised: 'The agent revised a proposal', rejected: 'Proposal rejected', asked: 'You asked the agent', 'ask-failed': 'The agent did not draft the change', marked: 'Marked as still current' }[e.kind] || e.kind;
998
+ const t = e.kind === 'pin' ? pinTitle(e) : e.kind === 'design' ? DESIGN_T[e.choice] || 'A decision on the design'
999
+ : { proposed: 'The agent drafted a proposal', revised: 'The agent revised a proposal', rejected: 'Proposal rejected', asked: 'You asked the agent', 'ask-failed': 'The agent did not draft the change', marked: 'Marked as still current' }[e.kind] || e.kind;
975
1000
  out.push({ kind: e.kind, at: e.at, e, ag: e.by === 'agent' || e.kind === 'proposed' || e.kind === 'revised' || e.kind === 'ask-failed', outside: false, people: e.by === 'pm' || e.kind === 'rejected' || e.kind === 'asked' || e.kind === 'marked', title: t });
976
1001
  }
977
1002
  return out.sort((a, b) => String(b.at).localeCompare(String(a.at)));
@@ -995,7 +1020,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
995
1020
  else if (noReason) why = el('p', { class: 'why none' }, c3ic('alert', 'sm'), ' No reason recorded: this change was made outside PMOS. ', btn('chip2 lk ds-pe', { 'data-doc-whyadd': String(r.rev) }, 'Add a reason', () => { S.whyFor = r.rev; S.whyText = ''; draw(); const i = ui.body.querySelector('#daddwhy'); if (i) i.focus(); }));
996
1021
  else if (r.why) why = el('p', { class: 'why' }, r.why);
997
1022
  } else {
998
- const t = e.kind === 'asked' ? '“' + e.prompt + '”' : e.kind === 'proposed' || e.kind === 'revised' ? 'For: “' + (e.prompt || '') + '”' : e.why || '';
1023
+ const t = e.kind === 'pin' ? (e.text ? '“' + e.text + '” Note on the slide: the words on the slide did not change.' : 'Note on the slide.') : e.kind === 'design' ? (e.what || e.diff || '') : e.kind === 'asked' ? '“' + e.prompt + '”' : e.kind === 'proposed' || e.kind === 'revised' ? 'For: “' + (e.prompt || '') + '”' : e.why || '';
999
1024
  if (t) why = el('p', { class: 'why' }, t);
1000
1025
  }
1001
1026
  const tags = [];
@@ -1103,7 +1128,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
1103
1128
  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.'); }
1104
1129
  function drawTools() {
1105
1130
  if (!toolsNode) return;
1106
- toolsNode.replaceChildren(...[R.state === 'draft' ? pb('outline', { 'data-doc-issue': '' }, [c3ic('check', 'sm'), 'Issue'], () => issue()) : null,
1131
+ toolsNode.replaceChildren(...[...(R.tools ? R.tools() : []), R.state === 'draft' ? pb('outline', { 'data-doc-issue': '' }, [c3ic('check', 'sm'), 'Issue'], () => issue()) : null,
1107
1132
  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));
1108
1133
  }
1109
1134
 
@@ -1122,7 +1147,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
1122
1147
  const had = keep ? { id: keep.id, s: keep.selectionStart } : null;
1123
1148
  const above = docAbove.map((fn) => { try { return fn({ ws, doc, text: S.text, key: S }); } catch { return null; } });
1124
1149
  ui.body.replaceChildren(el('div', { class: 'dpn ds' }, top(), above, copiesPanel(), body));
1125
- const sh = ui.body.closest('.sheet'); if (sh) sh.classList.toggle('dwx', S.mode === 'read' && S.ol && !!ui.body.querySelector('.ds-ol'));
1150
+ const sh = ui.body.closest('.sheet'); if (sh) sh.classList.toggle('dwx', S.mode === 'read' && ((S.ol && !!ui.body.querySelector('.ds-ol')) || !!ui.body.querySelector('[data-rd-deck]')));
1126
1151
  const sb = ui.body.closest('.sb');
1127
1152
  if (sb && !sb.__dsTrack) { sb.__dsTrack = true; sb.addEventListener('scroll', trackCur, { passive: true }); }
1128
1153
  if (S.mode === 'read') { placeOl(); trackCur(); }
@@ -1,6 +1,7 @@
1
1
  /* pm/c3-ib-items.js — the Backlog, Board and Timeline tabs of a board (mockup v5, 34-initiatives.js tabItems / backlogBody / kanbanBody /
2
2
  * timelineBody). The three show the same work items, Jira items and PMOS tasks mixed (GET /api/boards/items through loadItems), under ONE
3
- * filter set per board that the three tabs share: search, Type, Hide done, Initiative and Source. ctx.filter.ini (a link "this initiative
3
+ * filter set per board that the three tabs share, on one row (c3-board-declutter): search, an initiative picker, and a Filters panel
4
+ * with Type, Source and Done; a filter that is on shows as a chip with an x. ctx.filter.ini (a link "this initiative
4
5
  * only" from an initiative page) sets the Initiative filter on mount. Source shows only when the board has Build and deliver on: with it
5
6
  * off there is no Jira, and every item is a PMOS task.
6
7
  * registerBoardTab('backlog' | 'board' | 'timeline', (host, ctx) => ...)
@@ -14,7 +15,7 @@
14
15
  * Every control is a button; the drag handlers are drag events on the cards and columns (pointer handlers stay on the document). */
15
16
  import { el } from './dom.js';
16
17
  import { registerBoardTab } from './c3-initiatives.js';
17
- import { icon, pill, lk, ib, pbtn, note, lbl, sect, card, list, empty, stepsMini, jiraLabel, plural, trunc, openMenu, mono, put } from './c3-ini-ui.js';
18
+ import { icon, pill, lk, ib, pbtn, note, sect, card, list, empty, stepsMini, jiraLabel, plural, trunc, openMenu, mono, put } from './c3-ini-ui.js';
18
19
  import { STATUSES, wfOf, tplName, loadItems, dropItems, workRow, workTlRows, iniTlRow, rescheduleTask, lockedToast } from './c3-ini-work.js';
19
20
  import { timeline, tlInit, tlRange, tlStill } from './c3-ini-timeline.js';
20
21
 
@@ -52,25 +53,33 @@ function mountItems(tab) {
52
53
  const withWork = [...new Set(all.map((w) => w.ini))].map((id) => byId.get(id)).filter(Boolean).sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || a.id.localeCompare(c.id));
53
54
  if (st.ini && !withWork.some((i) => i.id === st.ini) && byId.get(st.ini)) withWork.push(byId.get(st.ini));
54
55
  const others = R.initiatives.length - withWork.length;
55
- const n = shown.filter((w) => w.kind !== 'Epic').length; const ne = shown.filter((w) => w.kind === 'Epic').length;
56
56
  const search = el('input', { type: 'text', value: st.q, placeholder: 'Search ' + b.name + ' work items', 'aria-label': 'Search work items', 'data-wk': 'q', autocomplete: 'off' });
57
57
  let deb = null;
58
58
  search.addEventListener('input', () => { clearTimeout(deb); deb = setTimeout(() => { st.q = search.value; draw('[data-wk="q"]'); }, 200); });
59
- const any = st.q || st.ini || st.src || st.kind || st.hideDone;
59
+ const tick = () => el('span', { class: 'il-tick' }, icon('check', 'sm'));
60
+ const SRCS = [['', 'All sources'], ['jira', 'Jira ' + jiraLabel(b)], ['pmos', 'PMOS tasks']];
61
+ const nf = (st.kind ? 1 : 0) + (st.src ? 1 : 0) + (st.hideDone ? 1 : 0);
62
+ const iniMenu = (anchor) => openMenu(anchor, { label: 'Initiative', items: [{ group: 'Initiative' },
63
+ { kids: [tick(), 'All initiatives'], checked: !st.ini, attrs: { 'data-wk-ini': '' }, onPick: () => { st.ini = ''; draw('[data-wk="ini"]'); } },
64
+ ...withWork.map((i) => ({ kids: [tick(), el('span', { class: 'mono' }, i.id), ' ' + trunc(i.title, 30)], checked: st.ini === i.id, attrs: { 'data-wk-ini': i.id, title: i.id + ' ' + i.title }, onPick: () => { st.ini = i.id; draw('[data-wk="ini"]'); } })),
65
+ ...(others > 0 ? [{ group: plural(others, 'more has', 'more have') + ' no work yet' }] : [])] });
66
+ const filterMenu = (anchor) => {
67
+ const sel = (k, label, opts) => el('label', { class: 'il-f' }, el('span', null, label), el('select', { 'aria-label': label, 'data-wk': k === 'kind' ? 'kind' : 'src', onchange: (e) => { st[k] = e.target.value; draw('[data-wk="filters"]'); } }, ...opts.map(([v, l]) => el('option', { value: v, selected: v === st[k] }, l))));
68
+ openMenu(anchor, { label: 'Filters', body: el('div', { 'data-wk': 'filter-panel' }, el('div', { class: 'ini-mlbl' }, 'Filter work items'),
69
+ sel('kind', 'Type', KINDS), b.build ? sel('src', 'Source', SRCS) : null,
70
+ el('label', { class: 'il-f' }, el('span', null, 'Done'), el('select', { 'aria-label': 'Done items', 'data-wk': 'done', onchange: (e) => { st.hideDone = e.target.value === 'hide'; draw('[data-wk="filters"]'); } }, el('option', { value: 'show', selected: !st.hideDone }, 'Show'), el('option', { value: 'hide', selected: st.hideDone }, 'Hide'))),
71
+ b.build ? null : el('div', { class: 'dim il-fnote', 'data-wk': 'no-jira' }, 'Build and deliver is off, so every item is a PMOS task. ', lk({ onclick: () => ctx.openBoardSettings() }, 'Board settings')),
72
+ el('div', { class: 'chips2 il-fend' }, lk({ 'data-wk': 'clear-in-menu', onclick: () => { Object.assign(st, { q: '', ini: '', src: '', kind: '', hideDone: false }); draw(); } }, 'Clear filters'))) });
73
+ };
74
+ const on = [st.kind ? ['kind', 'Type: ' + st.kind] : null, st.src ? ['src', 'Source: ' + (st.src === 'jira' ? 'Jira' : 'PMOS tasks')] : null, st.hideDone ? ['hideDone', 'Done hidden'] : null].filter(Boolean);
75
+ const any = st.q || st.ini || on.length;
60
76
  return [
61
- el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, search),
62
- el('label', { class: 'chips2' }, lbl('Type'), el('select', { 'aria-label': 'Type', 'data-wk': 'kind', onchange: (e) => { st.kind = e.target.value; draw('[data-wk="kind"]'); } }, ...KINDS.map(([v, l]) => el('option', { value: v, selected: v === st.kind }, l)))),
63
- pill({ 'data-wk': 'done', 'aria-pressed': String(st.hideDone), onclick: () => { st.hideDone = !st.hideDone; draw('[data-wk="done"]'); } }, 'Hide done')),
64
- el('div', { class: 'chips2', role: 'group', 'aria-label': 'Initiative filter', 'data-wk': 'inis' }, lbl('INITIATIVE'),
65
- pill({ 'data-wk-ini': '', 'aria-pressed': String(!st.ini), onclick: () => { st.ini = ''; draw('[data-wk-ini=""]'); } }, 'All initiatives'),
66
- ...withWork.map((i) => pill({ 'data-wk-ini': i.id, 'aria-pressed': String(st.ini === i.id), title: i.id + ' ' + i.title, onclick: () => { st.ini = i.id; draw('[data-wk-ini="' + i.id + '"]'); } }, el('span', { class: 'mono' }, i.id), ' ' + trunc(i.title, 30))),
67
- others > 0 ? el('span', { class: 'dim ini-note' }, plural(others, 'more initiative has', 'more initiatives have') + ' no epics or tasks yet') : null),
68
- b.build
69
- ? el('div', { class: 'chips2', role: 'group', 'aria-label': 'Source filter', 'data-wk': 'srcs' }, lbl('SOURCE'),
70
- ...[['', 'All sources'], ['jira', [icon('jira', 'sm'), 'Jira ' + jiraLabel(b)]], ['pmos', 'PMOS tasks']].map(([v, l]) => pill({ 'data-wk-src': v, 'aria-pressed': String(st.src === v), onclick: () => { st.src = v; draw('[data-wk-src="' + v + '"]'); } }, ...[].concat(l))))
71
- : el('div', { class: 'dim ini-note', 'data-wk': 'no-jira' }, icon('info', 'sm'), ' Build and deliver is off for ' + b.name + ', so there is no Jira here: every item is a PMOS task. ', lk({ onclick: () => ctx.openBoardSettings() }, 'Board settings')),
72
- el('div', { class: 'dim ini-note', 'data-wk': 'count' }, plural(n, 'item') + (ne ? ' in ' + plural(ne, 'epic') : '') + (st.ini ? ' · ' + st.ini + ' only' : '') + (st.src ? ' · ' + (st.src === 'jira' ? 'Jira only' : 'PMOS only') : '') + ' · ',
73
- any ? lk({ 'data-wk': 'clear', onclick: () => { Object.assign(st, { q: '', ini: '', src: '', kind: '', hideDone: false }); draw(); } }, 'Clear filters') : null, any ? ' · ' : null, 'same filters on Backlog, Board and Timeline'),
77
+ el('div', { class: 'calbar wk-fbar' }, el('div', { class: 'form il-search' }, icon('search', 'sm'), search),
78
+ pbtn('ghost', [st.ini ? el('span', { class: 'mono' }, st.ini) : 'All initiatives', el('span', { class: 'il-chev' }, icon('next', 'sm'))], null, { 'data-wk': 'ini', 'aria-haspopup': 'menu', 'aria-expanded': 'false', title: 'Initiative', onclick: (e) => iniMenu(e.currentTarget) }),
79
+ ib(nf ? 'Filters, ' + nf + ' on' : 'Filters', [icon('filter', 'sm'), nf ? el('span', { class: 'bdg' }, String(nf)) : null], { class: 'il-ib', 'data-wk': 'filters', 'aria-haspopup': 'dialog', 'aria-expanded': 'false', title: 'Filters', onclick: (e) => filterMenu(e.currentTarget) }),
80
+ any ? el('div', { class: 'chips2 il-chips', 'data-wk': 'chips' },
81
+ ...on.map(([k, l]) => lk({ 'data-wk-unset': k, title: 'Remove this filter', onclick: () => { st[k] = k === 'hideDone' ? false : ''; draw('[data-wk="filters"]'); } }, l + ' ', icon('x', 'sm'))),
82
+ lk({ 'data-wk': 'clear', onclick: () => { Object.assign(st, { q: '', ini: '', src: '', kind: '', hideDone: false }); draw(); } }, 'Clear')) : null),
74
83
  ];
75
84
  }
76
85
 
@@ -4,11 +4,11 @@
4
4
  * and `status` when a release carries them) and keeps the releases whose initiative_ids meet the board's initiatives (loadItems). A
5
5
  * release that names another board in `board` is left out. Read-only: releases mirror Jira fix versions; their dates live on the Calendar.
6
6
  * Left: Unreleased (the nearest first), each row with its status, date and days to go, the share of its scope done, and in place its
7
- * scope as links; then Released. Right: where releases live, and Open the Calendar. Nothing is invented: a release with no name shows
7
+ * scope as links; then Released. The Unreleased header links to the Calendar (c3-board-declutter: no side card). Nothing is invented: a release with no name shows
8
8
  * its id, one with no date says "No date yet". */
9
9
  import { el } from './dom.js';
10
10
  import { registerBoardTab } from './c3-initiatives.js';
11
- import { icon, pbtn, lk, chip, tag, st, note, sect, card, list, empty, nrow, cols, hbs, hbr, statusChip, jiraLabel, plural, day, daysBetween, put } from './c3-ini-ui.js';
11
+ import { icon, lk, chip, tag, st, note, sect, list, empty, nrow, hbs, hbr, statusChip, jiraLabel, plural, day, daysBetween, put } from './c3-ini-ui.js';
12
12
  import { loadItems } from './c3-ini-work.js';
13
13
 
14
14
  const whenOf = (r) => String(r.release_at || r.deploy_at || r.date || '').slice(0, 10);
@@ -42,10 +42,7 @@ registerBoardTab('releases', async (host, ctx) => {
42
42
  : [el('span', { class: 'dim' }, 'No initiatives in scope')]))],
43
43
  attrs: { 'data-inb-release': r.id, 'data-inb-when': isPast ? 'past' : r === up[0] ? 'next' : 'later' } });
44
44
  };
45
- put(host, cols(
46
- [sect({ ic: 'out', title: 'Unreleased', count: up.length, attrs: { 'data-inb': 'release-next' } }, up.length ? list(up.map((r) => row(r, false))) : empty('No release planned for ' + b.name + '.', { 'data-inb': 'release-none-next' })),
47
- past.length ? sect({ ic: 'check', title: 'Released', count: past.length, attrs: { 'data-inb': 'release-past' } }, list(past.map((r) => row(r, true)))) : null],
48
- [card({ ic: 'out', title: 'Where releases live' }, note('Releases mirror Jira ' + jiraLabel(b) + ' fix versions, because this board builds and delivers. Release dates are references: their home is the Calendar.'),
49
- el('div', { class: 'chips2' }, pbtn('outline', 'Open the Calendar', 'cal', { 'data-inb': 'open-calendar', onclick: () => ctx.showTab('cal') })))]));
45
+ put(host, ...[sect({ ic: 'out', title: 'Unreleased', count: up.length, right: lk({ 'data-inb': 'open-calendar', title: 'Releases mirror Jira ' + jiraLabel(b) + ' fix versions. Their dates live on the Calendar.', onclick: () => ctx.showTab('cal') }, icon('cal', 'sm'), 'On the Calendar'), attrs: { 'data-inb': 'release-next' } }, up.length ? list(up.map((r) => row(r, false))) : empty('No release planned for ' + b.name + '.', { 'data-inb': 'release-none-next' })),
46
+ past.length ? sect({ ic: 'check', title: 'Released', count: past.length, attrs: { 'data-inb': 'release-past' } }, list(past.map((r) => row(r, true)))) : null].filter(Boolean));
50
47
  return () => { dead = true; };
51
48
  });