create-pmos 0.53.1 → 0.53.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/REMOTE.md +87 -6
- package/workspace/first-paint-audit.js +6 -2
- package/workspace/fixtures/initiatives-api/list.json +13 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +13 -0
- package/workspace/fixtures/report-api/draft.json +105 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +12 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiatives-roadmap.md +1 -0
- package/workspace/guide/initiatives-screen.md +1 -0
- package/workspace/guide/layout.md +3 -2
- package/workspace/guide/report-slides.md +28 -0
- package/workspace/guide/reports.md +2 -0
- package/workspace/lib/auto-sync.js +83 -0
- package/workspace/lib/remote-access.js +96 -16
- package/workspace/lib/report-draft.js +33 -4
- package/workspace/lib/report-slides-data.js +141 -0
- package/workspace/lib/routes-initiatives.js +35 -2
- package/workspace/lib/routes-reports.js +3 -3
- package/workspace/lib/sync.js +61 -6
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +16 -1
- package/workspace/public/pm/components.css +783 -0
- package/workspace/public/pm/dest-reports.js +8 -2
- package/workspace/public/pm/initiative-list.js +18 -1
- package/workspace/public/pm/initiatives-copy.js +4 -0
- package/workspace/public/pm/print.css +11 -0
- package/workspace/public/pm/report-slides.js +438 -0
- package/workspace/public/pm/report-view.js +8 -2
- package/workspace/public/pm/reports-copy.js +3 -0
- package/workspace/public/pm/roadmap.js +23 -0
- package/workspace/public/pm/slides-copy.js +52 -0
- package/workspace/public/pm/tokens.css +13 -0
- package/workspace/public/pm/topbar.js +5 -2
- package/workspace/replaced-tests.json +23 -1
- package/workspace/server.js +33 -13
- package/workspace/skills/report-biweekly.skill +1 -0
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/run6.json +9 -0
- package/workspace/test-lists/workspace-public-host.json +5 -0
|
@@ -23,7 +23,8 @@ import { pmGet, pmPost } from './http.js';
|
|
|
23
23
|
import { watchSig } from './sig-watch.js';
|
|
24
24
|
import { wsOf } from './data.js';
|
|
25
25
|
import { COPY, cadenceName } from './reports-copy.js';
|
|
26
|
-
import {
|
|
26
|
+
import { openIssueSheet, openNotesSheet, draftNow, printReport, copyMarkdown, clearPrint, ensurePrintCss } from './report-view.js';
|
|
27
|
+
import { openSlides, slideCards } from './report-slides.js';
|
|
27
28
|
|
|
28
29
|
const SHOWN = 4; // the reports listed before "Show older"
|
|
29
30
|
const insts = new WeakMap(); // host -> instance
|
|
@@ -227,6 +228,10 @@ function viewerBlock(inst) {
|
|
|
227
228
|
const bar = el('div', { class: 'rp-toolbar', role: 'toolbar', 'aria-label': COPY.reportName(r.cadence, r.slot) });
|
|
228
229
|
bar.appendChild(el('h3', { class: 'rp-section__title rp-toolbar__title' }, COPY.reportName(r.cadence, r.slot)));
|
|
229
230
|
const acts = el('div', { class: 'rp-toolbar__acts' });
|
|
231
|
+
// Present: the slides full screen (Run 6, R22.7); it reads the file and writes nothing
|
|
232
|
+
const present = button({ label: COPY.present, variant: 'outline', size: 'sm', icon: 'columns-2', attrs: { 'data-rp-act': 'present', 'data-rp-key': 'present' }, disabled: !doc,
|
|
233
|
+
onClick: () => doc && openSlides({ md: doc.markdown, label: COPY.reportName(r.cadence, r.slot), printable: !!r.issued, returnFocus: present }) });
|
|
234
|
+
acts.appendChild(present);
|
|
230
235
|
if (r.issued) {
|
|
231
236
|
acts.appendChild(button({ label: COPY.print, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'print', 'data-rp-key': 'print' }, disabled: !doc, onClick: () => doc && printReport(doc.markdown) }));
|
|
232
237
|
acts.appendChild(button({ label: COPY.copyMd, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'copy', 'data-rp-key': 'copy' }, disabled: !doc, onClick: () => doc && copyMarkdown(doc.markdown) }));
|
|
@@ -246,7 +251,8 @@ function viewerBlock(inst) {
|
|
|
246
251
|
bar.appendChild(acts);
|
|
247
252
|
sec.appendChild(bar);
|
|
248
253
|
if (r.issued) sec.appendChild(el('p', { class: 'rp-viewer__state' }, r.edited ? COPY.editedWhy : COPY.frozen));
|
|
249
|
-
|
|
254
|
+
// the report is read as its slides (Run 6): the PM-OS deck, scaled to the column
|
|
255
|
+
sec.appendChild(doc ? slideCards(doc.markdown) : el('p', { class: 'rp-section__empty' }, COPY.loading));
|
|
250
256
|
return sec;
|
|
251
257
|
}
|
|
252
258
|
|
|
@@ -23,6 +23,7 @@ import { watchSig } from './sig-watch.js';
|
|
|
23
23
|
import { COPY, shortDate } from './initiatives-copy.js';
|
|
24
24
|
import { getList, sigKey, place, placed, leaveNow, setLifecycleBulk } from './initiatives-api.js';
|
|
25
25
|
import { healthSignal, say, confirmSheet, checkBySheet, onContextMenu } from './initiatives-ui.js';
|
|
26
|
+
import { openDoc } from './panel-initiatives.js';
|
|
26
27
|
|
|
27
28
|
const ORDER = { now: 0, next: 1, later: 2 };
|
|
28
29
|
const MAX_BULK = 20;
|
|
@@ -143,6 +144,20 @@ export function mountList(host, env) {
|
|
|
143
144
|
el('h3', { class: 'in-group__head' }, head), g.anchors.map(anchorEl));
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
// The units with documents and no record (routes-initiatives.js unrecordedOf), last: they have no anchor to group by.
|
|
148
|
+
function unrecordedEl(list) {
|
|
149
|
+
return el('section', { class: 'in-group', 'data-in-group': 'unrecorded' },
|
|
150
|
+
el('h3', { class: 'in-group__head' }, el('span', { class: 'in-group__name' }, COPY.noRecordGroup)),
|
|
151
|
+
el('div', { class: 'in-rows', role: 'list' }, list.map((u) => {
|
|
152
|
+
const doc = u.artifacts.find((a) => a.role === 'PRD') || u.artifacts[0];
|
|
153
|
+
return row({
|
|
154
|
+
title: u.title, meta: [COPY.noRecord],
|
|
155
|
+
onOpen: () => openDoc(null, doc.path, doc.role || 'DOC'), openLabel: COPY.aria.open(u.title),
|
|
156
|
+
attrs: { class: 'in-row', 'data-in-unrecorded-row': u.id }, openAttrs: { 'data-in-unrecorded-open': u.id },
|
|
157
|
+
});
|
|
158
|
+
})));
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
function bulkBar() {
|
|
147
162
|
const n = picked.size;
|
|
148
163
|
const can = env.grants().includes('lifecycle-bulk');
|
|
@@ -167,12 +182,14 @@ export function mountList(host, env) {
|
|
|
167
182
|
return;
|
|
168
183
|
}
|
|
169
184
|
const kids = [];
|
|
170
|
-
|
|
185
|
+
const unrecorded = data.unrecorded || [];
|
|
186
|
+
if (!allRows().length && !unrecorded.length) kids.push(empty({ text: COPY.emptyAll }));
|
|
171
187
|
else {
|
|
172
188
|
kids.push(el('p', { class: 'in-note in-hint', 'data-in-hint': '' }, COPY.menuHintRow));
|
|
173
189
|
kids.push(el('div', { class: 'in-toolbar' }, selecting ? bulkBar() : button({ label: COPY.select, variant: 'ghost', size: 'sm', onClick: () => { selecting = true; render(); }, attrs: { 'data-in-select-mode': '' } })));
|
|
174
190
|
kids.push(...data.groups.map(groupEl));
|
|
175
191
|
}
|
|
192
|
+
if (unrecorded.length) kids.push(unrecordedEl(unrecorded));
|
|
176
193
|
root.replaceChildren(...kids);
|
|
177
194
|
if (active) { const t = root.querySelector('[data-in-open="' + CSS.escape(active) + '"], [data-in-select="' + CSS.escape(active) + '"]'); if (t) t.focus({ preventScroll: true }); }
|
|
178
195
|
}
|
|
@@ -41,6 +41,10 @@ export const COPY = {
|
|
|
41
41
|
emptyColumn: { now: 'Nothing in Now.', next: 'Nothing in Next.', later: 'Nothing in Later.' },
|
|
42
42
|
dormant: (n) => `${n} dormant`,
|
|
43
43
|
hideDormant: (n) => `Hide ${n} dormant`,
|
|
44
|
+
unrecorded: (n) => `${n} with no record`,
|
|
45
|
+
hideUnrecorded: (n) => `Hide ${n} with no record`,
|
|
46
|
+
noRecord: 'no record yet',
|
|
47
|
+
noRecordGroup: 'No record yet',
|
|
44
48
|
unconfirmed: 'unconfirmed',
|
|
45
49
|
checkBy: (d) => `check-by ${d}`,
|
|
46
50
|
waitingYou: 'waiting on you',
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
@page {
|
|
6
6
|
margin: 16mm;
|
|
7
7
|
}
|
|
8
|
+
/* the slides (Run 6): each slide is one 1280 × 720 px page (13.333in × 7.5in), 16:9, with no margin */
|
|
9
|
+
@page slides {
|
|
10
|
+
size: 13.3333in 7.5in;
|
|
11
|
+
margin: 0;
|
|
12
|
+
}
|
|
8
13
|
@media print {
|
|
9
14
|
body > *:not(.rp-print) {
|
|
10
15
|
display: none !important;
|
|
@@ -30,4 +35,10 @@
|
|
|
30
35
|
.rp-print .rp-doc__note {
|
|
31
36
|
break-inside: avoid;
|
|
32
37
|
}
|
|
38
|
+
.rp-print--slides .rp-slide {
|
|
39
|
+
page: slides;
|
|
40
|
+
break-after: page;
|
|
41
|
+
print-color-adjust: exact;
|
|
42
|
+
-webkit-print-color-adjust: exact;
|
|
43
|
+
}
|
|
33
44
|
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/* pm/report-slides.js — the biweekly as slides (pm-os-absorb Run 6; PRD R22.7, open question Q3 answered "slides").
|
|
2
|
+
*
|
|
3
|
+
* The PM asked for the PM-OS biweekly deck, cut to four kinds of slide (2026-10-06): the roadmap snapshot, the
|
|
4
|
+
* initiative list, one slide per initiative and the changelog. Each is drawn on the PM-OS canvas: 1280 × 720, a
|
|
5
|
+
* numbered blue header band, a footer with the page number and the report's name.
|
|
6
|
+
*
|
|
7
|
+
* The slides read the report file and nothing else. The drafter (lib/report-draft.js) folds the facts into one
|
|
8
|
+
* `<!-- pmos:slides v1 {json} -->` block at the end of the draft, so an issued report freezes its slides with its
|
|
9
|
+
* text. The PM's words are the note markers: an initiative's note is the box on its slide, and `report.changelog` is
|
|
10
|
+
* the release notes. This file computes no fact and writes nothing.
|
|
11
|
+
* slidesOf(md) the report as a list of slides (pure, no DOM); [] when the file has no slides block
|
|
12
|
+
* renderSlide(s) one slide as DOM nodes, at 1280 × 720
|
|
13
|
+
* slideCards(md) every slide, scaled to the width of the Reports viewer
|
|
14
|
+
* openSlides({ md }) Present: full screen on black; arrow keys, Space, PageUp/PageDown, Home/End, a swipe, Escape
|
|
15
|
+
* printSlides(md) every slide on its own 1280 × 720 page (print.css), then window.print()
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { el } from './dom.js';
|
|
19
|
+
import { button, iconButton, layer, ensureComponentsCss } from './primitives.js';
|
|
20
|
+
import { icon, ensureSprite } from './icons.js';
|
|
21
|
+
import { parseReport, ensurePrintCss, clearPrint } from './report-view.js';
|
|
22
|
+
import { COPY } from './slides-copy.js';
|
|
23
|
+
|
|
24
|
+
export const W = 1280; // the PM-OS slide canvas, in CSS px (13.333in × 7.5in on paper)
|
|
25
|
+
export const H = 720;
|
|
26
|
+
const ROADMAP_LINES = 9; // group and initiative rows on one roadmap slide (52px each)
|
|
27
|
+
const LIST_ROWS = 10; // initiatives on one list slide (49px each)
|
|
28
|
+
const COLUMN_CAP = 6; // items in a Last / Next cycle column before "+N more"
|
|
29
|
+
const NOTE_LINES = 4; // lines in an initiative's note box
|
|
30
|
+
const CHANGELOG_BUDGET = 560; // px of release notes on one changelog slide
|
|
31
|
+
const SLIDES_RE = /<!-- pmos:slides v1 (\{[^\n]*\}) -->\s*$/; // the drafter's block: one line, last in the file
|
|
32
|
+
const STAGE_TONE = { intake: 'intake', brief: 'intake', spec: 'spec', draft: 'spec', contract: 'spec', build: 'build', evaluate: 'evaluate', handoff: 'handoff' };
|
|
33
|
+
const EARLY = /^(intake|brief|spec|draft|contract|unknown)$/i; // a lifecycle badge before the build reads muted
|
|
34
|
+
const TYPES = ['launch', 'improvement', 'fix', 'security', 'deprecation', 'beta'];
|
|
35
|
+
const KINDS = ['added', 'changed', 'fixed', 'deprecated', 'security', 'removed'];
|
|
36
|
+
|
|
37
|
+
/* ── dates, the PM-OS way ─────────────────────────────────────────────────────────────────────────────────────────── */
|
|
38
|
+
|
|
39
|
+
const utc = (ymd) => new Date(String(ymd).slice(0, 10) + 'T00:00:00Z');
|
|
40
|
+
const fmt = (ymd, opts, loc = 'en-GB') => utc(ymd).toLocaleDateString(loc, { timeZone: 'UTC', ...opts });
|
|
41
|
+
const dayBefore = (ymd) => new Date(utc(ymd).getTime() - 86400000).toISOString().slice(0, 10);
|
|
42
|
+
/** "10 Jun 26" */
|
|
43
|
+
export const msDate = (ymd) => (ymd ? fmt(ymd, { day: 'numeric', month: 'short', year: '2-digit' }) : '');
|
|
44
|
+
/** "28 Sept → 4 Oct"; inside one month "5 Oct → 11". Both ends are included. */
|
|
45
|
+
export function rangeText(from, to) {
|
|
46
|
+
if (!from || !to) return '';
|
|
47
|
+
const a = fmt(from, { day: 'numeric', month: 'short' });
|
|
48
|
+
return from.slice(0, 7) === to.slice(0, 7) ? a + ' → ' + Number(to.slice(8, 10)) : a + ' → ' + fmt(to, { day: 'numeric', month: 'short' });
|
|
49
|
+
}
|
|
50
|
+
const longDate = (ymd) => fmt(ymd, { day: 'numeric', month: 'long', year: 'numeric' });
|
|
51
|
+
const monthName = (ym) => fmt(ym + '-01', { month: 'short' }, 'en-US');
|
|
52
|
+
const monthTitle = (ymd) => fmt(ymd, { month: 'long', year: 'numeric' }, 'en-US');
|
|
53
|
+
|
|
54
|
+
/* ── the report as slides (pure) ──────────────────────────────────────────────────────────────────────────────────── */
|
|
55
|
+
|
|
56
|
+
/** The slides block of the report, or null when the file has none (a draft from before Run 6, or a hand-written one). */
|
|
57
|
+
export function slidesBlock(md) {
|
|
58
|
+
const m = SLIDES_RE.exec(String(md || '').replace(/\r\n/g, '\n'));
|
|
59
|
+
if (!m) return null;
|
|
60
|
+
try { const d = JSON.parse(m[1]); return d && d.v === 1 ? d : null; } catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The release notes the PM writes in `report.changelog`. Entries are separated by a blank line:
|
|
64
|
+
* Launch · Sign-in: Email codes for every title (2026-10-02)
|
|
65
|
+
* Readers can sign in with a one-time code.
|
|
66
|
+
* - Added: Code entry screen
|
|
67
|
+
* The first line is `Type[ · Area]: Title[ (YYYY-MM-DD)]`; a plain line is the summary; `- Kind: text` is a change. */
|
|
68
|
+
export function parseChangelog(text, fallbackDate) {
|
|
69
|
+
// Line by line, not paragraph by paragraph: md-newlines --fix joins a paragraph into one line and puts a blank line
|
|
70
|
+
// before a list, so an entry must read the same with or without blank lines. An entry starts on a typed head line
|
|
71
|
+
// ("Launch · Area: Title"), or on a plain line after a blank line once the entry before has a summary or a change.
|
|
72
|
+
const out = [];
|
|
73
|
+
let e = null; let gap = true;
|
|
74
|
+
for (const raw of String(text || '').replace(/\r\n/g, '\n').split('\n')) {
|
|
75
|
+
const ln = raw.trim().replace(/^#{1,6}\s+/, '');
|
|
76
|
+
if (!ln) { gap = true; continue; }
|
|
77
|
+
const head = /^([A-Za-z]+)(?:\s*·\s*([^:]+?))?\s*:\s*(.+?)(?:\s*\((\d{4}-\d{2}-\d{2})\))?$/.exec(ln);
|
|
78
|
+
const c = /^-\s+([A-Za-z]+)\s*:\s*(.+)$/.exec(ln);
|
|
79
|
+
const item = /^-\s+/.test(ln);
|
|
80
|
+
if (head && !item && TYPES.includes(head[1].toLowerCase())) {
|
|
81
|
+
e = { type: head[1].toLowerCase(), area: head[2] ? head[2].trim() : '', title: head[3].trim(), date: head[4] || fallbackDate, summary: [], changes: [] };
|
|
82
|
+
out.push(e);
|
|
83
|
+
} else if (!e || (!item && gap && (e.summary.length || e.changes.length))) {
|
|
84
|
+
e = { type: 'improvement', area: '', title: ln.replace(/^-\s+/, ''), date: fallbackDate, summary: [], changes: [] };
|
|
85
|
+
out.push(e);
|
|
86
|
+
} else if (c && KINDS.includes(c[1].toLowerCase())) e.changes.push({ kind: c[1].toLowerCase(), text: c[2].trim() });
|
|
87
|
+
else if (item) e.changes.push({ kind: 'changed', text: ln.replace(/^-\s+/, '') });
|
|
88
|
+
else e.summary.push(ln);
|
|
89
|
+
gap = false;
|
|
90
|
+
}
|
|
91
|
+
for (const x of out) x.summary = x.summary.join(' ');
|
|
92
|
+
return out.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Roadmap lines, nine to a slide: a slide never ends on a group label, and a group that runs on repeats its label. */
|
|
96
|
+
function roadmapPages(lines) {
|
|
97
|
+
const pages = [[]];
|
|
98
|
+
for (const ln of lines) {
|
|
99
|
+
let cur = pages[pages.length - 1];
|
|
100
|
+
const room = ROADMAP_LINES - cur.length;
|
|
101
|
+
if (room <= 0 || (!ln.row && room <= 1)) { cur = []; pages.push(cur); if (ln.row) cur.push({ group: ln.group }); }
|
|
102
|
+
cur.push(ln);
|
|
103
|
+
}
|
|
104
|
+
return pages;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const chunks = (xs, n) => { const out = []; for (let i = 0; i < xs.length; i += n) out.push(xs.slice(i, i + n)); return out.length ? out : [[]]; };
|
|
108
|
+
|
|
109
|
+
/** The height one release-notes entry takes, near enough to fill a slide without running off it. */
|
|
110
|
+
const entryHeight = (e) => 20 + 30 + Math.ceil((e.summary || '').length / 150) * 23 + e.changes.length * 24 + 28;
|
|
111
|
+
|
|
112
|
+
/** "northwind | Biweekly · 16 Oct → 29 | 16 October 2026" */
|
|
113
|
+
function footOf(meta) {
|
|
114
|
+
return meta.product + ' | ' + meta.cadence + ' · ' + rangeText(meta.next.from, dayBefore(meta.next.to)) + ' | ' + longDate(meta.slot || meta.today);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The report as slides. Pure: the same file gives the same slides. Each slide carries its own number. */
|
|
118
|
+
export function slidesOf(md) {
|
|
119
|
+
const d = slidesBlock(md);
|
|
120
|
+
if (!d) return [];
|
|
121
|
+
const notes = new Map(parseReport(md).filter((b) => b.t === 'note').map((b) => [b.id, b.text.trim()]));
|
|
122
|
+
const slides = [];
|
|
123
|
+
|
|
124
|
+
// 1. the roadmap snapshot: group label lines and initiative lines, nine to a slide
|
|
125
|
+
const lines = [];
|
|
126
|
+
for (const g of d.roadmap.groups) { lines.push({ group: g.name }); for (const r of g.rows) lines.push({ group: g.name, row: r }); }
|
|
127
|
+
for (const part of roadmapPages(lines)) slides.push({ kind: 'roadmap', title: COPY.roadmap, months: d.roadmap.months, today: d.roadmap.today, lines: part });
|
|
128
|
+
|
|
129
|
+
// 2. the initiative list, ten to a slide
|
|
130
|
+
for (const part of chunks(d.list, LIST_ROWS)) slides.push({ kind: 'list', title: COPY.list, rows: part, today: d.meta.today });
|
|
131
|
+
|
|
132
|
+
// 3. one slide per initiative, its note in the box
|
|
133
|
+
for (const x of d.initiatives) slides.push({ kind: 'initiative', title: x.title, init: x, note: notes.get(x.id) || '', meta: d.meta });
|
|
134
|
+
|
|
135
|
+
// 4. the changelog: the PM's own release notes, typed into the report.changelog note; nothing written, nothing shown
|
|
136
|
+
const entries = parseChangelog(notes.get('report.changelog'), d.meta.today);
|
|
137
|
+
const pages = [[]];
|
|
138
|
+
let used = 0;
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
const h = entryHeight(e) + 36;
|
|
141
|
+
if (used + h > CHANGELOG_BUDGET && pages[pages.length - 1].length) { pages.push([]); used = 0; }
|
|
142
|
+
pages[pages.length - 1].push(e); used += h;
|
|
143
|
+
}
|
|
144
|
+
for (const part of pages) slides.push({ kind: 'changelog', title: COPY.changelog, entries: part });
|
|
145
|
+
|
|
146
|
+
slides.forEach((s, i) => { s.n = i + 1; s.foot = footOf(d.meta); });
|
|
147
|
+
return slides;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ── one slide as DOM ─────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
151
|
+
|
|
152
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
153
|
+
const span = (cls, text) => el('span', { class: cls }, text);
|
|
154
|
+
const glyph = (name, cls) => el('span', { class: cls, 'aria-hidden': 'true' }, icon(name));
|
|
155
|
+
/** Run-time geometry, as the layout's own custom properties (R28-1). */
|
|
156
|
+
function placed(node, vars) { for (const [k, v] of Object.entries(vars)) node.style.setProperty(k, v); return node; }
|
|
157
|
+
|
|
158
|
+
function frame(s, body) {
|
|
159
|
+
return el('section', { class: 'rp-slide rp-slide--' + s.kind, 'data-slide': s.kind, 'aria-roledescription': 'slide', 'aria-label': s.title },
|
|
160
|
+
el('header', { class: 'rp-slide__header' },
|
|
161
|
+
el('div', { class: 'rp-slide__num' }, pad2(s.n)),
|
|
162
|
+
el('div', { class: 'rp-slide__band' }, el('h2', { class: 'rp-slide__title' }, s.title))),
|
|
163
|
+
el('div', { class: 'rp-slide__body' }, body),
|
|
164
|
+
el('footer', { class: 'rp-slide__footer' },
|
|
165
|
+
span('rp-slide__page', pad2(s.n)), span('rp-slide__meta', s.foot)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* the roadmap: a 300px column of names, then six months of bars */
|
|
169
|
+
const MONTH_W = (1212 - 300) / 6;
|
|
170
|
+
function xOf(months, ymd) {
|
|
171
|
+
const ym = ymd.slice(0, 7);
|
|
172
|
+
const k = months.indexOf(ym);
|
|
173
|
+
if (k < 0) return ym < months[0] ? -8 : months.length * MONTH_W + 8;
|
|
174
|
+
const days = new Date(Date.UTC(Number(ym.slice(0, 4)), Number(ym.slice(5, 7)), 0)).getUTCDate();
|
|
175
|
+
return (k + (Number(ymd.slice(8, 10)) - 1) / days) * MONTH_W;
|
|
176
|
+
}
|
|
177
|
+
const DAY_W = MONTH_W / 30;
|
|
178
|
+
const pos = (left, width) => ({ '--rp-x': Math.round(left) + 'px', '--rp-w': Math.max(4, Math.round(width)) + 'px' });
|
|
179
|
+
|
|
180
|
+
function roadmapBody(s) {
|
|
181
|
+
const names = el('div', { class: 'rp-slide__gnames' },
|
|
182
|
+
el('div', { class: 'rp-slide__ghead' }, el('p', { class: 'rp-slide__gissues' }, COPY.issues), el('p', { class: 'rp-slide__gdur' }, COPY.duration)));
|
|
183
|
+
const lanes = el('div', { class: 'rp-slide__glanes' },
|
|
184
|
+
s.months.map((ym, k) => placed(el('div', { class: 'rp-slide__gcol' },
|
|
185
|
+
k === 0 || ym.endsWith('-01') ? el('p', { class: 'rp-slide__gyear' }, ym.slice(0, 4)) : null,
|
|
186
|
+
el('p', { class: 'rp-slide__gmonth' }, monthName(ym))), pos(k * MONTH_W, MONTH_W))));
|
|
187
|
+
const rows = el('div', { class: 'rp-slide__grows' });
|
|
188
|
+
lanes.appendChild(rows);
|
|
189
|
+
for (const ln of s.lines) {
|
|
190
|
+
if (!ln.row) {
|
|
191
|
+
names.appendChild(el('p', { class: 'rp-slide__glabel' }, ln.group));
|
|
192
|
+
rows.appendChild(el('div', { class: 'rp-slide__gline' }));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const r = ln.row;
|
|
196
|
+
names.appendChild(el('div', { class: 'rp-slide__gname', 'data-init': r.id },
|
|
197
|
+
glyph('chevron-right', 'rp-slide__gchev'),
|
|
198
|
+
el('span', { class: 'rp-slide__ldot rp-slide__ldot--' + ln.group.toLowerCase() }),
|
|
199
|
+
el('span', { class: 'rp-slide__gtitle' }, r.title)));
|
|
200
|
+
const line = el('div', { class: 'rp-slide__gline', 'data-init': r.id });
|
|
201
|
+
if (r.segments.length) {
|
|
202
|
+
const x0 = xOf(s.months, r.segments.reduce((a, g) => (g.start < a ? g.start : a), r.segments[0].start));
|
|
203
|
+
const x1 = xOf(s.months, r.segments.reduce((a, g) => (g.due > a ? g.due : a), r.segments[0].due)) + DAY_W;
|
|
204
|
+
const bar = placed(el('div', { class: 'rp-slide__gbar' }), pos(x0 - 6, x1 - x0 + 12));
|
|
205
|
+
for (const g of r.segments) {
|
|
206
|
+
const a = xOf(s.months, g.start); const b = xOf(s.months, g.due) + DAY_W;
|
|
207
|
+
bar.appendChild(placed(el('span', {
|
|
208
|
+
class: 'rp-slide__gseg rp-slide__gseg--' + (STAGE_TONE[g.stage] || 'other') + (r.estimated ? ' rp-slide__gseg--est' : ''),
|
|
209
|
+
'data-stage': g.label, title: g.label + ' · ' + msDate(g.start) + ' → ' + msDate(g.due),
|
|
210
|
+
}), pos(a - x0 + 6, b - a)));
|
|
211
|
+
}
|
|
212
|
+
line.appendChild(bar);
|
|
213
|
+
}
|
|
214
|
+
rows.appendChild(line);
|
|
215
|
+
}
|
|
216
|
+
return el('div', { class: 'rp-slide__gantt' }, names, lanes);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* the initiative list */
|
|
220
|
+
const badge = (status) => el('span', { class: 'rp-slide__badge' + (EARLY.test(status || '') ? ' rp-slide__badge--early' : '') }, status || '');
|
|
221
|
+
const dash = () => span('rp-slide__dash', '—');
|
|
222
|
+
function milestoneCell(m, today) {
|
|
223
|
+
if (!m) return el('div', { class: 'rp-slide__td rp-slide__td--ms' }, dash());
|
|
224
|
+
const late = m.due < today;
|
|
225
|
+
return el('div', { class: 'rp-slide__td rp-slide__td--ms' },
|
|
226
|
+
el('p', { class: 'rp-slide__mstitle' }, m.title),
|
|
227
|
+
el('p', { class: 'rp-slide__msdue' + (late ? ' rp-slide__msdue--late' : '') }, late ? COPY.overdue(msDate(m.due)) : msDate(m.due)));
|
|
228
|
+
}
|
|
229
|
+
function listBody(s) {
|
|
230
|
+
const head = el('div', { class: 'rp-slide__tr rp-slide__tr--head' },
|
|
231
|
+
el('div', { class: 'rp-slide__th' }, COPY.initiative), el('div', { class: 'rp-slide__th rp-slide__th--caps' }, COPY.status),
|
|
232
|
+
el('div', { class: 'rp-slide__th' }, COPY.nextMilestone), el('div', { class: 'rp-slide__th' }, COPY.release));
|
|
233
|
+
const rows = s.rows.map((x) => el('div', { class: 'rp-slide__tr', 'data-init': x.id },
|
|
234
|
+
el('div', { class: 'rp-slide__td rp-slide__td--name' }, x.title),
|
|
235
|
+
el('div', { class: 'rp-slide__td' }, badge(x.status)),
|
|
236
|
+
milestoneCell(x.next_milestone, s.today),
|
|
237
|
+
el('div', { class: 'rp-slide__td rp-slide__td--rel' }, x.release ? msDate(x.release) : dash())));
|
|
238
|
+
return el('div', { class: 'rp-slide__table' }, head, s.rows.length ? rows : el('p', { class: 'rp-slide__empty rp-slide__empty--list' }, COPY.noInitiatives));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* one initiative */
|
|
242
|
+
function itemRow(it) {
|
|
243
|
+
const mark = it.milestone ? el('span', { class: 'rp-slide__mark rp-slide__mark--ms', 'aria-hidden': 'true' })
|
|
244
|
+
: it.done ? glyph('check', 'rp-slide__mark rp-slide__mark--done') : el('span', { class: 'rp-slide__mark rp-slide__mark--open', 'aria-hidden': 'true' });
|
|
245
|
+
const sub = it.milestone
|
|
246
|
+
? [span('rp-slide__subms', COPY.milestone), it.date ? ' · ' + msDate(it.date) : '', it.owner ? ' · ' + it.owner : '']
|
|
247
|
+
: [it.stage || '', it.owner ? ' · ' + it.owner : ''];
|
|
248
|
+
return el('li', { class: 'rp-slide__item' + (it.milestone ? ' rp-slide__item--ms' : ''), 'data-done': it.done ? 'true' : 'false' }, mark,
|
|
249
|
+
el('div', { class: 'rp-slide__itext' }, el('p', { class: 'rp-slide__ititle' }, it.title), el('p', { class: 'rp-slide__isub' }, sub)));
|
|
250
|
+
}
|
|
251
|
+
function cycleColumn(key, title, range, items, empty) {
|
|
252
|
+
const sorted = [...items.filter((x) => x.milestone), ...items.filter((x) => !x.milestone)];
|
|
253
|
+
return el('div', { class: 'rp-slide__ccol', 'data-cycle': key },
|
|
254
|
+
el('div', { class: 'rp-slide__chead' }, el('h3', { class: 'rp-slide__ctitle' }, title), span('rp-slide__crange', range)),
|
|
255
|
+
sorted.length
|
|
256
|
+
? el('ul', { class: 'rp-slide__items' }, sorted.slice(0, COLUMN_CAP).map(itemRow),
|
|
257
|
+
sorted.length > COLUMN_CAP ? el('li', { class: 'rp-slide__more' }, COPY.more(sorted.length - COLUMN_CAP)) : null)
|
|
258
|
+
: el('p', { class: 'rp-slide__empty' }, empty));
|
|
259
|
+
}
|
|
260
|
+
function noteBox(text) {
|
|
261
|
+
if (!text) return null;
|
|
262
|
+
const m = /^(note|info|warning)\s*:\s*/i.exec(text);
|
|
263
|
+
const type = m ? m[1].toLowerCase() : 'note';
|
|
264
|
+
const lines = text.slice(m ? m[0].length : 0).split('\n').map((l) => l.replace(/^[-*]\s+/, '').trim()).filter(Boolean);
|
|
265
|
+
return el('div', { class: 'rp-slide__note rp-slide__note--' + type, 'data-note-type': type },
|
|
266
|
+
el('p', { class: 'rp-slide__notehead' }, COPY.noteType[type]),
|
|
267
|
+
el('ul', { class: 'rp-slide__notelist' }, lines.slice(0, NOTE_LINES).map((l) => el('li', { class: 'rp-slide__noteli' }, l))));
|
|
268
|
+
}
|
|
269
|
+
function initiativeBody(s) {
|
|
270
|
+
const x = s.init; const m = s.meta;
|
|
271
|
+
const top = el('div', { class: 'rp-slide__itop' }, badge(x.status),
|
|
272
|
+
x.pct === null ? el('p', { class: 'rp-slide__pct rp-slide__pct--none' }, COPY.noBreakdown) : el('div', { class: 'rp-slide__prog' },
|
|
273
|
+
el('div', { class: 'rp-slide__track' }, placed(el('div', { class: 'rp-slide__fill' }), { '--rp-pct': x.pct + '%' })),
|
|
274
|
+
el('p', { class: 'rp-slide__pct' }, x.pct + '%')));
|
|
275
|
+
const ms = x.next_milestone;
|
|
276
|
+
const late = ms && ms.due < m.today;
|
|
277
|
+
const msLine = el('div', { class: 'rp-slide__msline' },
|
|
278
|
+
span('rp-slide__mslabel', COPY.nextMilestone),
|
|
279
|
+
ms ? span('rp-slide__msname', ms.title) : dash(),
|
|
280
|
+
ms ? span('rp-slide__mswhen' + (late ? ' rp-slide__mswhen--late' : ''), late ? COPY.overdue(msDate(ms.due)) : msDate(ms.due)) : null,
|
|
281
|
+
span('rp-slide__mslabel rp-slide__mslabel--rel', COPY.release),
|
|
282
|
+
x.release ? span('rp-slide__msname', msDate(x.release)) : dash());
|
|
283
|
+
return [top, msLine,
|
|
284
|
+
el('div', { class: 'rp-slide__cycles' },
|
|
285
|
+
cycleColumn('last', COPY.lastCycle, rangeText(m.last.from, dayBefore(m.last.to)), x.last, COPY.nothingShipped),
|
|
286
|
+
cycleColumn('next', COPY.nextCycle, rangeText(m.next.from, dayBefore(m.next.to)), x.next, COPY.nothingPlanned)),
|
|
287
|
+
noteBox(s.note)];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* release notes */
|
|
291
|
+
function changelogBody(s) {
|
|
292
|
+
if (!s.entries.length) return el('p', { class: 'rp-slide__empty' }, COPY.noReleaseNotes);
|
|
293
|
+
const out = [];
|
|
294
|
+
let month = null; let list = null;
|
|
295
|
+
for (const e of s.entries) {
|
|
296
|
+
const ym = e.date.slice(0, 7);
|
|
297
|
+
if (ym !== month) {
|
|
298
|
+
month = ym;
|
|
299
|
+
const n = s.entries.filter((y) => y.date.slice(0, 7) === ym).length;
|
|
300
|
+
out.push(el('h3', { class: 'rp-slide__month' }, monthTitle(e.date), span('rp-slide__mcount', COPY.entries(n))));
|
|
301
|
+
list = el('ol', { class: 'rp-slide__timeline' });
|
|
302
|
+
out.push(list);
|
|
303
|
+
}
|
|
304
|
+
list.appendChild(el('li', { class: 'rp-slide__entry rp-slide__entry--' + e.type, 'data-entry-type': e.type },
|
|
305
|
+
el('span', { class: 'rp-slide__edot', 'aria-hidden': 'true' }),
|
|
306
|
+
el('div', { class: 'rp-slide__erow' },
|
|
307
|
+
span('rp-slide__pill', e.label || e.type),
|
|
308
|
+
e.area ? span('rp-slide__area', e.area) : null,
|
|
309
|
+
el('time', { class: 'rp-slide__edate', datetime: e.date }, fmt(e.date, { month: 'short', day: 'numeric', year: 'numeric' }, 'en-US'),
|
|
310
|
+
span('rp-slide__eday', ' · ' + fmt(e.date, { weekday: 'long' }, 'en-US')))),
|
|
311
|
+
el('h4', { class: 'rp-slide__etitle' }, e.title),
|
|
312
|
+
e.summary ? el('p', { class: 'rp-slide__esum' }, e.summary) : null,
|
|
313
|
+
e.changes.map((c) => el('p', { class: 'rp-slide__change' }, span('rp-slide__kind rp-slide__kind--' + c.kind, c.kind), span('rp-slide__ctext', c.text)))));
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const BODY = { roadmap: roadmapBody, list: listBody, initiative: initiativeBody, changelog: changelogBody };
|
|
319
|
+
|
|
320
|
+
/** One slide, 1280 × 720. */
|
|
321
|
+
export function renderSlide(s) {
|
|
322
|
+
ensureSprite();
|
|
323
|
+
return frame(s, BODY[s.kind](s));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ── in the Reports viewer: the slides, one under the other, scaled to the column ────────────────────────────────── */
|
|
327
|
+
|
|
328
|
+
const fitTo = (host, width) => host.style.setProperty('--rp-scale', String(Math.max(0.05, Math.round((width / W) * 10000) / 10000)));
|
|
329
|
+
|
|
330
|
+
/** Every slide of the report as a card the width of the column. A file with no slides block says how to get them. */
|
|
331
|
+
export function slideCards(md) {
|
|
332
|
+
ensureComponentsCss();
|
|
333
|
+
const slides = slidesOf(md);
|
|
334
|
+
if (!slides.length) return el('p', { class: 'rp-section__empty', 'data-rp-noslides': '' }, COPY.slidesMissing);
|
|
335
|
+
const host = el('div', { class: 'rp-slides', role: 'list', 'aria-label': COPY.slidesTitle },
|
|
336
|
+
slides.map((s) => el('div', { class: 'rp-slides__card', role: 'listitem', 'data-slide-n': String(s.n) }, renderSlide(s))));
|
|
337
|
+
if (typeof ResizeObserver === 'function') {
|
|
338
|
+
const ro = new ResizeObserver((es) => { for (const e of es) if (e.contentRect.width) fitTo(host, e.contentRect.width); });
|
|
339
|
+
ro.observe(host);
|
|
340
|
+
}
|
|
341
|
+
return host;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ── on paper: one slide per 1280 × 720 page ──────────────────────────────────────────────────────────────────────── */
|
|
345
|
+
|
|
346
|
+
/** Draw every slide into `body > .rp-print.rp-print--slides` and print. print.css gives each slide its own page. */
|
|
347
|
+
export function printSlides(md) {
|
|
348
|
+
ensureComponentsCss();
|
|
349
|
+
ensurePrintCss();
|
|
350
|
+
clearPrint();
|
|
351
|
+
const host = el('div', { class: 'rp-print rp-print--slides', 'aria-hidden': 'true' }, slidesOf(md).map((s) => renderSlide(s)));
|
|
352
|
+
document.body.appendChild(host);
|
|
353
|
+
const drop = () => { host.remove(); window.removeEventListener('afterprint', drop); };
|
|
354
|
+
window.addEventListener('afterprint', drop);
|
|
355
|
+
try { window.print(); } catch { drop(); }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* ── Present ──────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* openSlides({ md, label, start?, printable?, returnFocus? }): the deck, full screen on black, one slide fitted. ← → Space PageUp PageDown Home
|
|
362
|
+
* End move; a horizontal swipe of 40px moves; Escape or Close ends it. Returns { dialog, go(i), close, index, count }.
|
|
363
|
+
*/
|
|
364
|
+
export function openSlides({ md, label, start = 0, printable = false, returnFocus = null } = {}) {
|
|
365
|
+
ensureComponentsCss();
|
|
366
|
+
ensureSprite();
|
|
367
|
+
const slides = slidesOf(md);
|
|
368
|
+
let i = Math.min(Math.max(0, start), Math.max(0, slides.length - 1));
|
|
369
|
+
let closed = false;
|
|
370
|
+
const stage = el('div', { class: 'rp-deck__stage' });
|
|
371
|
+
const frameEl = el('div', { class: 'rp-deck__frame', 'aria-live': 'polite' });
|
|
372
|
+
stage.appendChild(frameEl);
|
|
373
|
+
const count = el('p', { class: 'rp-deck__count' });
|
|
374
|
+
const prev = iconButton({ icon: 'chevron-left', ariaLabel: COPY.slidesPrev, attrs: { 'data-rp-act': 'slide-prev' }, onClick: () => go(i - 1) });
|
|
375
|
+
const next = iconButton({ icon: 'chevron-right', ariaLabel: COPY.slidesNext, attrs: { 'data-rp-act': 'slide-next' }, onClick: () => go(i + 1) });
|
|
376
|
+
// Print / PDF only for an issued report: a report leaves the workspace after Issue, never as a draft (D71)
|
|
377
|
+
const print = printable ? button({ label: COPY.print, variant: 'ghost', size: 'sm', attrs: { 'data-rp-act': 'slides-print' }, onClick: () => printSlides(md) }) : null;
|
|
378
|
+
const shut = iconButton({ icon: 'x', ariaLabel: COPY.slidesClose, attrs: { 'data-rp-act': 'slides-close' }, onClick: () => close() });
|
|
379
|
+
const bar = el('div', { class: 'rp-deck__bar', role: 'toolbar', 'aria-label': label || COPY.slidesTitle },
|
|
380
|
+
el('p', { class: 'rp-deck__label' }, label || COPY.slidesTitle), prev, count, next, print, shut);
|
|
381
|
+
const dialog = el('dialog', { class: 'rp-deck', 'aria-modal': 'true', 'aria-label': label || COPY.slidesTitle, 'aria-roledescription': 'carousel', tabindex: '-1' }, stage, bar);
|
|
382
|
+
|
|
383
|
+
const fit = () => {
|
|
384
|
+
const r = stage.getBoundingClientRect();
|
|
385
|
+
const k = Math.max(0.05, Math.min(r.width / W, r.height / H));
|
|
386
|
+
dialog.style.setProperty('--rp-scale', String(Math.round(k * 10000) / 10000));
|
|
387
|
+
};
|
|
388
|
+
function go(to) {
|
|
389
|
+
if (!slides.length) {
|
|
390
|
+
frameEl.replaceChildren(el('p', { class: 'rp-deck__empty' }, COPY.slidesMissing));
|
|
391
|
+
count.textContent = ''; prev.disabled = true; next.disabled = true;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
i = Math.min(Math.max(0, to), slides.length - 1);
|
|
395
|
+
frameEl.replaceChildren(renderSlide(slides[i]));
|
|
396
|
+
count.textContent = COPY.slidesCount(i + 1, slides.length);
|
|
397
|
+
const was = document.activeElement;
|
|
398
|
+
prev.disabled = i === 0;
|
|
399
|
+
next.disabled = i === slides.length - 1;
|
|
400
|
+
// a button that was just disabled loses the focus to the page: keep it in the deck, so the keys still move
|
|
401
|
+
if ((was === prev && prev.disabled) || (was === next && next.disabled)) dialog.focus();
|
|
402
|
+
}
|
|
403
|
+
function close() {
|
|
404
|
+
if (closed) return;
|
|
405
|
+
closed = true;
|
|
406
|
+
window.removeEventListener('resize', fit);
|
|
407
|
+
try { if (dialog.open) dialog.close(); } catch { /* already closed */ }
|
|
408
|
+
dialog.remove();
|
|
409
|
+
if (returnFocus && returnFocus.isConnected) returnFocus.focus();
|
|
410
|
+
}
|
|
411
|
+
dialog.addEventListener('cancel', (e) => { e.preventDefault(); close(); });
|
|
412
|
+
dialog.addEventListener('keydown', (e) => {
|
|
413
|
+
const k = e.key;
|
|
414
|
+
if (k === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); }
|
|
415
|
+
else if (k === 'ArrowRight' || k === 'PageDown' || (k === ' ' && !(e.target && e.target.closest && e.target.closest('button')))) { e.preventDefault(); go(i + 1); }
|
|
416
|
+
else if (k === 'ArrowLeft' || k === 'PageUp') { e.preventDefault(); go(i - 1); }
|
|
417
|
+
else if (k === 'Home') { e.preventDefault(); go(0); }
|
|
418
|
+
else if (k === 'End') { e.preventDefault(); go(slides.length - 1); }
|
|
419
|
+
});
|
|
420
|
+
// a swipe: touch, not pointer, so a mouse drag selects text as usual; the buttons are the way for a mouse and a keyboard
|
|
421
|
+
let x0 = null; let y0 = null;
|
|
422
|
+
stage.addEventListener('touchstart', (e) => { const t = e.changedTouches[0]; x0 = t.clientX; y0 = t.clientY; }, { passive: true });
|
|
423
|
+
stage.addEventListener('touchend', (e) => {
|
|
424
|
+
if (x0 === null) return;
|
|
425
|
+
const t = e.changedTouches[0];
|
|
426
|
+
const dx = t.clientX - x0; const dy = t.clientY - y0;
|
|
427
|
+
x0 = null;
|
|
428
|
+
if (Math.abs(dx) > 40 && Math.abs(dx) > 1.5 * Math.abs(dy)) go(i + (dx < 0 ? 1 : -1));
|
|
429
|
+
}, { passive: true });
|
|
430
|
+
|
|
431
|
+
layer().appendChild(dialog);
|
|
432
|
+
dialog.showModal();
|
|
433
|
+
window.addEventListener('resize', fit);
|
|
434
|
+
fit();
|
|
435
|
+
go(i);
|
|
436
|
+
dialog.focus();
|
|
437
|
+
return { dialog, go, close, get index() { return i; }, get count() { return slides.length; } };
|
|
438
|
+
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* noteSlots(md) [{ id, title, text }] the notes the PM can edit, in the order they appear
|
|
7
7
|
* markdownToCopy(md) the markdown without its frontmatter and note markers, for Copy as markdown
|
|
8
8
|
* copyMarkdown(md) put that on the clipboard, then toast
|
|
9
|
-
* printReport(md)
|
|
9
|
+
* printReport(md) print the report's slides (report-slides.js), or for a file with none draw the text into
|
|
10
|
+
* `body > .rp-print`; then window.print(), and print.css shows only it
|
|
10
11
|
* openIssueSheet(args) the two-tap Issue: the sheet reads the draft, the PM presses Issue, the file is frozen
|
|
11
12
|
* openNotesSheet(args) one sheet for every note of the draft
|
|
12
13
|
* draftNow(args) POST /api/report/draft, then toast
|
|
@@ -24,6 +25,7 @@ import { ensureSprite } from './icons.js';
|
|
|
24
25
|
import { pmGet, pmPost } from './http.js';
|
|
25
26
|
import { registerTodayCard } from './registry.js';
|
|
26
27
|
import { COPY } from './reports-copy.js';
|
|
28
|
+
import { slidesBlock, printSlides } from './report-slides.js';
|
|
27
29
|
|
|
28
30
|
/* ── the markdown subset the drafter writes ───────────────────────────────────────────────────────────────────────── */
|
|
29
31
|
|
|
@@ -31,7 +33,9 @@ const NOTE_RE = /<!-- pmos:note ([a-z0-9][a-z0-9._-]*) -->\n?([\s\S]*?)\n?<!-- \
|
|
|
31
33
|
const MARK = '\u0001NOTE:';
|
|
32
34
|
const FRONT = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
// the slides' data block (Run 6) is for the slides only: no reader of the text sees it
|
|
37
|
+
const SLIDES_DATA = /\n?<!-- pmos:slides v1 \{[^\n]*\} -->\s*$/;
|
|
38
|
+
const stripFront = (md) => String(md || '').replace(FRONT, '').replace(/\r\n/g, '\n').replace(SLIDES_DATA, '\n');
|
|
35
39
|
const cells = (ln) => ln.trim().replace(/^\|/, '').replace(/\|$/, '').split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, '|'));
|
|
36
40
|
const isRule = (ln) => /^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(ln.trim());
|
|
37
41
|
const bare = (h) => String(h || '').replace(/\s*\([a-z0-9._-]+\)\s*$/, '');
|
|
@@ -40,6 +44,7 @@ const bare = (h) => String(h || '').replace(/\s*\([a-z0-9._-]+\)\s*$/, '');
|
|
|
40
44
|
function noteTitle(id, heading) {
|
|
41
45
|
if (id === 'report.headline') return COPY.noteHeadline;
|
|
42
46
|
if (id === 'report.asks') return COPY.noteAsks;
|
|
47
|
+
if (id === 'report.changelog') return COPY.noteChangelog;
|
|
43
48
|
return COPY.noteOf(heading || id);
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -151,6 +156,7 @@ export function clearPrint() { for (const n of document.querySelectorAll('body >
|
|
|
151
156
|
* of the body and shows only it. It is removed when the print dialog closes, and again before the next print.
|
|
152
157
|
*/
|
|
153
158
|
export function printReport(md) {
|
|
159
|
+
if (slidesBlock(md)) { printSlides(md); return; } // a report with slides prints as slides, one per page (Run 6)
|
|
154
160
|
ensureComponentsCss();
|
|
155
161
|
ensurePrintCss();
|
|
156
162
|
clearPrint();
|
|
@@ -61,6 +61,7 @@ export const COPY = {
|
|
|
61
61
|
notesSub: 'A note is your text between the markers. Saving a note draws the report again from the files as they are now.',
|
|
62
62
|
noteHeadline: 'Headline note',
|
|
63
63
|
noteAsks: 'Note for needs your call',
|
|
64
|
+
noteChangelog: 'Note for the changelog slide',
|
|
64
65
|
noteOf: (title) => 'Note on ' + title,
|
|
65
66
|
noteSave: 'Save notes',
|
|
66
67
|
noteSaved: 'Notes saved.',
|
|
@@ -103,6 +104,8 @@ export const COPY = {
|
|
|
103
104
|
firesOnlyRunning: 'A schedule fires only while the workspace server runs.',
|
|
104
105
|
|
|
105
106
|
flagTitle: 'The issues ledger has a line that is not read',
|
|
107
|
+
|
|
108
|
+
present: 'Present',
|
|
106
109
|
};
|
|
107
110
|
|
|
108
111
|
export const GLOSSARY = {
|