create-pmos 0.197.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 (38) 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-report-design-real-test.js +225 -0
  7. package/workspace/c3-report-design-test.js +403 -0
  8. package/workspace/c3-report-setup-test.js +3 -2
  9. package/workspace/c3-work-test.js +1 -1
  10. package/workspace/guide/index.md +1 -0
  11. package/workspace/guide/report-design.md +104 -0
  12. package/workspace/guide/report-slides.md +3 -1
  13. package/workspace/guide/reports-set-up.md +2 -1
  14. package/workspace/lib/headless.js +24 -6
  15. package/workspace/lib/report-design.js +741 -0
  16. package/workspace/lib/report-draft.js +8 -3
  17. package/workspace/lib/report-setups.js +1 -1
  18. package/workspace/lib/route-modules.js +1 -0
  19. package/workspace/lib/routes-doc-sheet.js +8 -1
  20. package/workspace/lib/routes-report-design.js +481 -0
  21. package/workspace/lib/routes-reports.js +2 -1
  22. package/workspace/package-lock.json +2 -2
  23. package/workspace/package.json +1 -1
  24. package/workspace/public/pm/c3-demo-rd.js +426 -0
  25. package/workspace/public/pm/c3-demo-rep2.js +2 -0
  26. package/workspace/public/pm/c3-demo-rs.js +5 -1
  27. package/workspace/public/pm/c3-design-studio.js +308 -0
  28. package/workspace/public/pm/c3-doc-formats.js +3 -2
  29. package/workspace/public/pm/c3-doc-sheet.js +30 -5
  30. package/workspace/public/pm/c3-initiatives.js +2 -1
  31. package/workspace/public/pm/c3-report-deck.css +139 -0
  32. package/workspace/public/pm/c3-report-deck.js +558 -0
  33. package/workspace/public/pm/c3-report-frame.css +20 -0
  34. package/workspace/public/pm/c3-report-setup.js +18 -6
  35. package/workspace/server.js +17 -5
  36. package/workspace/skills/report-design.skill +61 -0
  37. package/workspace/test-lists/report-design-backend.json +5 -0
  38. package/workspace/test-lists/report-design-studio-ui.json +5 -0
@@ -0,0 +1,558 @@
1
+ /* pm/c3-report-deck.js — a DESIGNED report cycle in the document sheet's Read tab: the deck or the interactive document, Present, speaker notes,
2
+ * notes on the slide, Print / PDF and the "Differs from the design" notice (initiative report-design-studio; planning/prd/report-design-studio-prd.md
3
+ * sections 3 and 4; mockup 42-reportdeck.js, hashes #deck, #deck-pin, #deck-speaker, #deck-present, #deck-audience, #deck-cycles).
4
+ *
5
+ * createDeck({ ws, path, setup, name, cycle, output, copy, onStudio, onBrief }) -> { node(api), tools(api), present(kind), state }
6
+ * c3-report-setup.js makes one per open document and gives node/tools to openDocSheet's report option. node(api) is null until the cycle
7
+ * is read, and stays null for a cycle with no design: the sheet then draws its ordinary format view.
8
+ * RD the routes of the API contract (report-design-contract.md), through pmGet/pmPost
9
+ * splitRender(html) a sanitized render as { style, slides, secs, body, format } (DOMParser; nothing of it runs)
10
+ * frameDoc(style, inner) the srcdoc of one sandboxed frame: the slide tokens, the frame affordances, the render's own <style>
11
+ * slideCard(R, slide, o) a 1280 x 720 slide in <iframe sandbox="allow-same-origin" srcdoc> (no allow-scripts), scaled to its width
12
+ * wireFrame(doc, h) the data attributes of a render, wired from this page: data-src, data-value, data-more, data-initiative, data-from
13
+ * printDeckHtml(R, o) the print page: the render's slides one per page, nothing else (no speaker note, no pin)
14
+ * diffOutlines(a, b) what changed between two outlines, in words (pure; Node-testable)
15
+ * The render is HTML the server sanitized. It shows only in frames with no script permission; every interaction is wired from here. Nothing is
16
+ * sent: Present, Print and Download stay on this device. Writes (cycle decisions, notes, pins) go through pmPost, which the server takes only
17
+ * from a press on the page. */
18
+ import { el } from './dom.js';
19
+ import { icon, pbtn, btn, chip, sect, put, lk } from './c3-ini-ui.js';
20
+ import { pmGet, pmPost } from './http.js';
21
+ import { toast } from './c3-toast.js';
22
+ import { ensureC3Css } from './pm-work-view.js';
23
+
24
+ export const W = 1280;
25
+ export const H = 720;
26
+ const q = (o) => Object.entries(o).filter(([, v]) => v != null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
27
+ const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
28
+ const pad2 = (n) => String(n).padStart(2, '0');
29
+ const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
30
+ /** 'Wed 7 Oct' for '2026-10-07', 'Sep 2026' for '2026-09', anything else as it is. */
31
+ export function slotWord(slot) {
32
+ const s = String(slot || '');
33
+ let m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
34
+ if (m) { const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])); return WD[d.getUTCDay()] + ' ' + (+m[3]) + ' ' + MON[+m[2] - 1]; }
35
+ m = /^(\d{4})-(\d{2})$/.exec(s); if (m) return MON[+m[2] - 1] + ' ' + m[1];
36
+ return s;
37
+ }
38
+
39
+ /** Link c3-report-deck.css once (the deck, the studio and the brief share it). */
40
+ export function ensureDeckCss() {
41
+ if (typeof document === 'undefined' || document.querySelector('link[data-c3-report-deck]')) return;
42
+ const l = document.createElement('link');
43
+ l.rel = 'stylesheet'; l.href = new URL('./c3-report-deck.css', import.meta.url).href; l.setAttribute('data-c3-report-deck', '');
44
+ document.head.appendChild(l);
45
+ }
46
+
47
+ /* ── the routes (report-design-contract.md) ── */
48
+ async function textOf(url) {
49
+ const r = await fetch(url);
50
+ if (!r.ok) { let m = ''; try { m = (await r.json()).error; } catch { /* not JSON */ } throw new Error(m || 'the workspace did not answer'); }
51
+ return r.text();
52
+ }
53
+ const post = (p, body) => Promise.resolve(pmPost('/api/' + p, body)).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
54
+ export const RD = {
55
+ design: (ws, id) => Promise.resolve(pmGet('/api/report-design?' + q({ ws, id }))).catch(() => null),
56
+ version: (ws, id, n) => textOf('/api/report-design/version?' + q({ ws, id, n })),
57
+ describe: (b) => post('report-design/describe', b),
58
+ sample: (b) => post('report-design/sample', b),
59
+ turn: (b) => post('report-design/turn', b),
60
+ accept: (b) => post('report-design/accept', b),
61
+ revert: (b) => post('report-design/revert', b),
62
+ cycle: (ws, path) => Promise.resolve(pmGet('/api/report-design/cycle?' + q({ ws, path }))).catch(() => null),
63
+ cycleHtml: (ws, path) => textOf('/api/report-design/cycle-html?' + q({ ws, path })),
64
+ decide: (b) => post('report-design/cycle-decision', b),
65
+ notes: (ws, path) => Promise.resolve(pmGet('/api/report-notes?' + q({ ws, path }))).catch(() => null),
66
+ speaker: (b) => post('report-notes/speaker', b),
67
+ pin: (b) => post('report-notes/pin', b),
68
+ };
69
+ export const failed = (r, what) => toast({ text: what + ': ' + ((r && (r.error || r.reason)) || 'the workspace did not answer') + '.', tone: 'error', timeout: 6000 });
70
+
71
+ /* ── a render: parsed, never run ── */
72
+ /** The render as parts. A second line of defence after the server's sanitizer: scripts, frames and on* attributes are dropped here too. */
73
+ export function splitRender(html) {
74
+ const d = new DOMParser().parseFromString(String(html || ''), 'text/html');
75
+ d.querySelectorAll('script,iframe,object,embed,link,meta,base').forEach((n) => n.remove());
76
+ d.querySelectorAll('*').forEach((n) => { for (const a of [...n.attributes]) if (/^on/i.test(a.name) || /^\s*javascript:/i.test(a.value)) n.removeAttribute(a.name); });
77
+ const style = [...d.querySelectorAll('style')].map((s) => s.textContent).join('\n');
78
+ d.querySelectorAll('style').forEach((s) => s.remove());
79
+ const slides = [...d.querySelectorAll('section.pmos-slide')].map((s, i) => ({ id: s.getAttribute('data-slide-id') || 'slide-' + (i + 1), kind: s.getAttribute('data-kind') || '', title: s.getAttribute('data-title') || 'Slide ' + (i + 1), section: s.getAttribute('data-section') || '', html: s.outerHTML, n: i + 1 }));
80
+ const secs = [...d.querySelectorAll('section.pmos-sec')].map((s, i) => ({ id: s.getAttribute('data-sec-id') || 'sec-' + (i + 1), kind: s.getAttribute('data-kind') || '', title: s.getAttribute('data-title') || 'Section ' + (i + 1), section: s.getAttribute('data-section') || s.getAttribute('data-title') || '', n: i + 1 }));
81
+ return { style, slides, secs, body: d.body.innerHTML, format: slides.length ? 'deck' : 'doc' };
82
+ }
83
+ const TOKENS = () => new URL('./tokens.css', import.meta.url).href;
84
+ const FRAME_CSS = () => new URL('./c3-report-frame.css', import.meta.url).href;
85
+ /** The srcdoc of one frame. The render's own <style> comes last, so its rules win over the frame affordances. */
86
+ export function frameDoc(style, inner, cls) {
87
+ return '<!doctype html><html lang="en"><head><meta charset="utf-8"><link rel="stylesheet" href="' + esc(TOKENS()) + '"><link rel="stylesheet" href="' + esc(FRAME_CSS()) + '">'
88
+ + '<style>' + String(style || '').replace(/<\/style/gi, '<\\/style') + '</style></head><body class="pmos-frame' + (cls ? ' ' + cls : '') + '">' + String(inner || '') + '</body></html>';
89
+ }
90
+ /** A frame: sandboxed, no scripts. onLoad(doc) runs each time it loads (moving it in the page loads it again). */
91
+ export function frame(style, inner, { title, cls, onLoad, focusable } = {}) {
92
+ const f = el('iframe', { class: 'rd-if', sandbox: 'allow-same-origin', srcdoc: frameDoc(style, inner, cls), title: title || 'Slide', 'data-rd-frame': '', ...(focusable ? {} : { tabindex: '-1' }) });
93
+ if (onLoad) f.addEventListener('load', () => { try { if (f.contentDocument) onLoad(f.contentDocument, f); } catch { /* the frame went away */ } });
94
+ return f;
95
+ }
96
+ const RO = typeof ResizeObserver === 'function' ? new ResizeObserver((es) => { for (const e of es) { const w = e.contentRect.width; if (w) e.target.style.setProperty('--rp-scale', String(Math.round((w / W) * 10000) / 10000)); } }) : null;
97
+ /** Scale a card's 1280 x 720 frame to the card's width (as slideCards does in report-slides.js). */
98
+ export function fitCard(card) {
99
+ if (RO) RO.observe(card);
100
+ requestAnimationFrame(() => { const w = card.getBoundingClientRect().width; if (w) card.style.setProperty('--rp-scale', String(Math.round((w / W) * 10000) / 10000)); });
101
+ return card;
102
+ }
103
+ /** One slide as a card. o = { cls, attrs, wire(doc), over: [nodes on top], focusable }. */
104
+ export function slideCard(R, slide, o = {}) {
105
+ const f = frame(R.style, slide.html, { title: 'Slide ' + slide.n + ': ' + slide.title, cls: 'pmos-one', onLoad: o.wire, focusable: o.focusable });
106
+ return fitCard(el('div', { class: 'rd-card' + (o.cls ? ' ' + o.cls : ''), 'data-rd-card': slide.id, ...(o.attrs || {}) }, f, ...(o.over || [])));
107
+ }
108
+
109
+ /* ── the data attributes, wired from the page ── */
110
+ const toggleMore = (m) => { if (m.tagName === 'DETAILS') m.open = !m.open; else if (m.hasAttribute('data-open')) m.removeAttribute('data-open'); else m.setAttribute('data-open', ''); };
111
+ /** data-from: a "was <value>" note after each changed value while `on`. */
112
+ export function showChanges(doc, on) {
113
+ doc.querySelectorAll('[data-from]').forEach((n) => {
114
+ n.classList.toggle('pmos-chg', !!on);
115
+ const nx = n.nextElementSibling; const has = nx && nx.classList.contains('pmos-was');
116
+ if (on && !has) { const w = doc.createElement('small'); w.className = 'pmos-was'; w.textContent = 'was ' + n.getAttribute('data-from'); n.after(w); }
117
+ if (!on && has) nx.remove();
118
+ });
119
+ }
120
+ /**
121
+ * h = { place(e, doc) -> true when the click placed a marker, src(ref, node), initiative(id), value(text, node) / leave(), key(e), chg }.
122
+ * Every interactive part gets a role and the keyboard; links never navigate.
123
+ */
124
+ export function wireFrame(doc, h) {
125
+ const win = doc.defaultView; if (!win) return;
126
+ doc.querySelectorAll('[data-src],[data-initiative],[data-more]').forEach((n) => { if (n.tagName !== 'DETAILS' && n.tagName !== 'SUMMARY') { if (!n.hasAttribute('tabindex')) n.setAttribute('tabindex', '0'); if (!n.hasAttribute('role')) n.setAttribute('role', 'button'); } });
127
+ doc.querySelectorAll('[data-value]').forEach((n) => { if (!n.hasAttribute('tabindex')) n.setAttribute('tabindex', '0'); });
128
+ const act = (e) => {
129
+ const t = e.target && e.target.nodeType === 1 ? e.target : e.target && e.target.parentElement; if (!t) return;
130
+ if (h.place && h.place(e, doc)) { e.preventDefault(); return; }
131
+ if (t.closest('a')) e.preventDefault();
132
+ const s = t.closest('[data-src]'); if (s) { e.preventDefault(); if (h.src) h.src(s.getAttribute('data-src'), s); return; }
133
+ const i = t.closest('[data-initiative]'); if (i) { e.preventDefault(); if (h.initiative) h.initiative(i.getAttribute('data-initiative'), i); return; }
134
+ const m = t.closest('[data-more]'); if (m && !t.closest('summary')) { e.preventDefault(); toggleMore(m); if (h.more) h.more(m); }
135
+ else if (m && m.tagName === 'DETAILS' && h.more) setTimeout(() => h.more(m), 0);
136
+ };
137
+ doc.defaultView.addEventListener('click', act);
138
+ win.addEventListener('keydown', (e) => {
139
+ const t = e.target && e.target.closest ? e.target : null;
140
+ if (t && (e.key === 'Enter' || e.key === ' ') && t.closest('[data-src],[data-initiative],[data-more]') && t.tagName !== 'SUMMARY') { act({ target: t, preventDefault: () => e.preventDefault(), clientX: 0, clientY: 0 }); e.preventDefault(); return; }
141
+ if (h.key) h.key(e);
142
+ });
143
+ const hov = (e) => { const v = e.target && e.target.closest && e.target.closest('[data-value]'); if (v) { if (h.value) h.value(v.getAttribute('data-value'), v); } else if (h.leave) h.leave(); };
144
+ win.addEventListener('mouseover', hov); win.addEventListener('focusin', hov);
145
+ doc.documentElement.addEventListener('mouseleave', () => { if (h.leave) h.leave(); });
146
+ if (h.touch) { win.addEventListener('touchstart', h.touch.start, { passive: true }); win.addEventListener('touchend', h.touch.end, { passive: true }); }
147
+ showChanges(doc, !!h.chg);
148
+ }
149
+
150
+ /* ── pure ── */
151
+ /** What changed from outline a to outline b: [text]. Outlines are [{ id, kind, title }]. */
152
+ export function diffOutlines(a, b) {
153
+ const A = a || []; const B = b || []; const out = [];
154
+ const ia = new Map(A.map((s, i) => [s.id, i]));
155
+ for (const [i, s] of B.entries()) {
156
+ if (!ia.has(s.id)) { out.push('Added: ' + s.title + ' (slide ' + (i + 1) + ')'); continue; }
157
+ const p = A[ia.get(s.id)];
158
+ if (p.kind !== s.kind) out.push(s.title + ': ' + (p.kind || 'plain') + ' became ' + (s.kind || 'plain'));
159
+ if (p.title !== s.title) out.push('Renamed: ' + p.title + ' is now ' + s.title);
160
+ }
161
+ for (const s of A) if (!B.some((x) => x.id === s.id)) out.push('Removed: ' + s.title);
162
+ const common = B.filter((s) => ia.has(s.id)).map((s) => s.id); const was = A.filter((s) => common.includes(s.id)).map((s) => s.id);
163
+ common.forEach((id, k) => { if (was[k] !== id) { const s = B.find((x) => x.id === id); const from = A.findIndex((x) => x.id === id) + 1; const to = B.findIndex((x) => x.id === id) + 1; if (from !== to && !out.some((t) => t.startsWith('Moved: ' + s.title))) out.push('Moved: ' + s.title + ' to slide ' + to + ', was slide ' + from); } });
164
+ return out;
165
+ }
166
+ /** The print page: every slide of the render on its own 1280 x 720 page, in order. Nothing else: no speaker note, no pin, no page chrome. */
167
+ export function printDeckHtml(R, o = {}) {
168
+ const title = esc(o.title || 'Report');
169
+ if (R.format !== 'deck') return frameDoc(R.style, R.body, 'pmos-print pmos-print-doc').replace('<head>', '<head><title>' + title + '</title>');
170
+ return frameDoc(R.style, R.slides.map((s) => s.html).join(''), 'pmos-print pmos-print-deck').replace('<head>', '<head><title>' + title + '</title>');
171
+ }
172
+ /** Open the print page in a window and print it. False when the browser blocked the window. */
173
+ export function printDeck(R, o) {
174
+ const w = typeof window !== 'undefined' && window.open ? window.open('', '_blank') : null;
175
+ if (!w) return false;
176
+ w.document.open(); w.document.write(printDeckHtml(R, o)); w.document.close();
177
+ w.focus(); setTimeout(() => { try { w.print(); } catch { /* the PM prints from the window */ } }, 300);
178
+ return true;
179
+ }
180
+
181
+ /* open an initiative: the Initiatives page, on the initiative (c3-initiatives.js reads pmos:ini-go) */
182
+ export function openInitiative(id) {
183
+ try { window.dispatchEvent(new CustomEvent('pmos:go-tab', { detail: { tab: 'init' } })); window.dispatchEvent(new CustomEvent('pmos:ini-go', { detail: { id } })); } catch { /* no events */ }
184
+ }
185
+ const SRC_WORD = { jira: 'Jira', initiative: 'PMOS initiative', ini: 'PMOS initiative', commit: 'Commit', doc: 'Document', metric: 'Metric', release: 'Release', dashboard: 'Dashboard', confluence: 'Confluence', section: 'Section of the document' };
186
+ export function srcParts(ref) { const s = String(ref || ''); const k = s.indexOf(':'); const kind = k > 0 ? s.slice(0, k) : 'source'; return { kind, id: k > 0 ? s.slice(k + 1) : s, word: SRC_WORD[kind.toLowerCase()] || kind.charAt(0).toUpperCase() + kind.slice(1) }; }
187
+
188
+ /* ── the deck in the Read tab ── */
189
+ const CAPS = [['eye', 'Hover a value for its detail'], ['org', 'Click a number or status for its source'], ['plus', 'Expand the details'], ['init', 'Live link to each initiative'], ['notes', 'Notes on the slide'], ['mic', 'Presenter view with your notes']];
190
+ const KIND_WORD = (k) => (k ? String(k).replace(/[-_]/g, ' ').replace(/^./, (c) => c.toUpperCase()) : 'Slide');
191
+ const hintOf = (k) => (/headline|metric|kpi/.test(k) ? 'Hover a value for its detail. Click a number to see its source.' : /list|table/.test(k) ? 'Click a status to see where it comes from, or an initiative to open it.'
192
+ : /init/.test(k) ? 'Click the status for its source, or the live link for the initiative today. A long column folds into "more".' : /roadmap/.test(k) ? 'Hover a bar for its stage and dates.'
193
+ : /release|ship/.test(k) ? 'Click a release type to see its commit or ticket.' : /risk|need/.test(k) ? 'Hover a risk for its impact.' : 'The words come from the document. Change them in the Edit tab; the deck redraws.');
194
+
195
+ export function createDeck(o) {
196
+ ensureDeckCss();
197
+ const { ws, path } = o;
198
+ const S = { loaded: false, cyc: null, R: null, prev: null, prevR: null, notes: { speaker: {}, suggestions: {}, pins: [] }, cur: 0, mode: 'deck', pins: true, open: null, src: null, placing: false, draft: null, chg: false, err: '', timer: null, api: null, spkT: {} };
199
+ const root = el('div', { class: 'rd-dk', 'data-rd-deck': path });
200
+ const redraw = () => { if (S.api && S.api.redraw) S.api.redraw(); };
201
+
202
+ async function load() {
203
+ const c = await RD.cycle(ws, path);
204
+ if (!c || c.ok === false) { S.loaded = true; S.cyc = null; S.err = (c && c.error) || ''; redraw(); return; }
205
+ S.cyc = c;
206
+ if (c.designed) {
207
+ try { S.R = splitRender(await RD.cycleHtml(ws, path)); } catch (e) { S.err = (e && e.message) || 'the render could not be read'; S.R = null; }
208
+ const n = await RD.notes(ws, path); if (n && n.ok !== false) S.notes = { speaker: n.speaker || {}, suggestions: n.suggestions || {}, pins: n.pins || [] };
209
+ if (c.previous && c.previous.path) {
210
+ const pc = await RD.cycle(ws, c.previous.path);
211
+ if (pc && pc.ok !== false && pc.designed) { S.prev = { ...pc, ...c.previous }; try { S.prevR = splitRender(await RD.cycleHtml(ws, c.previous.path)); } catch { S.prevR = null; } }
212
+ }
213
+ }
214
+ S.loaded = true; S.cur = Math.min(S.cur, Math.max(0, count() - 1));
215
+ if (c.rendering) poll();
216
+ redraw();
217
+ }
218
+ function poll() { clearTimeout(S.timer); S.timer = setTimeout(async () => { if (!root.isConnected && !OV) return; await load(); }, 2000); }
219
+ const count = () => (S.R ? (S.R.format === 'deck' ? S.R.slides.length : S.R.secs.length) : 0);
220
+ const slides = () => (S.R ? S.R.slides : []);
221
+ const isDeck = () => !!S.R && S.R.format === 'deck';
222
+ const prevWord = () => (S.cyc && S.cyc.previous ? slotWord(S.cyc.previous.slot) : 'the last cycle');
223
+ const live = () => S.notes.pins.filter((p) => p.state !== 'dismissed');
224
+ const pinsOf = (id) => live().filter((p) => p.slide === id);
225
+ const numOf = (p) => live().indexOf(p) + 1;
226
+ const curSlide = () => slides()[S.cur] || null;
227
+
228
+ /* ── interactions inside a frame ── */
229
+ const hands = (card, slide, extra) => ({
230
+ chg: S.chg,
231
+ src: (ref, n) => { S.src = { ref, label: (n.textContent || '').trim().slice(0, 60), detail: n.getAttribute('data-value') || '' }; S.open = null; paint(); const c = root.querySelector('[data-rd-src]'); if (c) c.scrollIntoView({ block: 'nearest' }); },
232
+ initiative: (id) => { S.src = { ref: 'initiative:' + id, label: id, detail: '', ini: id }; S.open = null; paint(); },
233
+ value: (text, n) => tipAt(card, n, text),
234
+ leave: () => tipOff(card),
235
+ place: (e, doc) => {
236
+ if (!S.placing || !slide) return false;
237
+ const box = slide.box ? slide.box(e, doc) : { x: e.clientX / W * 100, y: e.clientY / H * 100, id: slide.id };
238
+ if (!box) return false;
239
+ S.draft = { slide: box.id, x: Math.round(box.x), y: Math.round(box.y) }; S.placing = false; paint();
240
+ const i = root.querySelector('[data-rd-pindraft]'); if (i) i.focus();
241
+ return true;
242
+ },
243
+ ...(extra || {}),
244
+ });
245
+ function tipAt(card, n, text) {
246
+ const t = card && card.querySelector('.rd-tip'); if (!t) return;
247
+ const k = Number(getComputedStyle(card).getPropertyValue('--rp-scale')) || 1; const r = n.getBoundingClientRect();
248
+ t.textContent = text; t.hidden = false;
249
+ t.style.setProperty('--rp-x', Math.round((r.left + r.width / 2) * k) + 'px'); t.style.setProperty('--rp-y', Math.round(r.top * k) + 'px');
250
+ }
251
+ const tipOff = (card) => { const t = card && card.querySelector('.rd-tip'); if (t) t.hidden = true; };
252
+
253
+ /* ── parts ── */
254
+ function bar() {
255
+ const c = S.cyc; const v = c.design_version;
256
+ return el('div', { class: 'rd-dk-bar' },
257
+ el('div', { class: 'chips2 rd-wrap' }, lk({ 'data-rd-act': 'brief', title: 'The design of this report', onclick: () => o.onBrief && o.onBrief() }, icon('spark', 'sm'), 'Design D' + v),
258
+ chip(icon('doc', 'sm'), count() + (isDeck() ? (count() === 1 ? ' slide' : ' slides') : (count() === 1 ? ' section' : ' sections'))),
259
+ matchChip(),
260
+ isDeck() && S.prevR ? el('div', { class: 'seg', role: 'group', 'aria-label': 'Deck view' },
261
+ btn({ 'data-rd-act': 'mode', 'data-v': 'deck', 'aria-pressed': String(S.mode === 'deck'), onclick: () => { S.mode = 'deck'; paint(); } }, 'Deck'),
262
+ btn({ 'data-rd-act': 'mode', 'data-v': 'cmp', 'aria-pressed': String(S.mode === 'cmp'), onclick: () => { S.mode = 'cmp'; paint(); } }, 'Side by side with ' + prevWord())) : null),
263
+ el('div', { class: 'chips2 rd-wrap' },
264
+ isDeck() ? pbtn('primary', 'Present', 'play', { 'data-rd-act': 'present', onclick: () => present('pres') }) : null,
265
+ isDeck() ? pbtn('outline', 'Audience view', 'eye', { 'data-rd-act': 'audience', onclick: () => present('aud') }) : null,
266
+ pbtn('outline', 'Refine the design', 'spark', { 'data-rd-act': 'studio', onclick: () => o.onStudio && o.onStudio() }),
267
+ pbtn('ghost', 'Print / PDF', 'pdf', { 'data-rd-act': 'print', onclick: () => doPrint() }),
268
+ isDeck() ? pbtn('ghost', 'Download .pptx', 'drop', { 'data-rd-act': 'pptx', onclick: () => S.api && S.api.download && S.api.download('pptx') }) : null));
269
+ }
270
+ function matchChip() {
271
+ const c = S.cyc;
272
+ if (c.rendering) return el('span', { class: 'chip2', 'data-rd-match': 'rendering' }, icon('clock', 'sm'), 'Drawing to match the design');
273
+ if (c.matches) return el('span', { class: 'chip2 ds-good', 'data-rd-match': 'yes' }, icon('check', 'sm'), 'Matches the design');
274
+ const open = openDiffs();
275
+ return open.length ? el('span', { class: 'chip2 ds-warn', 'data-rd-match': 'no' }, icon('alert', 'sm'), open.length + (open.length === 1 ? ' difference' : ' differences') + ' from the design')
276
+ : el('span', { class: 'chip2 ds-good', 'data-rd-match': 'kept' }, icon('check', 'sm'), 'Kept for this cycle');
277
+ }
278
+ const openDiffs = () => (S.cyc.differences || []).filter((d) => !(S.cyc.decisions || []).some((x) => x.diff === d.id));
279
+ function caps() {
280
+ return el('div', { class: 'rd-dk-caps', 'aria-label': 'What this ' + (isDeck() ? 'deck' : 'document') + ' can do', 'data-rd-caps': '' }, el('span', { class: 'dim mono' }, isDeck() ? 'MORE THAN SLIDES' : 'MORE THAN A PAGE'),
281
+ CAPS.filter(([ic]) => isDeck() || ic !== 'mic').map(([ic, t]) => chip(icon(ic, 'sm'), isDeck() ? t : t.replace('the slide', 'the document'))),
282
+ S.cyc.previous ? lk({ 'data-rd-act': 'chg', 'aria-pressed': String(S.chg), class: S.chg ? 'rd-on' : '', onclick: () => { S.chg = !S.chg; paint(); } }, icon('undo', 'sm'), (S.chg ? 'Hide' : 'Show') + ' what changed since ' + prevWord()) : null);
283
+ }
284
+ function rbar() {
285
+ const tg = (id, label, on, set) => el('label', { class: 'ds-tg' }, el('input', { type: 'checkbox', checked: on, 'data-rd-ck': id, onChange: (e) => set(e.target.checked) }), label);
286
+ return el('div', { class: 'ds-rbar' }, el('span', { class: 'ds-acts' }, tg('pins', 'Show notes on the ' + (isDeck() ? 'slide' : 'document'), S.pins, (v) => { S.pins = v; paint(); }), tg('src', 'Markdown source', false, (v) => { if (v && S.api && S.api.setSrc) S.api.setSrc(true); })),
287
+ el('span', { class: 'dim mono rd-small' }, 'Present, print and download stay on this device. Nothing is sent.'));
288
+ }
289
+ function copyNotice() {
290
+ if (o.output !== 'google-slides') return null;
291
+ return el('div', { class: 'fold', 'data-rd-copynote': '' }, icon('info', 'sm'), ' The deck now lives in PMOS. The Google Slides copy of this report is optional. ', lk({ onclick: () => S.api && S.api.toggleCopies && S.api.toggleCopies() }, 'Copies in other tools'));
292
+ }
293
+ function diffBox() {
294
+ const c = S.cyc; if (c.rendering) return el('div', { class: 'fold a', role: 'status', 'data-rd-rendering': '' }, icon('clock', 'sm'), ' PMOS is drawing this cycle again to match the design. You can leave this page.');
295
+ const D = c.differences || []; if (!D.length) return null;
296
+ const open = openDiffs();
297
+ const kept = D.filter((d) => (c.decisions || []).some((x) => x.diff === d.id && x.choice === 'keep'));
298
+ if (!open.length) return kept.length ? el('div', { class: 'fold', 'data-rd-kept': '' }, icon('check', 'sm'), ' Kept for this cycle: ' + kept.map((d) => d.what).join(' ') + ' The design stays as it is.') : null;
299
+ const frozen = o.state && o.state() === 'issued';
300
+ return el('section', { class: 'ds-stale rd-dk-diff', 'aria-label': 'Differs from the design', 'data-rd-diff': '' }, el('div', { class: 't' }, icon('alert', 'sm'), 'Differs from the design'),
301
+ open.map((d) => el('div', { class: 'rd-stack', 'data-rd-diffrow': d.id }, el('p', null, d.what),
302
+ frozen ? el('p', { class: 'dim' }, 'This cycle is issued, so it stays as it was drawn.') : el('div', { class: 'ds-acts' },
303
+ btn({ class: 'ds-btn', 'data-rd-decide': 'keep', onclick: () => decide(d, 'keep') }, 'Keep for this cycle'),
304
+ btn({ class: 'ds-btn ag', 'data-rd-decide': 'adopt', onclick: () => decide(d, 'adopt') }, icon('spark', 'sm'), 'Make it part of the design'),
305
+ btn({ class: 'ds-btn', 'data-rd-decide': 'match', onclick: () => decide(d, 'match') }, 'Match the design'),
306
+ d.slide && slides().some((s) => s.id === d.slide) ? lk({ 'data-rd-act': 'showslide', onclick: () => { S.mode = 'deck'; go(slides().findIndex((s) => s.id === d.slide)); } }, 'Show the slide') : null))));
307
+ }
308
+ async function decide(d, choice) {
309
+ const r = await RD.decide({ ws, path, diff: d.id, choice });
310
+ if (!r || !r.ok) { failed(r, 'It was not recorded'); return; }
311
+ toast(choice === 'keep' ? 'Kept for this cycle. The design stays as it is. It is in the Edit log.' : choice === 'adopt' ? 'Added to the design as a new version. Every next cycle follows it.' : 'PMOS draws this cycle again to match the design. It is in the Edit log.');
312
+ await load();
313
+ }
314
+ function thumbs(cur, horiz, onPick) {
315
+ return el('nav', { class: 'rd-dk-strip' + (horiz ? ' h' : ''), 'aria-label': 'Slides', 'data-rd-strip': '' }, slides().map((s, i) => {
316
+ const np = pinsOf(s.id).length;
317
+ return btn({ class: 'rd-dk-th', 'aria-current': String(cur === i), 'aria-label': 'Slide ' + s.n + ': ' + s.title, 'data-rd-go': String(i), onclick: () => onPick(i) },
318
+ el('div', { inert: '' }, slideCard(S.R, s, { cls: 'rd-thumb' })), el('span', { class: 'rd-dk-thn' }, String(s.n)),
319
+ np && !onPick.studio ? el('span', { class: 'rd-dk-thp', title: np + ' notes on the slide' }, icon('notes', 'sm'), String(np)) : null);
320
+ }));
321
+ }
322
+ function go(i) { if (i < 0 || i >= count()) return; S.cur = i; S.src = null; S.open = null; S.draft = null; S.placing = false; paint(); }
323
+ function pinLayer(id) {
324
+ if (!S.pins) return [];
325
+ const L = pinsOf(id).map((p) => { const b = btn({ class: 'rd-pindot ' + (p.by === 'pmos' ? 'a' : 'h') + (p.state === 'open' && p.by === 'pmos' ? ' prop' : '') + (S.open === p.id ? ' on' : ''), 'data-rd-pin': p.id, 'aria-label': 'Note ' + numOf(p) + ': ' + p.text, onclick: () => { S.open = S.open === p.id ? null : p.id; S.src = null; paint(); } }, String(numOf(p)));
326
+ b.style.setProperty('--rp-x', p.x + '%'); b.style.setProperty('--rp-y', p.y + '%'); return b; });
327
+ if (S.draft && S.draft.slide === id) { const d = el('span', { class: 'rd-pindot h on', 'aria-hidden': 'true' }, String(live().length + 1)); d.style.setProperty('--rp-x', S.draft.x + '%'); d.style.setProperty('--rp-y', S.draft.y + '%'); L.push(d); }
328
+ return L;
329
+ }
330
+ function cap(s) {
331
+ const frozen = o.state && o.state() === 'issued';
332
+ return [el('div', { class: 'rd-dk-cap' }, el('span', { class: 'chips2 rd-wrap' }, el('b', { 'data-rd-cap': '' }, 'Slide ' + s.n + ' of ' + count()), chip(icon('img', 'sm'), KIND_WORD(s.kind)), chip(icon('doc', 'sm'), s.title)),
333
+ el('span', { class: 'chips2' }, s.section && !frozen ? pbtn('ghost', 'Edit the words', 'edit', { 'data-rd-act': 'editwords', onclick: () => S.api && S.api.editAt && S.api.editAt(s.section) }) : null,
334
+ btn({ class: 'ib sm', 'aria-label': 'Previous slide', 'data-rd-act': 'prev', disabled: S.cur === 0, onclick: () => go(S.cur - 1) }, icon('prev', 'sm')),
335
+ btn({ class: 'ib sm', 'aria-label': 'Next slide', 'data-rd-act': 'next', disabled: S.cur >= count() - 1, onclick: () => go(S.cur + 1) }, icon('next', 'sm')))),
336
+ el('div', { class: 'dim rd-small' }, hintOf(s.kind))];
337
+ }
338
+ function speaker(s) {
339
+ const n = S.notes.speaker[s.id]; const sug = !n && S.notes.suggestions[s.id] ? S.notes.suggestions[s.id] : null;
340
+ const from = sug ? slotWord(sug.from_slot) : '';
341
+ const ta = el('textarea', { class: 'rd-spk', 'data-rd-spk': s.id, 'aria-label': 'Speaker notes for slide ' + s.n, placeholder: sug ? 'Keep the note from ' + from + ', or write a new one.' : 'What you want to say on this slide. Only you see it, in the presenter view.',
342
+ onInput: (e) => { clearTimeout(S.spkT[s.id]); const v = e.target.value; S.spkT[s.id] = setTimeout(() => saveSpk(s.id, v), 700); }, onBlur: (e) => { clearTimeout(S.spkT[s.id]); if ((S.notes.speaker[s.id] || { text: '' }).text !== e.target.value) saveSpk(s.id, e.target.value); } });
343
+ ta.value = n ? n.text : '';
344
+ return sect({ ic: 'mic', title: 'Speaker notes', right: el('span', { class: 'tag' }, 'Private'), attrs: { 'data-rd-spksec': s.id } },
345
+ sug ? el('div', { class: 'rd-carry', 'data-rd-carry': s.id }, el('span', { class: 'dim mono rd-small' }, 'FROM ' + from.toUpperCase() + ', SAME SLIDE'), el('p', null, sug.text),
346
+ el('span', { class: 'ds-acts' }, btn({ class: 'ds-btn', 'data-rd-act': 'keep', onclick: () => keepSpk(s.id) }, icon('check', 'sm'), 'Keep'), btn({ class: 'ds-btn', 'data-rd-act': 'clear', onclick: () => clearSpk(s.id) }, icon('x', 'sm'), 'Clear'))) : null,
347
+ ta, el('div', { class: 'dim rd-small' }, icon('lock', 'sm'), ' Only you see speaker notes. They are not on the slide, the PDF or the .pptx.'));
348
+ }
349
+ async function saveSpk(id, text) {
350
+ const r = await RD.speaker({ ws, path, slide: id, text });
351
+ if (!r || !r.ok) { failed(r, 'The speaker note was not saved'); return; }
352
+ S.notes.speaker[id] = { text, at: new Date().toISOString() };
353
+ const sv = root.querySelector('[data-rd-spksaved]'); if (sv) sv.textContent = 'Saved';
354
+ }
355
+ async function keepSpk(id) {
356
+ const r = await RD.speaker({ ws, path, slide: id, keep: true }); if (!r || !r.ok) { failed(r, 'The note was not kept'); return; }
357
+ S.notes.speaker[id] = { text: S.notes.suggestions[id].text, at: new Date().toISOString() }; paint(); toast('Kept the speaker note. Change it if you like.');
358
+ }
359
+ async function clearSpk(id) {
360
+ const r = await RD.speaker({ ws, path, slide: id, text: '' }); if (!r || !r.ok) { failed(r, 'The note was not cleared'); return; }
361
+ S.notes.speaker[id] = { text: '', at: new Date().toISOString() }; paint();
362
+ }
363
+ function srcCard() {
364
+ if (!S.src) return null;
365
+ const p = srcParts(S.src.ref); const ini = S.src.ini || (/^(ini|initiative)$/i.test(p.kind) ? p.id : null);
366
+ return el('section', { class: 'ds-sync', 'aria-label': 'Source', 'data-rd-src': S.src.ref }, el('div', { class: 'hd' }, el('b', null, icon(ini ? 'init' : 'org', 'sm'), ' Where "' + (S.src.label || p.id) + '" comes from'),
367
+ btn({ class: 'ib sm', 'aria-label': 'Close', onclick: () => { S.src = null; paint(); } }, icon('x', 'sm'))),
368
+ el('ul', null, el('li', null, el('div', { class: 'l1' }, icon(ini ? 'init' : 'org', 'sm'), el('b', null, p.word + ': ' + p.id)),
369
+ el('p', null, S.src.detail || (ini ? 'The slide shows the figures of this cycle. The initiative in PMOS shows them as they are today.' : 'The render names this source. PMOS read it when it drafted this cycle.')),
370
+ ini ? el('div', { class: 'ds-acts' }, btn({ class: 'ds-btn', 'data-rd-act': 'openini', onclick: () => openInitiative(ini) }, icon('out', 'sm'), 'Open the initiative')) : null)));
371
+ }
372
+ function notesSect(s) {
373
+ const id = s.id; const ps = pinsOf(id); const frozen = o.state && o.state() === 'issued';
374
+ const row = (p) => el('li', { class: 'rd-note' + (S.open === p.id ? ' on' : ''), 'data-rd-pinrow': p.id },
375
+ btn({ class: 'rd-pindot ' + (p.by === 'pmos' ? 'a' : 'h') + (p.state === 'open' && p.by === 'pmos' ? ' prop' : ''), 'aria-label': 'Note ' + numOf(p), onclick: () => { S.open = S.open === p.id ? null : p.id; paint(); } }, String(numOf(p))),
376
+ el('div', { class: 'ds-le' }, el('div', { class: 'l1' }, el('b', null, p.by === 'pmos' ? 'PMOS' : 'You'), el('span', { class: 'when' }, dayAt(p.at))),
377
+ p.by === 'pmos' && p.state === 'open' ? el('div', { class: 'who' }, el('span', { class: 'ds-ag' }, 'Proposed by PMOS')) : null,
378
+ S.open === p.id ? [S.editPin === p.id ? el('div', { class: 'ds-whyadd' }, el('input', { type: 'text', 'data-rd-pinedit': p.id, value: p.text, 'aria-label': 'Edit the note' }),
379
+ btn({ class: 'ds-btn pri', onclick: () => { const i = root.querySelector('[data-rd-pinedit]'); pinOp('edit', p, i && i.value.trim()); } }, 'Save'), btn({ class: 'ds-btn', onclick: () => { S.editPin = null; paint(); } }, 'Cancel'))
380
+ : el('p', { class: 'why' }, p.text),
381
+ el('div', { class: 'ds-acts' }, p.by === 'pmos' && p.state === 'open' ? [btn({ class: 'ds-btn pri', 'data-rd-pinact': 'keep', onclick: () => pinOp('keep', p) }, icon('check', 'sm'), 'Keep the note'), btn({ class: 'ds-btn', 'data-rd-pinact': 'dismiss', onclick: () => pinOp('dismiss', p) }, icon('x', 'sm'), 'Dismiss')]
382
+ : [btn({ class: 'ds-btn', 'data-rd-pinact': 'edit', onclick: () => { S.editPin = p.id; paint(); } }, icon('edit', 'sm'), 'Change the note'), btn({ class: 'ds-btn', 'data-rd-pinact': 'remove', onclick: () => pinOp('remove', p) }, icon('trash', 'sm'), 'Remove')],
383
+ (p.section || s.section) && !frozen ? btn({ class: 'ds-btn ag', 'data-rd-pinact': 'editthis', onclick: () => S.api && S.api.editAt && S.api.editAt(p.section || s.section) }, icon('edit', 'sm'), 'Edit this') : null)] : el('p', { class: 'why rd-clip' }, p.text)));
384
+ const draft = S.draft && S.draft.slide === id ? el('div', { class: 'ds-whyadd' }, el('input', { type: 'text', 'data-rd-pindraft': '', placeholder: 'Your note. Readers of the report see it.', 'aria-label': 'Note on the slide', onKeyDown: (e) => { if (e.key === 'Enter') savePin(); } }),
385
+ btn({ class: 'ds-btn pri', 'data-rd-act': 'pinsave', onclick: () => savePin() }, 'Pin it'), btn({ class: 'ds-btn', 'data-rd-act': 'pincancel', onclick: () => { S.draft = null; paint(); } }, 'Cancel')) : null;
386
+ const add = S.placing ? el('div', { class: 'fold a', role: 'status', 'data-rd-placing': '' }, icon('notes', 'sm'), ' Click the spot on the ' + (isDeck() ? 'slide' : 'document') + ' where the note goes. ', lk({ onclick: () => { S.placing = false; paint(); } }, 'Cancel'))
387
+ : el('div', { class: 'ds-acts' }, btn({ class: 'ds-btn', 'data-rd-act': 'place', onclick: () => { S.placing = true; S.draft = null; S.pins = true; paint(); } }, icon('plus', 'sm'), 'Add a note on the ' + (isDeck() ? 'slide' : 'document')));
388
+ return sect({ ic: 'notes', title: 'Notes on the ' + (isDeck() ? 'slide' : 'document'), count: ps.length, right: el('span', { class: 'tag acc' }, 'Readers see them'), attrs: { 'data-rd-notes': id } },
389
+ ps.length ? el('ul', { class: 'ds-log rd-notes' }, ps.map(row)) : el('div', { class: 'fold' }, icon('info', 'sm'), 'No notes here.'), draft || add,
390
+ el('div', { class: 'dim rd-small' }, 'Readers see these notes. They do not change the words: to change the words, use Edit this. Each note is in the Edit log.'));
391
+ }
392
+ async function savePin() {
393
+ const i = root.querySelector('[data-rd-pindraft]'); const text = i && i.value.trim(); if (!text) { if (i) i.focus(); return; }
394
+ const d = S.draft; const r = await RD.pin({ ws, path, op: 'add', slide: d.slide, x: d.x, y: d.y, text });
395
+ if (!r || !r.ok) { failed(r, 'The note was not pinned'); return; }
396
+ S.draft = null; await reloadNotes(); const p = (r.pin && r.pin.id) || (live().slice(-1)[0] || {}).id; S.open = p || null; paint();
397
+ toast('Pinned. Readers see it. It is in the Edit log.');
398
+ }
399
+ async function pinOp(op, p, text) {
400
+ if (op === 'edit' && !text) return;
401
+ const r = await RD.pin({ ws, path, op, id: p.id, slide: p.slide, x: p.x, y: p.y, text: op === 'edit' ? text : p.text });
402
+ if (!r || !r.ok) { failed(r, 'The note was not changed'); return; }
403
+ S.editPin = null; if (op === 'remove' || op === 'dismiss') S.open = null;
404
+ await reloadNotes(); paint();
405
+ toast(op === 'keep' ? 'Kept. Readers see the note.' : op === 'dismiss' ? 'Dismissed. It stays in the Edit log.' : op === 'remove' ? 'Removed. It stays in the Edit log.' : 'Changed. It is in the Edit log.');
406
+ }
407
+ async function reloadNotes() { const n = await RD.notes(ws, path); if (n && n.ok !== false) S.notes = { speaker: n.speaker || {}, suggestions: n.suggestions || {}, pins: n.pins || [] }; if (S.api && S.api.logChanged) S.api.logChanged(); }
408
+
409
+ /* ── the views ── */
410
+ function deckMain() {
411
+ const s = curSlide(); if (!s) return el('div', { class: 'fold w' }, icon('alert', 'sm'), 'The render has no slide.');
412
+ const big = slideCard(S.R, s, { cls: 'rd-big' + (S.placing ? ' placing' : ''), attrs: { 'data-rd-big': s.id }, focusable: true, over: [...pinLayer(s.id), el('span', { class: 'rd-tip', hidden: true, role: 'tooltip' })],
413
+ wire: (doc) => wireFrame(doc, hands(big, s, { key: stageKey })) });
414
+ return el('div', { class: 'rd-dk-main' }, thumbs(S.cur, false, go), el('div', { class: 'rd-dk-stage' }, ...cap(s), big,
415
+ el('div', { class: 'rd-dk-under' }, speaker(s), el('div', { class: 'rd-dk-ncol' }, srcCard(), notesSect(s)))));
416
+ }
417
+ function stageKey(e) { if (e.key === 'ArrowRight' || e.key === 'PageDown') { e.preventDefault(); go(S.cur + 1); } else if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.preventDefault(); go(S.cur - 1); } }
418
+ function cmpMain() {
419
+ const s = curSlide(); if (!s) return null;
420
+ const ps = S.prevR ? S.prevR.slides.find((x) => x.id === s.id) : null;
421
+ const was = []; const tmp = new DOMParser().parseFromString(s.html, 'text/html');
422
+ tmp.querySelectorAll('[data-from]').forEach((n) => was.push((n.textContent || '').trim() + ', was ' + n.getAttribute('data-from')));
423
+ const diff = ps ? (was.length ? was : ['The same words and numbers as ' + prevWord() + '.']) : ['This slide is new this cycle. ' + (openDiffs().length ? 'It is what differs from the design.' : '')];
424
+ /* each column says how its cycle stands against the design (#deck-cycles) */
425
+ const prevChip = () => { const pc = S.prev || {}; const n = (pc.differences || []).filter((d) => !(pc.decisions || []).some((x) => x.diff === d.id)).length;
426
+ return pc.matches || !n ? el('span', { class: 'chip2 ds-good' }, icon('check', 'sm'), pc.matches ? 'Matches the design' : 'Kept for that cycle') : el('span', { class: 'chip2 ds-warn' }, icon('alert', 'sm'), n + (n === 1 ? ' difference' : ' differences') + ' from the design'); };
427
+ const col = (label, state, R, sl, chip) => el('div', { class: 'rd-cmpc' }, el('div', { class: 'chips2 rd-cmph' }, el('b', null, label), el('span', { class: 'tag ' + (state === 'issued' ? 'ok' : 'acc') }, state === 'issued' ? 'Issued' : 'Draft'), chip),
428
+ sl ? slideCard(R, sl, { wire: (doc) => showChanges(doc, R === S.R && S.chg) }) : el('div', { class: 'fold rd-none' }, icon('info', 'sm'), ' ' + label + ' has no slide "' + s.title + '".'));
429
+ return [el('div', { class: 'rd-dk-cap' }, el('span', { class: 'chips2 rd-wrap' }, el('b', null, 'Slide ' + s.n + ' of ' + count() + ': ' + s.title), el('span', { class: 'tag ok' }, 'Same design, D' + S.cyc.design_version)),
430
+ el('span', { class: 'chips2' }, btn({ class: 'ib sm', 'aria-label': 'Previous slide', disabled: S.cur === 0, onclick: () => go(S.cur - 1) }, icon('prev', 'sm')), btn({ class: 'ib sm', 'aria-label': 'Next slide', disabled: S.cur >= count() - 1, onclick: () => go(S.cur + 1) }, icon('next', 'sm')))),
431
+ el('div', { class: 'rd-cmpg', 'data-rd-cmp': '' }, col(prevWord(), S.prev && S.prev.state ? S.prev.state : 'issued', S.prevR, ps, prevChip()), col(slotWord(o.slot) || 'This cycle', o.state ? o.state() : 'draft', S.R, s, matchChip())),
432
+ sect({ ic: 'cols', title: 'What differs on this slide', count: diff.length }, el('ul', { class: 'chgl' }, diff.map((t) => el('li', null, icon('edit', 'sm'), el('span', null, t), el('span')))),
433
+ el('div', { class: 'dim rd-small' }, 'Both cycles were drafted to match the saved design. Only the numbers and the words change, unless PMOS tells you something differs.')),
434
+ thumbs(S.cur, true, go)];
435
+ }
436
+ function docMain() {
437
+ const R = S.R; const card = el('div', { class: 'rd-docf' + (S.placing ? ' placing' : ''), 'data-rd-big': 'doc' });
438
+ const layer = el('div', { class: 'rd-doclayer' }); const tip = el('span', { class: 'rd-tip', hidden: true, role: 'tooltip' });
439
+ const box = (e, doc) => { 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 }; };
440
+ const f = frame(R.style, R.body, { title: 'The document', cls: 'pmos-doc', focusable: true, onLoad: (doc, fr) => {
441
+ const fit = () => { fr.style.setProperty('--rp-h', Math.max(200, doc.documentElement.scrollHeight) + 'px'); placeDocPins(doc, layer); };
442
+ fit(); setTimeout(fit, 120);
443
+ wireFrame(doc, { ...hands(card, { id: 'doc', box }), value: (t, n) => { tip.textContent = t; tip.hidden = false; const r = n.getBoundingClientRect(); tip.style.setProperty('--rp-x', Math.round(r.left + r.width / 2) + 'px'); tip.style.setProperty('--rp-y', Math.round(r.top) + 'px'); }, leave: () => { tip.hidden = true; }, more: () => fit() });
444
+ } });
445
+ card.append(f, layer, tip);
446
+ const all = live(); const secOf = (id) => R.secs.find((x) => x.id === id) || { id, title: id, section: '' };
447
+ const sec = S.open ? secOf((all.find((p) => p.id === S.open) || {}).slide) : (S.draft ? secOf(S.draft.slide) : { id: '__all', title: 'the document', section: '' });
448
+ return el('div', { class: 'rd-dk-stage' }, card, el('div', { class: 'rd-dk-under' }, el('div', { class: 'rd-dk-ncol' }, srcCard()), el('div', { class: 'rd-dk-ncol' }, notesSect({ ...sec, id: sec.id }))));
449
+ }
450
+ function placeDocPins(doc, layer) {
451
+ const L = [];
452
+ if (S.pins) {
453
+ for (const p of live()) {
454
+ const sec = doc.querySelector('section.pmos-sec[data-sec-id="' + String(p.slide).replace(/"/g, '') + '"]'); if (!sec) continue;
455
+ const r = sec.getBoundingClientRect(); const b = btn({ class: 'rd-pindot ' + (p.by === 'pmos' ? 'a' : 'h') + (p.state === 'open' && p.by === 'pmos' ? ' prop' : '') + (S.open === p.id ? ' on' : ''), 'data-rd-pin': p.id, 'aria-label': 'Note ' + numOf(p) + ': ' + p.text, onclick: () => { S.open = S.open === p.id ? null : p.id; paint(); } }, String(numOf(p)));
456
+ b.style.setProperty('--rp-x', Math.round(r.left + r.width * p.x / 100) + 'px'); b.style.setProperty('--rp-y', Math.round(r.top + r.height * p.y / 100) + 'px'); L.push(b);
457
+ }
458
+ if (S.draft) { const sec = doc.querySelector('section.pmos-sec[data-sec-id="' + String(S.draft.slide).replace(/"/g, '') + '"]'); if (sec) { const r = sec.getBoundingClientRect(); const d = el('span', { class: 'rd-pindot h on', 'aria-hidden': 'true' }, String(live().length + 1)); d.style.setProperty('--rp-x', Math.round(r.left + r.width * S.draft.x / 100) + 'px'); d.style.setProperty('--rp-y', Math.round(r.top + r.height * S.draft.y / 100) + 'px'); L.push(d); } }
459
+ }
460
+ put(layer, ...L);
461
+ }
462
+ /* pins in a document are keyed by section: a pin's section box is the box its x and y are read in */
463
+ function paint() {
464
+ if (!S.loaded || !S.cyc || !S.cyc.designed) { put(root); return; }
465
+ if (!S.R) { put(root, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), ' The render of this cycle could not be read: ' + (S.err || 'no answer') + '.')); return; }
466
+ const y = scroller() ? scroller().scrollTop : 0;
467
+ put(root, bar(), caps(), rbar(), copyNotice(), diffBox(), isDeck() ? (S.mode === 'cmp' && S.prevR ? cmpMain() : deckMain()) : docMain());
468
+ const sc = scroller(); if (sc) sc.scrollTop = y;
469
+ root.querySelectorAll('.rd-dk-th[aria-current="true"]').forEach((th) => { const st = th.parentNode; if (st.scrollHeight > st.clientHeight + 4) st.scrollTop = Math.max(0, th.offsetTop - st.offsetTop - 8); if (st.scrollWidth > st.clientWidth + 4) st.scrollLeft = Math.max(0, th.offsetLeft - st.offsetLeft - 8); });
470
+ }
471
+ const scroller = () => root.closest('.sb');
472
+ 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]) + ', ' + m[4] + ':' + m[5] : String(iso || ''); };
473
+
474
+ function doPrint() {
475
+ if (!S.R) return;
476
+ const ok = printDeck(S.R, { title: (o.name || 'Report') + (o.cycle ? ' ' + o.cycle : '') });
477
+ toast(ok ? 'The print view is open: one ' + (isDeck() ? 'slide' : 'document') + ' per page, without your speaker notes or the notes on the slide. Choose Save as PDF. Nothing is uploaded.' : 'The browser blocked the print window. Allow pop-ups for this page, then press Print again.');
478
+ }
479
+
480
+ /* ── Present: the presenter view and the audience view, on the .rp-deck stage ── */
481
+ let OV = null;
482
+ function closeOV() {
483
+ if (!OV) return; clearInterval(OV.timer); window.removeEventListener('keydown', OV.key, true); window.removeEventListener('resize', OV.fit);
484
+ OV.host.remove(); const i = OV.i; OV = null; S.cur = i; paint(); const b = root.querySelector('[data-rd-act="present"]'); if (b) b.focus();
485
+ }
486
+ function present(kind, start) {
487
+ if (!isDeck()) return;
488
+ if (OV) { OV.kind = kind; paintOV(); return; }
489
+ ensureC3Css();
490
+ const el0 = el('div', { class: 'rp-deck rd-ov ' + (kind === 'pres' ? 'rd-pres' : 'rd-aud'), open: '', role: 'dialog', 'aria-modal': 'true', 'aria-roledescription': 'carousel', tabindex: '-1', 'data-rd-ov': '' });
491
+ const host = el('div', { class: 'c3 c3e c3pop rd-ovhost', 'data-shell': 'present' }, el0);
492
+ OV = { kind, i: Math.max(0, Math.min(count() - 1, start == null ? S.cur : start)), el: el0, host, t0: Date.now(), timer: null, x0: null, y0: null };
493
+ OV.key = (e) => ovKey(e);
494
+ OV.fit = () => fitOV();
495
+ document.body.appendChild(host);
496
+ window.addEventListener('keydown', OV.key, true); window.addEventListener('resize', OV.fit);
497
+ el0.addEventListener('touchstart', touchStart, { passive: true }); el0.addEventListener('touchend', touchEnd, { passive: true });
498
+ OV.timer = setInterval(() => { const t = OV && OV.el.querySelector('[data-rd-timer]'); if (t) t.textContent = clock(); }, 1000);
499
+ paintOV(); el0.focus();
500
+ }
501
+ const touchStart = (e) => { const t = e.changedTouches[0]; if (OV) { OV.x0 = t.clientX; OV.y0 = t.clientY; } };
502
+ const touchEnd = (e) => { if (!OV || OV.x0 == null) return; const t = e.changedTouches[0]; const dx = t.clientX - OV.x0; const dy = t.clientY - OV.y0; OV.x0 = null; if (Math.abs(dx) > 40 && Math.abs(dx) > 1.5 * Math.abs(dy)) ovGo(OV.i + (dx < 0 ? 1 : -1)); };
503
+ function ovKey(e) {
504
+ if (!OV) return; const k = e.key;
505
+ if (k === 'Escape') { e.preventDefault(); e.stopImmediatePropagation(); closeOV(); return; }
506
+ const onBtn = e.target && e.target.closest && e.target.closest('button');
507
+ if (k === 'ArrowRight' || k === 'PageDown' || (k === ' ' && !onBtn)) { e.preventDefault(); e.stopImmediatePropagation(); ovGo(OV.i + 1); }
508
+ else if (k === 'ArrowLeft' || k === 'PageUp') { e.preventDefault(); e.stopImmediatePropagation(); ovGo(OV.i - 1); }
509
+ else if (k === 'Home') { e.preventDefault(); e.stopImmediatePropagation(); ovGo(0); }
510
+ else if (k === 'End') { e.preventDefault(); e.stopImmediatePropagation(); ovGo(count() - 1); }
511
+ }
512
+ function ovGo(j) { if (!OV) return; const n = Math.max(0, Math.min(count() - 1, j)); if (n === OV.i) return; OV.i = n; paintOV(); }
513
+ const clock = () => { const s = Math.floor((Date.now() - OV.t0) / 1000); return pad2(Math.floor(s / 60)) + ':' + pad2(s % 60); };
514
+ function fitOV() {
515
+ if (!OV) return;
516
+ if (OV.kind === 'aud') { const st = OV.el.querySelector('.rp-deck__stage'); if (!st) return; const r = st.getBoundingClientRect(); OV.el.style.setProperty('--rp-scale', String(Math.max(0.05, Math.min(r.width / W, r.height / H)).toFixed(4))); }
517
+ }
518
+ function ovBar(label) {
519
+ const N = count();
520
+ return el('div', { class: 'rp-deck__bar', role: 'toolbar', 'aria-label': label }, el('p', { class: 'rp-deck__label' }, label),
521
+ btn({ class: 'ib', 'aria-label': 'Previous slide', 'data-rd-ovact': 'prev', disabled: OV.i === 0, onclick: () => ovGo(OV.i - 1) }, icon('prev')),
522
+ el('p', { class: 'rp-deck__count', 'data-rd-count': '' }, (OV.i + 1) + ' / ' + N),
523
+ btn({ class: 'ib', 'aria-label': 'Next slide', 'data-rd-ovact': 'next', disabled: OV.i === N - 1, onclick: () => ovGo(OV.i + 1) }, icon('next')),
524
+ OV.kind === 'pres' ? pbtn('ghost', 'Audience view', 'eye', { 'data-rd-ovact': 'aud', onclick: () => { OV.kind = 'aud'; paintOV(); } }) : pbtn('ghost', 'Presenter view', 'mic', { 'data-rd-ovact': 'pres', onclick: () => { OV.kind = 'pres'; paintOV(); } }),
525
+ btn({ class: 'ib', 'aria-label': 'Close the slides (Escape)', 'data-rd-ovact': 'close', onclick: () => closeOV() }, icon('x')));
526
+ }
527
+ function paintOV() {
528
+ const s = slides()[OV.i]; const name = (o.name || 'Report') + (o.cycle ? ' · ' + o.cycle : '');
529
+ const fwd = (doc) => wireFrame(doc, { key: ovKey, touch: { start: touchStart, end: touchEnd }, src: (ref, n) => { const p = srcParts(ref); toast((n.textContent || '').trim() + ': ' + p.word + ' ' + p.id); }, initiative: (id) => toast('Initiative ' + id + '. Open it from the deck after the session.') });
530
+ OV.el.className = 'rp-deck rd-ov ' + (OV.kind === 'pres' ? 'rd-pres' : 'rd-aud');
531
+ OV.el.setAttribute('aria-label', OV.kind === 'pres' ? 'Presenter view' : 'Slides, full screen');
532
+ if (OV.kind === 'aud') {
533
+ const fr = frame(S.R.style, s.html, { title: 'Slide ' + s.n + ': ' + s.title, cls: 'pmos-one', onLoad: fwd });
534
+ put(OV.el, el('div', { class: 'rp-deck__stage' }, el('div', { class: 'rp-deck__frame rd-ovframe', 'aria-live': 'polite', 'data-rd-ovslide': s.id }, fr)), ovBar(name + '. Arrows, Space or a swipe move; Escape closes.'));
535
+ } else {
536
+ const nx = slides()[OV.i + 1]; const n = S.notes.speaker[s.id]; const sug = !n && S.notes.suggestions[s.id];
537
+ put(OV.el, el('div', { class: 'rd-pg' },
538
+ el('div', { class: 'rd-pmain' }, el('div', { class: 'rd-pp rd-plab' }, el('span', { class: 'chips2' }, el('span', { class: 'tag acc' }, 'Now'), el('b', null, 'Slide ' + s.n + ' of ' + count() + ': ' + s.title)), el('span', { class: 'dim mono rd-small' }, 'The audience sees only this slide')),
539
+ slideCard(S.R, s, { attrs: { 'data-rd-ovslide': s.id }, wire: fwd }),
540
+ el('div', { class: 'rd-pp', 'data-rd-ovnote': '' }, el('div', { class: 'rd-ph' }, icon('mic', 'sm'), 'Speaker notes ', el('span', { class: 'tag' }, 'Private')),
541
+ n && n.text ? el('p', { class: 'rd-pnote' }, n.text) : sug ? el('p', { class: 'rd-pnote dim' }, 'From ' + slotWord(sug.from_slot) + ', not kept yet: ' + sug.text) : el('p', { class: 'dim rd-m0' }, 'No speaker note for this slide.'))),
542
+ el('div', { class: 'rd-pside' }, el('div', { class: 'rd-pp rd-ptime' }, el('div', null, el('div', { class: 'rd-ph' }, icon('clock', 'sm'), 'Time'), el('b', { class: 'rd-timer', 'data-rd-timer': '' }, clock())), el('div', null, el('div', { class: 'rd-ph' }, 'Slide'), el('b', { class: 'rd-timer', 'data-rd-pn': '' }, s.n + ' / ' + count()))),
543
+ el('div', { class: 'rd-pp' }, el('div', { class: 'rd-ph' }, icon('next', 'sm'), 'Next'), nx ? [slideCard(S.R, nx, { attrs: { 'data-rd-ovnext': nx.id } }), el('div', { class: 'dim rd-small' }, nx.n + '. ' + nx.title)] : el('p', { class: 'dim rd-m0' }, 'This is the last slide.')),
544
+ el('div', { class: 'rd-pp' }, el('div', { class: 'rd-ph' }, icon('menu', 'sm'), 'Jump to'), el('ol', { class: 'rd-jump' }, slides().map((x, j) => el('li', null, btn({ 'data-rd-jump': String(j), 'aria-current': String(j === OV.i), onclick: () => ovGo(j) }, el('span', { class: 'mono' }, pad2(x.n)), x.title, S.notes.speaker[x.id] && S.notes.speaker[x.id].text ? icon('mic', 'sm') : null))))))),
545
+ ovBar('Presenter view · ' + name + '. Only you see this screen. Nothing is sent.'));
546
+ }
547
+ requestAnimationFrame(fitOV);
548
+ if (!OV.el.contains(document.activeElement)) OV.el.focus();
549
+ }
550
+
551
+ load();
552
+ return {
553
+ state: S,
554
+ node(api) { S.api = api; if (!S.loaded || !S.cyc || !S.cyc.designed) return null; paint(); return root; },
555
+ tools() { return S.loaded && S.cyc && S.cyc.designed && isDeck() ? [btn({ class: 'ib sm', 'aria-label': 'Present', title: 'Present', 'data-rd-act': 'present-top', onclick: () => present('pres') }, icon('play', 'sm'))] : []; },
556
+ present, reload: load, close: closeOV,
557
+ };
558
+ }
@@ -0,0 +1,20 @@
1
+ /* pm/c3-report-frame.css — inside the sandboxed frame of a report render (initiative report-design-studio; c3-report-deck.js frameDoc).
2
+ The render brings its own <style> (linked after this sheet, so its rules win). This sheet only sets the page box and the affordances the
3
+ parent page wires: a source chip, a value with a detail, a part that expands, a live initiative link, and what changed since the last cycle.
4
+ Colours are the slide tokens of tokens.css (linked first). */
5
+ html, body { margin: 0; padding: 0; background: var(--slide-bg); color: var(--slide-fg); font-family: var(--sans); }
6
+ body.pmos-one { width: 1280px; height: 720px; overflow: hidden; }
7
+ body.pmos-one > .pmos-slide { width: 1280px; height: 720px; box-sizing: border-box; overflow: hidden; }
8
+ body.pmos-doc { overflow: hidden; }
9
+ [data-src], [data-initiative], [data-more] { cursor: pointer; }
10
+ [data-src] { text-decoration: underline dotted; text-underline-offset: 3px; }
11
+ [data-value] { cursor: help; }
12
+ [data-src]:focus-visible, [data-initiative]:focus-visible, [data-more]:focus-visible, [data-value]:focus-visible { outline: 2px solid color-mix(in oklab, var(--slide-primary) 60%, var(--slide-fg)); outline-offset: 2px; }
13
+ [data-more] .pmos-more { display: none; }
14
+ [data-more][data-open] .pmos-more { display: revert; }
15
+ .pmos-chg { background: color-mix(in oklab, var(--slide-note-warning) 22%, transparent); }
16
+ .pmos-was { margin-left: 6px; font-weight: 400; color: var(--slide-muted); white-space: nowrap; }
17
+ /* print: one slide per 1280 x 720 page, nothing else */
18
+ @page deck { size: 1280px 720px; margin: 0; }
19
+ body.pmos-print-deck > .pmos-slide { page: deck; break-after: page; width: 1280px; height: 720px; overflow: hidden; box-sizing: border-box; }
20
+ body.pmos-print-deck > .pmos-slide:last-child { break-after: auto; }