create-pmos 0.53.0 → 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/README.md +2 -0
- package/workspace/REMOTE.md +107 -65
- package/workspace/bin/pmos.js +4 -2
- 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/command-palette.md +1 -1
- package/workspace/guide/index.md +3 -1
- package/workspace/guide/initiatives-roadmap.md +1 -0
- package/workspace/guide/initiatives-screen.md +1 -0
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +7 -2
- package/workspace/guide/proactive-engine.md +21 -5
- 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/board-fold.js +2 -2
- package/workspace/lib/breakdown-export.js +1 -6
- package/workspace/lib/brief.js +20 -2
- package/workspace/lib/goals.js +165 -0
- package/workspace/lib/initiative-rows.js +7 -3
- package/workspace/lib/launcher-cli.js +207 -0
- package/workspace/lib/plan-tree.js +15 -2
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +2 -2
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +184 -12
- package/workspace/lib/report-draft.js +34 -6
- 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/routes-timeline.js +1 -1
- package/workspace/lib/sync.js +61 -6
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +11 -0
- package/workspace/public/pm/app.js +55 -9
- package/workspace/public/pm/components.css +865 -1
- package/workspace/public/pm/dest-reports.js +8 -2
- package/workspace/public/pm/dest-today.js +8 -2
- package/workspace/public/pm/dock.js +73 -3
- package/workspace/public/pm/frame-copy.js +5 -0
- package/workspace/public/pm/initiative-list.js +18 -1
- package/workspace/public/pm/initiatives-copy.js +4 -0
- package/workspace/public/pm/panel-tabs.js +14 -12
- 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/today-copy.js +29 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/public/pm/tokens.css +13 -0
- package/workspace/public/pm/topbar.js +5 -2
- package/workspace/replaced-tests.json +36 -1
- package/workspace/server.js +165 -15
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +1 -0
- package/workspace/test-files.json +4 -0
- package/workspace/test-lists/run6.json +9 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -0
- package/workspace/test-lists/workspace-public-host.json +5 -0
|
@@ -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 = {
|
|
@@ -27,6 +27,7 @@ import { loadSortable, baseOptions, dragSet } from './bd-drag.js';
|
|
|
27
27
|
import { COPY, shortDate } from './initiatives-copy.js';
|
|
28
28
|
import { getRoadmap, sigKey, place, placed, leaveNow } from './initiatives-api.js';
|
|
29
29
|
import { healthSignal, say, checkBySheet, onContextMenu } from './initiatives-ui.js';
|
|
30
|
+
import { openDoc } from './panel-initiatives.js';
|
|
30
31
|
|
|
31
32
|
const HORIZONS = ['now', 'next', 'later'];
|
|
32
33
|
|
|
@@ -37,6 +38,7 @@ export function mountRoadmap(host, env) {
|
|
|
37
38
|
let data = null;
|
|
38
39
|
let error = null;
|
|
39
40
|
let showDormant = false;
|
|
41
|
+
let showUnrecorded = false;
|
|
40
42
|
let dragging = false;
|
|
41
43
|
let deferred = null;
|
|
42
44
|
let focusId = null;
|
|
@@ -150,6 +152,18 @@ export function mountRoadmap(host, env) {
|
|
|
150
152
|
});
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
// A unit with documents and no record (routes-initiatives.js unrecordedOf). It opens its PRD, or its first document; it
|
|
156
|
+
// has no menu and no grip, because only a record can be placed.
|
|
157
|
+
function unrecordedCard(u) {
|
|
158
|
+
const doc = u.artifacts.find((a) => a.role === 'PRD') || u.artifacts[0];
|
|
159
|
+
const roles = [...new Set(u.artifacts.map((a) => a.role).filter(Boolean))];
|
|
160
|
+
return card({
|
|
161
|
+
title: u.title, meta: [COPY.noRecord, ...(roles.length ? [roles.join(' · ')] : [])],
|
|
162
|
+
onOpen: () => openDoc(null, doc.path, doc.role || 'DOC'), openLabel: COPY.aria.open(u.title),
|
|
163
|
+
muted: true, attrs: { class: 'in-card', 'data-in-unrecorded-card': u.id }, openAttrs: { 'data-in-unrecorded-open': u.id },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
function column(h) {
|
|
154
168
|
const rows = data.horizons[h] || [];
|
|
155
169
|
const list = el('div', { class: 'in-list', 'data-in-list': h, role: 'list', 'aria-label': COPY.aria.column(COPY.horizons[h]) }, rows.map((r) => cardOf(r, { grip: true })));
|
|
@@ -166,6 +180,15 @@ export function mountRoadmap(host, env) {
|
|
|
166
180
|
}));
|
|
167
181
|
if (showDormant) kids.push(el('div', { class: 'in-list in-list--dormant', 'data-in-dormant': '' }, data.dormant.map((r) => cardOf(r, { grip: false }))));
|
|
168
182
|
}
|
|
183
|
+
if (h === 'later' && data.unrecorded && data.unrecorded.length) {
|
|
184
|
+
const n = data.unrecorded.length;
|
|
185
|
+
kids.push(button({
|
|
186
|
+
label: showUnrecorded ? COPY.hideUnrecorded(n) : COPY.unrecorded(n), variant: 'ghost', size: 'sm',
|
|
187
|
+
icon: showUnrecorded ? 'chevron-down' : 'chevron-right', attrs: { 'data-in-unrecorded-toggle': '', 'aria-expanded': showUnrecorded ? 'true' : 'false' },
|
|
188
|
+
onClick: () => { showUnrecorded = !showUnrecorded; render(); },
|
|
189
|
+
}));
|
|
190
|
+
if (showUnrecorded) kids.push(el('div', { class: 'in-list in-list--dormant', 'data-in-unrecorded': '' }, data.unrecorded.map(unrecordedCard)));
|
|
191
|
+
}
|
|
169
192
|
return el('section', { class: 'in-col', 'data-in-col': h, 'aria-label': COPY.horizons[h] }, kids);
|
|
170
193
|
}
|
|
171
194
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* pm/slides-copy.js — every word on the biweekly slides and their deck (pm-os-absorb Run 6; PRD R22.7).
|
|
2
|
+
*
|
|
3
|
+
* Pure data and pure functions. No DOM and no import, so glossary.test.js loads this file in Node and checks each string
|
|
4
|
+
* against the glossary rules. report-slides.js draws the slides from these strings and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* The slides copy the PM-OS biweekly deck word for word: the PM asked for the slide format "exactly" as PM-OS has it
|
|
7
|
+
* (2026-10-06). So GLOSSARY.exempt lifts two rules for this file only: the column and slide names "Release" and "Release
|
|
8
|
+
* notes", and the empty column "Nothing shipped.", are the PM-OS deck's own words. Everywhere else in the workspace the
|
|
9
|
+
* rules hold. A slide is still ISSUED with its report, never sent (D71): the issued-not-sent rule applies here too.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const COPY = {
|
|
13
|
+
slidesTitle: 'Slides',
|
|
14
|
+
slidesMissing: 'This report has no slides. Redraft it to get them.',
|
|
15
|
+
slidesPrev: 'Previous slide',
|
|
16
|
+
slidesNext: 'Next slide',
|
|
17
|
+
slidesClose: 'Close the slides',
|
|
18
|
+
slidesCount: (n, of) => n + ' / ' + of,
|
|
19
|
+
print: 'Print / PDF',
|
|
20
|
+
more: (n) => '+' + n + ' more',
|
|
21
|
+
roadmap: 'Roadmap',
|
|
22
|
+
list: 'Initiative progress',
|
|
23
|
+
changelog: 'Release notes',
|
|
24
|
+
issues: 'Issues',
|
|
25
|
+
duration: 'Duration',
|
|
26
|
+
initiative: 'Initiative',
|
|
27
|
+
status: 'Status',
|
|
28
|
+
nextMilestone: 'Next milestone',
|
|
29
|
+
release: 'Release',
|
|
30
|
+
overdue: (d) => 'Overdue · ' + d,
|
|
31
|
+
noBreakdown: 'No breakdown',
|
|
32
|
+
noInitiatives: 'No initiatives in this window.',
|
|
33
|
+
lastCycle: 'Last cycle',
|
|
34
|
+
nextCycle: 'Next cycle',
|
|
35
|
+
milestone: 'Milestone',
|
|
36
|
+
nothingShipped: 'Nothing shipped.',
|
|
37
|
+
nothingPlanned: 'Nothing planned.',
|
|
38
|
+
noteType: { note: 'Note', info: 'Info', warning: 'Warning' },
|
|
39
|
+
noReleaseNotes: 'No release notes logged.',
|
|
40
|
+
entries: (n) => n + (n === 1 ? ' entry' : ' entries'),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const GLOSSARY = {
|
|
44
|
+
exempt: {
|
|
45
|
+
release: 'The PM-OS deck names its column "Release" and its slide "Release notes"; the PM asked for that format exactly (2026-10-06).',
|
|
46
|
+
claims: 'The PM-OS deck says "Nothing shipped." in an empty Last cycle; the PM asked for that format exactly (2026-10-06).',
|
|
47
|
+
},
|
|
48
|
+
extra: [
|
|
49
|
+
{ id: 'issued-not-sent', re: /\b(?:sent|sends?|sending|e-?mail(?:s|ed|ing)?|shar(?:e|es|ed|ing)|post(?:s|ed|ing)? to)\b/i, why: 'A report is issued, never sent (D71). The slides have no control that moves a report out of the workspace.' },
|
|
50
|
+
],
|
|
51
|
+
guide: 'report-slides.md',
|
|
52
|
+
};
|
|
@@ -56,6 +56,35 @@ export const COPY = {
|
|
|
56
56
|
commandShown: (cmd) => `Run this in a terminal: ${cmd}`,
|
|
57
57
|
noInitiative: 'This card names no initiative.',
|
|
58
58
|
sessionGone: 'That session has ended.',
|
|
59
|
+
/** The goals for today (initiative today-goals, PRD G6). The word for what the session finds is "step", never a plan. */
|
|
60
|
+
goals: {
|
|
61
|
+
title: 'Goals for today',
|
|
62
|
+
toggle: (open, total) => (total ? `Goals for today · ${open} open of ${total}` : 'Set your goals for today'),
|
|
63
|
+
intro: 'List what you want done today. Prepare my day then finds the meetings, mentions and steps that move each goal.',
|
|
64
|
+
inputLabel: 'Add a goal',
|
|
65
|
+
inputHint: 'What do you want done today? Press Enter to add it.',
|
|
66
|
+
full: (max) => `A day has at most ${max} goals. Drop or remove one to add another.`,
|
|
67
|
+
prepareDay: 'Prepare my day',
|
|
68
|
+
prepareDayHint: 'Starts a session that reads your calendar and your Slack, Jira and Confluence mentions, and ties what it finds to your goals.',
|
|
69
|
+
started: 'The session started. The steps show here when it is done.',
|
|
70
|
+
coverage: { planned: 'has steps', 'needs-plan': 'no step yet', done: 'done', dropped: 'dropped' },
|
|
71
|
+
done: 'Done', drop: 'Drop', reopen: 'Reopen', remove: 'Remove',
|
|
72
|
+
you: 'You', agent: 'Agent', mention: 'mention',
|
|
73
|
+
noStep: 'No step found yet. Press Prepare my day, or open the initiative.',
|
|
74
|
+
prepare: 'Prepare',
|
|
75
|
+
queue: 'Queue it',
|
|
76
|
+
prepareShows: (key, branch) => `${key} on ${branch || 'a local branch'}. Queue it to approve this exact prompt.`,
|
|
77
|
+
queued: (key) => `Queued ${key}. It starts when background prepare is on and the budget and quiet hours allow.`,
|
|
78
|
+
also: 'Also on your plate',
|
|
79
|
+
worthAsking: 'Worth asking',
|
|
80
|
+
free: (min, until) => `${Math.floor(min / 60)} h ${min % 60} min free until ${until}.`,
|
|
81
|
+
comingUp: (title, at) => `Coming up: ${title} at ${at}.`,
|
|
82
|
+
notRead: (list) => `Not read: ${list}.`,
|
|
83
|
+
stale: (date) => `What was read is from ${date}. It is out of date, so only what Today found is used.`,
|
|
84
|
+
noBrief: 'Nothing was read yet today. Press Prepare my day to read your calendar and mentions.',
|
|
85
|
+
refused: (why) => `Nothing changed. ${why}`,
|
|
86
|
+
unreached: 'The workspace did not answer.',
|
|
87
|
+
},
|
|
59
88
|
/** The Acceptance Gate card (D12): the PM's own act on a run. */
|
|
60
89
|
accept: {
|
|
61
90
|
review: 'Review',
|