create-pmos 0.530.0 → 0.532.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +27 -41
- 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/changelog.json +26 -0
- package/workspace/guide/reports.md +5 -1
- package/workspace/lib/report-cli.js +26 -6
- package/workspace/lib/report-engine.js +77 -20
- package/workspace/lib/report-export.js +155 -3
- package/workspace/lib/report-library.js +134 -0
- package/workspace/lib/report-pyramid.js +2 -0
- package/workspace/lib/report-slides.js +580 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/cozy-vw.css +1 -0
- package/workspace/public/pm/premium-report-read.js +24 -3
- package/workspace/skills/report-write.skill +4 -1
- package/workspace/test-lists/report-slides.json +9 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* report-slides.js — the report slide library: ONE frame (B2) and 23 layouts (batch 27 item 14, agreed by Wawan 2026-11-04 13:44; canvas
|
|
3
|
+
* "Report library" https://claude.ai/artifact/D1ssyUsrbTtxysSuibSjmV, boards L01–L23).
|
|
4
|
+
*
|
|
5
|
+
* LAYOUTS → the 23 layout names, in the canvas order
|
|
6
|
+
* layoutError(name, data) → null | a sentence that says what is wrong with the data (pure)
|
|
7
|
+
* textsOfLayout(name, data) → [text]: every string the writer wrote in the data (the engine checks them for bare figures) (pure)
|
|
8
|
+
* mapLayout(name, data, fn) → a copy of the data with fn applied to each of those strings (the engine resolves its reading tokens) (pure)
|
|
9
|
+
* linesOf(name, data) → [line]: the layout as plain lines, for the document and the PowerPoint file (pure)
|
|
10
|
+
* slideHtml(slide, ctx) → one <section class="pmos-slide rs"> (pure)
|
|
11
|
+
* deckHtml(deck) → one HTML page: ONE <style> block and one section per slide, 1280 x 720 each (pure)
|
|
12
|
+
* CSS → that style block
|
|
13
|
+
*
|
|
14
|
+
* THE FRAME (B2). One white panel 10 px inside the slide edge on a light gradient ground. In the panel: the eyebrow, the heading, the
|
|
15
|
+
* subheading and a hairline; the body; and the footer (source · deck name · n / N). Plus Jakarta Sans, the workspace face. Most bodies take
|
|
16
|
+
* a "✦ Insight" side column; a slide with no insight gives the whole width to its body (zero is silence). No card layouts: rows, hairlines and
|
|
17
|
+
* columns, never boxes on boxes.
|
|
18
|
+
*
|
|
19
|
+
* A SLIDE is { layout, eyebrow, title, sub, data, insight, source, n }. insight = { head, text, points: [{ tone, text }], note } or null.
|
|
20
|
+
* A deck is { name, slides: [slide…] }. Tones: ok · watch · late · bad · info · none.
|
|
21
|
+
*
|
|
22
|
+
* MOTION is CSS only and subtle: bars grow, rows rise, the header fades. It is switched off under prefers-reduced-motion and in print.
|
|
23
|
+
*
|
|
24
|
+
* INTERACTION is CSS only, inside the frame's empty sandbox (no script runs there; option A of the brief, proven in a real browser on
|
|
25
|
+
* 4 Nov). Two layouts have it:
|
|
26
|
+
* timeline Quarter | 6 months | Year (radio inputs) and "At risk or late" (a checkbox): every span is drawn, CSS shows the picked one.
|
|
27
|
+
* actions each action is a checkbox: ticking strikes it through, and a CSS counter says how many are left.
|
|
28
|
+
* The PDF and the PowerPoint file show the first state (Quarter, all rows, nothing ticked). The controls are hidden in print.
|
|
29
|
+
* The sanitizer (lib/report-design.js) writes `>` in CSS as an escape, so no rule here uses the child combinator.
|
|
30
|
+
*
|
|
31
|
+
* NOTHING HERE READS A FILE OR STARTS A JOB. lib/report-engine.js deckOf builds the slides; lib/report-pyramid.js deckHtml hands a B2 deck here;
|
|
32
|
+
* lib/report-export.js draws the same slides in the PowerPoint file.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const LAYOUTS = Object.freeze(['overview', 'tiles', 'ledger', 'timeline', 'compare', 'lists', 'risks', 'ranked', 'decisions', 'quotes', 'actions',
|
|
36
|
+
'agenda', 'heatmap', 'network', 'schedule', 'donut', 'bubbles', 'people', 'statement', 'sources', 'minutes', 'focus', 'next', 'blocks']);
|
|
37
|
+
const TONES = Object.freeze(['ok', 'watch', 'late', 'bad', 'info', 'none']);
|
|
38
|
+
const MONS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
39
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
40
|
+
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
41
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
42
|
+
const DAY = 86400000;
|
|
43
|
+
|
|
44
|
+
const str = (v) => (typeof v === 'string' ? v : v == null ? '' : String(v));
|
|
45
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
46
|
+
const isStr = (v) => typeof v === 'string';
|
|
47
|
+
const esc = (s) => str(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
48
|
+
const pct = (v) => Math.round(Math.max(0, Math.min(1, Number(v) || 0)) * 1000) / 10;
|
|
49
|
+
const ms = (ymd) => Date.parse(ymd + 'T00:00:00Z');
|
|
50
|
+
const short = (ymd) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(str(ymd)); return m ? `${+m[3]} ${MONS[+m[2] - 1]}` : str(ymd); };
|
|
51
|
+
/** "23 Apr 2026": a date in full, short month (the document's date check passes it) */
|
|
52
|
+
const dmy = (ymd) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(str(ymd)); return m ? `${+m[3]} ${MONS[+m[2] - 1]} ${m[1]}` : str(ymd); };
|
|
53
|
+
/** on a slide: "23 Apr" in the year of today, "11 Aug 2027" in another year */
|
|
54
|
+
const onSlide = (ymd, today) => (str(ymd).slice(0, 4) === str(today).slice(0, 4) ? short(ymd) : dmy(ymd));
|
|
55
|
+
const tone = (t) => (TONES.includes(t) ? t : 'none');
|
|
56
|
+
/** the class of each tone, per part */
|
|
57
|
+
const DOT = { ok: 'd-ok', watch: 'd-risk', late: 'd-late', bad: 'd-red', info: 'd-info', none: 'd-q' };
|
|
58
|
+
const PILL = { ok: 'ok', watch: 'w', late: 'l', bad: 'r', info: 'y', none: '' };
|
|
59
|
+
const BAR = { ok: 'o', watch: 'rk', late: 'rk', bad: 'rd', info: 'y', none: '' };
|
|
60
|
+
const CELL = { ok: 'ok', watch: 'w', late: 'w', bad: 'r', info: 'y', none: 'q' };
|
|
61
|
+
const TB = { ok: 'g', watch: 'r', late: 'r', bad: 'l', info: '', none: 'k' };
|
|
62
|
+
const FILL = { ok: '#3E6E49', watch: '#B8860B', late: '#A4461A', bad: '#A8322A', info: '#2B55C9', none: '#646A74' };
|
|
63
|
+
const RING = { ok: '#5E9C6A', watch: '#E3B34A', late: '#D9773F', bad: '#D0473E', info: '#4894EA', none: '#B9BDC4' };
|
|
64
|
+
|
|
65
|
+
// ── the data each layout takes ────────────────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
/* Each spec: the lists and their limits, and which keys are text (checked and resolved) — every other key is a number, a date or a tone. */
|
|
67
|
+
const SPEC = {
|
|
68
|
+
overview: { lead: 't', cols: { max: 4, min: 1, item: { head: 't', text: 't' } } },
|
|
69
|
+
tiles: { tiles: { max: 4, min: 1, item: { label: 't', value: 't', note: 't', share: 'n', target: 'n', tone: 'tone' } } },
|
|
70
|
+
ledger: { label: 't', rows: { max: 6, min: 1, item: { name: 't', value: 't', share: 'n', target: 'n', target_text: 't', verdict: 't', tone: 'tone' } } },
|
|
71
|
+
timeline: { today: 'date', rows: { max: 8, min: 1, item: { name: 't', start: 'date', end: 'date', tag: 't', tone: 'tone' } } },
|
|
72
|
+
compare: { left: { obj: { label: 't', items: { max: 3, min: 1, item: { value: 't', name: 't', note: 't', share: 'n', target: 'n', tone: 'tone' } } } },
|
|
73
|
+
right: { obj: { label: 't', items: { max: 3, min: 1, item: { value: 't', name: 't', note: 't', share: 'n', target: 'n', tone: 'tone' } } } } },
|
|
74
|
+
lists: { left: { obj: { label: 't', tone: 'tone', items: { max: 6, min: 1, item: { text: 't', detail: 't' } } } },
|
|
75
|
+
right: { obj: { label: 't', tone: 'tone', items: { max: 6, min: 1, item: { text: 't', detail: 't' } } } } },
|
|
76
|
+
risks: { rows: { max: 5, min: 1, item: { name: 't', owner: 't', why: 't', move: 't', level: 't', tone: 'tone' } } },
|
|
77
|
+
ranked: { a: 't', b: 't', rows: { max: 5, min: 1, item: { name: 't', a: 'n', b: 'n' } } },
|
|
78
|
+
decisions: { rows: { max: 3, min: 1, item: { question: 't', pick: 't', who: 't', by: 'date' } } },
|
|
79
|
+
quotes: { agreed: 't', quotes: { max: 3, min: 1, item: { text: 't', who: 't', at: 't' } } },
|
|
80
|
+
actions: { rows: { max: 6, min: 1, item: { text: 't', who: 't', due: 'date' } } },
|
|
81
|
+
agenda: { rows: { max: 8, min: 1, item: { title: 't', text: 't' } } },
|
|
82
|
+
heatmap: { cols: { max: 6, min: 1, text: true }, total: 't', rows: { max: 6, min: 1, item: { name: 't', total: 't', cells: { max: 6, min: 1, item: { text: 't', tone: 'tone' } } } } },
|
|
83
|
+
network: { nodes: { max: 8, min: 2, item: { id: 'id', label: 't', tone: 'tone' } }, edges: { max: 12, min: 1, pair: true } },
|
|
84
|
+
schedule: { cols: { max: 7, min: 1, text: true }, rows: { max: 6, min: 1, item: { name: 't', from: 'i', to: 'i', text: 't', tone: 'tone' } } },
|
|
85
|
+
donut: { center: 't', center_note: 't', parts: { max: 5, min: 1, item: { label: 't', n: 'n+', names: 't', tone: 'tone' } } },
|
|
86
|
+
bubbles: { x: 't', y: 't', points: { max: 8, min: 1, item: { label: 't', x: 'n', y: 'n', size: 'n', tone: 'tone' } } },
|
|
87
|
+
people: { people: { max: 4, min: 1, item: { name: 't', initials: 't', role: 't', owns: 't' } } },
|
|
88
|
+
statement: { text: 't', figures: { max: 3, min: 0, item: { value: 't', label: 't', source: 't', tone: 'tone' } } },
|
|
89
|
+
sources: { read: { max: 6, min: 0, item: { name: 't', at: 'date' } }, missing: { max: 6, min: 0, item: { name: 't', why: 't' } } },
|
|
90
|
+
minutes: { unit: 't', rows: { max: 6, min: 1, item: { name: 't', note: 't', value: 'n+', text: 't' } } },
|
|
91
|
+
focus: { gate: { obj: { title: 't', text: 't' } }, steps: { max: 4, min: 1, item: { label: 't', title: 't', text: 't' } } },
|
|
92
|
+
next: { date: 'date', time: 't', items: { max: 6, min: 1, item: { length: 't', text: 't' } } },
|
|
93
|
+
blocks: { blocks: { max: 12, min: 1, raw: true } },
|
|
94
|
+
};
|
|
95
|
+
const REQUIRED = {
|
|
96
|
+
overview: ['cols'], tiles: ['tiles'], ledger: ['rows'], timeline: ['rows'], compare: ['left', 'right'], lists: ['left', 'right'], risks: ['rows'],
|
|
97
|
+
ranked: ['rows'], decisions: ['rows'], quotes: ['quotes'], actions: ['rows'], agenda: ['rows'], heatmap: ['cols', 'rows'], network: ['nodes', 'edges'],
|
|
98
|
+
schedule: ['cols', 'rows'], donut: ['parts'], bubbles: ['points'], people: ['people'], statement: ['text'], sources: [], minutes: ['rows'],
|
|
99
|
+
focus: ['steps'], next: ['items'], blocks: ['blocks'],
|
|
100
|
+
};
|
|
101
|
+
const TEXT_MAX = 400;
|
|
102
|
+
|
|
103
|
+
function checkValue(kind, v, at) {
|
|
104
|
+
if (v == null) return null;
|
|
105
|
+
if (kind === 't') return isStr(v) && v.length <= TEXT_MAX && !/[\r\n]/.test(v) ? null : `${at} is one line of text (at most ${TEXT_MAX} characters)`;
|
|
106
|
+
if (kind === 'n') return typeof v === 'number' && v >= 0 && v <= 1 ? null : `${at} is a number from 0 to 1 (a share of the bar or the plot)`;
|
|
107
|
+
if (kind === 'n+') return typeof v === 'number' && v >= 0 && Number.isFinite(v) ? null : `${at} is a number, 0 or more`;
|
|
108
|
+
if (kind === 'i') return Number.isInteger(v) && v >= 0 ? null : `${at} is a column number, from 0`;
|
|
109
|
+
if (kind === 'date') return isStr(v) && DATE_RE.test(v) ? null : `${at} is a date, YYYY-MM-DD`;
|
|
110
|
+
if (kind === 'tone') return TONES.includes(v) ? null : `${at} is one of ${TONES.join(', ')}`;
|
|
111
|
+
if (kind === 'id') return isStr(v) && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? null : `${at} is a short id`;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function checkSpec(spec, data, at) {
|
|
115
|
+
if (!isObj(data)) return `${at} is an object`;
|
|
116
|
+
for (const k of Object.keys(data)) if (!(k in spec)) return `${at}.${k} is not part of this layout; it takes ${Object.keys(spec).join(', ')}`;
|
|
117
|
+
for (const [k, s] of Object.entries(spec)) {
|
|
118
|
+
const v = data[k]; const w = `${at}.${k}`;
|
|
119
|
+
if (typeof s === 'string') { const e = checkValue(s, v, w); if (e) return e; continue; }
|
|
120
|
+
if (v == null) continue;
|
|
121
|
+
if (s.obj) { const e = checkSpec(s.obj, v, w); if (e) return e; continue; }
|
|
122
|
+
if (!Array.isArray(v) || v.length > s.max || v.length < s.min) return `${w} is a list of ${s.min} to ${s.max}`;
|
|
123
|
+
for (const [i, x] of v.entries()) {
|
|
124
|
+
if (s.text) { const e = checkValue('t', x, `${w}[${i}]`); if (e) return e; }
|
|
125
|
+
else if (s.pair) { if (!Array.isArray(x) || x.length !== 2 || !x.every(isStr)) return `${w}[${i}] is [from id, to id]`; }
|
|
126
|
+
else if (s.raw) { if (!isObj(x)) return `${w}[${i}] is a block`; }
|
|
127
|
+
else { const e = checkSpec(s.item, x, `${w}[${i}]`); if (e) return e; }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/** null when the data fits the layout, else a sentence that says what to change. */
|
|
133
|
+
function layoutError(name, data) {
|
|
134
|
+
if (!LAYOUTS.includes(name)) return `layout is one of ${LAYOUTS.join(', ')}`;
|
|
135
|
+
if (!isObj(data)) return `data is the ${name} layout's object`;
|
|
136
|
+
for (const k of REQUIRED[name]) if (data[k] == null || (Array.isArray(data[k]) && !data[k].length) || (isStr(data[k]) && !data[k].trim())) return `the ${name} layout needs ${k}`;
|
|
137
|
+
const e = checkSpec(SPEC[name], data, 'data');
|
|
138
|
+
if (e) return e;
|
|
139
|
+
if (name === 'network') { const ids = new Set(data.nodes.map((n) => n.id)); const bad = data.edges.find(([a, b]) => !ids.has(a) || !ids.has(b)); if (bad) return `data.edges names a node that is not in data.nodes: ${bad.join(' → ')}`; }
|
|
140
|
+
if (name === 'schedule') { const n = data.cols.length; const bad = data.rows.find((r) => r.from == null || r.to == null || r.from > r.to || r.to >= n); if (bad) return `data.rows: from and to are column numbers, 0 to ${n - 1}, from ≤ to`; }
|
|
141
|
+
if (name === 'timeline') { const bad = data.rows.find((r) => !r.start || !r.end || r.start > r.end); if (bad) return 'data.rows: each row has a start and an end date, start ≤ end'; }
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function walk(spec, data, fn) {
|
|
145
|
+
if (!isObj(data)) return data;
|
|
146
|
+
const out = {};
|
|
147
|
+
for (const [k, v] of Object.entries(data)) {
|
|
148
|
+
const s = spec[k];
|
|
149
|
+
if (s === 't') out[k] = isStr(v) ? fn(v) : v;
|
|
150
|
+
else if (s && s.obj) out[k] = walk(s.obj, v, fn);
|
|
151
|
+
else if (s && Array.isArray(v)) out[k] = v.map((x) => (s.text ? (isStr(x) ? fn(x) : x) : s.item ? walk(s.item, x, fn) : x));
|
|
152
|
+
else out[k] = v;
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/** A copy of the data with fn applied to every text the writer wrote (the blocks layout is the engine's own and is left as it is). */
|
|
157
|
+
const mapLayout = (name, data, fn) => (SPEC[name] && name !== 'blocks' ? walk(SPEC[name], data, fn) : data);
|
|
158
|
+
/** Every text the writer wrote in the data. */
|
|
159
|
+
function textsOfLayout(name, data) { const out = []; mapLayout(name, data, (t) => { out.push(t); return t; }); return out; }
|
|
160
|
+
|
|
161
|
+
// ── the layout as plain lines (the document and the PowerPoint file) ─────────────────────────────────────────────────
|
|
162
|
+
const join = (...xs) => xs.filter((x) => str(x).trim()).join(' · ');
|
|
163
|
+
function linesOf(name, d) {
|
|
164
|
+
d = d || {};
|
|
165
|
+
const L = (xs, f) => (Array.isArray(xs) ? xs : []).map(f);
|
|
166
|
+
switch (name) {
|
|
167
|
+
case 'overview': return [d.lead || ''].concat(L(d.cols, (c) => join(c.head, c.text))).filter(Boolean);
|
|
168
|
+
case 'tiles': return L(d.tiles, (t) => join(t.label, t.value, t.note));
|
|
169
|
+
case 'ledger': return L(d.rows, (r) => join(r.name, r.value, r.target_text, r.verdict));
|
|
170
|
+
case 'timeline': return L(d.rows, (r) => join(r.name, `${dmy(r.start)} to ${dmy(r.end)}`, r.tag));
|
|
171
|
+
case 'compare': return [d.left, d.right].flatMap((c) => (c ? [c.label || ''].concat(L(c.items, (x) => join(x.value, x.name, x.note))) : [])).filter(Boolean);
|
|
172
|
+
case 'lists': return [d.left, d.right].flatMap((c) => (c ? [c.label || ''].concat(L(c.items, (x) => join(x.text, x.detail))) : [])).filter(Boolean);
|
|
173
|
+
case 'risks': return L(d.rows, (r) => join(r.name, r.owner, r.move, r.level));
|
|
174
|
+
case 'ranked': return L(d.rows, (r, i) => `${i + 1}. ${r.name}`);
|
|
175
|
+
case 'decisions': return L(d.rows, (r) => join(r.question, r.pick, r.who, r.by ? 'by ' + dmy(r.by) : ''));
|
|
176
|
+
case 'quotes': return L(d.quotes, (q) => `“${q.text}” ${join(q.who, q.at)}`).concat(d.agreed ? ['Agreed: ' + d.agreed] : []);
|
|
177
|
+
case 'actions': return L(d.rows, (r) => join(r.text, r.who, r.due ? dmy(r.due) : ''));
|
|
178
|
+
case 'agenda': return L(d.rows, (r, i) => `${i + 1}. ${join(r.title, r.text)}`);
|
|
179
|
+
case 'heatmap': return L(d.rows, (r) => join(r.name, ...(r.cells || []).map((c, i) => `${(d.cols || [])[i] || ''}: ${c.text}`), r.total));
|
|
180
|
+
case 'network': { const lab = new Map(L(d.nodes, (n) => [n.id, n.label])); return L(d.edges, ([a, b]) => `${lab.get(b) || b} waits on ${lab.get(a) || a}`); }
|
|
181
|
+
case 'schedule': return L(d.rows, (r) => join(r.name, `${(d.cols || [])[r.from] || ''} to ${(d.cols || [])[r.to] || ''}`, r.text));
|
|
182
|
+
case 'donut': return [join(d.center, d.center_note)].concat(L(d.parts, (p) => join(`${p.n} ${p.label}`, p.names))).filter(Boolean);
|
|
183
|
+
case 'bubbles': return L(d.points, (p) => p.label);
|
|
184
|
+
case 'people': return L(d.people, (p) => join(p.name, p.role, p.owns));
|
|
185
|
+
case 'statement': return [d.text || ''].concat(L(d.figures, (f) => join(f.value, f.label, f.source))).filter(Boolean);
|
|
186
|
+
case 'sources': return L(d.read, (r) => join('Read: ' + r.name, r.at ? dmy(r.at) : '')).concat(L(d.missing, (m) => join('Not known yet: ' + m.name, m.why)));
|
|
187
|
+
case 'minutes': return L(d.rows, (r) => join(r.name, r.text || `${r.value} ${d.unit || ''}`.trim()));
|
|
188
|
+
case 'focus': return [d.gate ? join(d.gate.title, d.gate.text) : ''].concat(L(d.steps, (s) => join(s.label, s.title, s.text))).filter(Boolean);
|
|
189
|
+
case 'next': return [join(d.date ? longDate(d.date) : '', d.time)].concat(L(d.items, (x) => join(x.length, x.text))).filter(Boolean);
|
|
190
|
+
case 'blocks': return L(d.blocks, (b) => (b.kind === 'p' ? b.text : b.kind === 'list' ? b.items.join(' · ') : b.kind === 'table' ? b.rows.map((r) => r.join(' | ')).join(' · ') : b.kind === 'bars' ? b.rows.map((r) => `${r.label}: ${r.text}`).join(' · ') : '')).filter(Boolean);
|
|
191
|
+
default: return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const longDate = (ymd) => { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(str(ymd)); return m ? `${DAYS[new Date(ms(ymd)).getUTCDay()]} ${+m[3]} ${MONTHS[+m[2] - 1]} ${m[1]}` : str(ymd); };
|
|
195
|
+
|
|
196
|
+
// ── the bodies ──────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
197
|
+
const dot = (t) => `<span class="dot ${DOT[tone(t)]}"></span>`;
|
|
198
|
+
const pill = (t, text) => `<span class="pill ${PILL[tone(t)]}">${esc(text)}</span>`;
|
|
199
|
+
/** a bar with an optional target mark; share and target are 0–1 */
|
|
200
|
+
const bar = (share, target, t, o = {}) => `<div class="bar2"${o.h ? ` style="height:${o.h}px;border-radius:${o.h / 2}px"` : ''}><i class="${BAR[tone(t)]}" style="width:${pct(share)}%${o.h ? `;border-radius:${o.h / 2}px` : ''}"></i>${target != null ? `<em style="left:${pct(target)}%"></em>` : ''}</div>`;
|
|
201
|
+
const has = (v) => v != null && str(v).trim() !== '';
|
|
202
|
+
|
|
203
|
+
const BODY = {
|
|
204
|
+
overview(d) {
|
|
205
|
+
const cols = d.cols.map((c, i) => `<div class="col"><div class="q4 big">${i + 1}</div><h3 class="t18">${esc(c.head)}</h3>${has(c.text) ? `<p class="ink2">${esc(c.text)}</p>` : ''}</div>`).join('');
|
|
206
|
+
return `<div class="stack">${has(d.lead) ? `<div class="strip"><span class="face" role="img" aria-label="Adler"></span><p class="t">${esc(d.lead)}</p></div>` : ''}<div class="cols3">${cols}</div></div>`;
|
|
207
|
+
},
|
|
208
|
+
tiles(d) {
|
|
209
|
+
const tiles = d.tiles.map((t) => `<div class="tile"><span class="lb2">${esc(t.label)}</span><span class="big">${esc(t.value)}</span>${has(t.note) ? `<span class="d">${esc(t.note)}</span>` : ''}${t.share != null ? `<div class="bar2 mt6"><i class="${BAR[tone(t.tone)]}" style="width:${pct(t.share)}%"></i>${t.target != null ? `<em style="left:${pct(t.target)}%"></em>` : ''}</div>` : ''}</div>`).join('');
|
|
210
|
+
return `<div class="tiles" style="grid-template-columns:repeat(${d.tiles.length},minmax(0,1fr))">${tiles}</div>`;
|
|
211
|
+
},
|
|
212
|
+
ledger(d) {
|
|
213
|
+
const rows = d.rows.map((r) => `<div class="lg"><div class="t15">${dot(r.tone)}${esc(r.name)}</div><div class="t18">${esc(r.value)}</div><div>${r.share != null ? bar(r.share, r.target, r.tone) : ''}${has(r.target_text) ? `<div class="meta mt6">${esc(r.target_text)}</div>` : ''}</div>${has(r.verdict) ? `<span class="pill ${PILL[tone(r.tone)]} je">${esc(r.verdict)}</span>` : '<span></span>'}</div>`).join('');
|
|
214
|
+
return `${has(d.label) ? `<div class="lb">${esc(d.label)}</div>` : ''}<div class="fl rows">${rows}</div>`;
|
|
215
|
+
},
|
|
216
|
+
timeline(d, ctx) { return timelineBody(d, ctx); },
|
|
217
|
+
compare(d) {
|
|
218
|
+
const side = (c) => `<div class="side"><div class="lb pb8">${esc(c.label)}</div>${c.items.map((x) => `<div class="cmp"><div class="bl"><span class="t26">${esc(x.value)}</span><span class="t15">${esc(x.name)}</span></div>${has(x.note) ? `<div class="meta nm">${esc(x.note)}</div>` : ''}${x.share != null ? bar(x.share, x.target, x.tone) : ''}</div>`).join('')}</div>`;
|
|
219
|
+
return `<div class="two-col">${side(d.left)}<div class="vr"></div>${side(d.right)}</div>`;
|
|
220
|
+
},
|
|
221
|
+
lists(d) {
|
|
222
|
+
const side = (c, def) => `<div class="side"><div class="lb pb8">${esc(c.label)}</div>${c.items.map((x) => `<div class="lrow big-row">${dot(c.tone || def)}<span class="tx t">${esc(x.text)}${has(x.detail) ? `<span class="meta block">${esc(x.detail)}</span>` : ''}</span></div>`).join('')}</div>`;
|
|
223
|
+
return `<div class="two-col">${side(d.left, 'ok')}<div class="vr"></div>${side(d.right, 'bad')}</div>`;
|
|
224
|
+
},
|
|
225
|
+
risks(d) {
|
|
226
|
+
const g = 'grid-template-columns:1.6fr 2fr 1.1fr';
|
|
227
|
+
const rows = d.rows.map((r) => `<div class="kr" style="${g}"><div class="n">${dot(r.tone)}${esc(r.name)}${has(r.owner) || has(r.why) ? `<small>${esc(join(r.owner, r.why))}</small>` : ''}</div><div>${esc(r.move)}</div><div>${has(r.level) ? pill(r.tone, r.level) : ''}</div></div>`).join('');
|
|
228
|
+
return `<div class="sec col0"><div class="th2" style="${g}"><span>Item and owner</span><span>Next step</span><span>Level</span></div>${rows}</div>`;
|
|
229
|
+
},
|
|
230
|
+
ranked(d) {
|
|
231
|
+
const rows = d.rows.map((r, i) => `<div class="lrow grow"><span class="ic">${i + 1}</span><span class="tx t">${esc(r.name)}</span>${r.a != null ? `<div class="w190"><div class="meta">${esc(d.a || 'Impact')}</div>${bar(r.a, null, 'info')}</div>` : ''}${r.b != null ? `<div class="w190 ml24"><div class="meta">${esc(d.b || 'Effort')}</div>${bar(r.b, null, 'none')}</div>` : ''}</div>`).join('');
|
|
232
|
+
return `<div class="sec fl first-flat">${rows}</div>`;
|
|
233
|
+
},
|
|
234
|
+
decisions(d, ctx) {
|
|
235
|
+
const rows = d.rows.map((r, i) => `<div class="dr pad"><span class="sp">✦</span><span class="f1"><b class="fs18">${esc(r.question)}</b>${has(r.pick) ? `<span class="meta block fs15">${i ? '' : '✦ '}${esc(r.pick)}</span>` : ''}</span>${has(r.who) || r.by ? `<span class="x"><span class="u">${esc(join(r.who, r.by ? 'by ' + onSlide(r.by, ctx.today) : ''))}</span></span>` : ''}</div>`).join('');
|
|
236
|
+
return `<div class="gap14 col0">${rows}</div>`;
|
|
237
|
+
},
|
|
238
|
+
quotes(d) {
|
|
239
|
+
const qs = d.quotes.map((q) => `<div class="col"><div class="q4">“</div><p class="quote">${esc(q.text)}</p>${has(q.who) || has(q.at) ? `<div class="mt16"><span class="pill">${esc(join(q.who, q.at))}</span></div>` : ''}</div>`).join('');
|
|
240
|
+
return `<div class="stack"><div class="cols3 grow">${qs}</div>${has(d.agreed) ? `<div class="dr"><span class="sp">✦</span><span><b>Agreed:</b> ${esc(d.agreed)}</span></div>` : ''}</div>`;
|
|
241
|
+
},
|
|
242
|
+
actions(d, ctx) {
|
|
243
|
+
const id = `rs${ctx.n}a`;
|
|
244
|
+
const rows = d.rows.map((r, i) => `<label class="lrow act grow" for="${id}${i}"><input type="checkbox" class="i-a" id="${id}${i}"><span class="chk" aria-hidden="true"></span><span class="tx t fs18">${esc(r.text)}</span>${has(r.who) ? `<span class="avl av2">${esc(initials(r.who))}</span>` : ''}${r.due ? `<span class="meta due">${esc(dueText(r.due, ctx.today))}</span>` : ''}</label>`).join('');
|
|
245
|
+
return `<div class="acts stack0"><div class="fl">${rows}</div><div class="dr amsg"><span class="sp">✦</span><span class="a-left"></span><span class="a-done">All done. Nothing carries over.</span></div></div>`;
|
|
246
|
+
},
|
|
247
|
+
agenda(d) {
|
|
248
|
+
const rows = d.rows.map((r, i) => `<div class="ag"><span class="t26 num">${i + 1}</span><div class="f1"><div class="t18">${esc(r.title)}</div>${has(r.text) ? `<div class="ink2">${esc(r.text)}</div>` : ''}</div></div>`).join('');
|
|
249
|
+
return `<div class="fl rows">${rows}</div>`;
|
|
250
|
+
},
|
|
251
|
+
heatmap(d) {
|
|
252
|
+
const n = d.cols.length;
|
|
253
|
+
const head = `<span></span>${d.cols.map((c) => `<span class="lb tc">${esc(c)}</span>`).join('')}${has(d.total) ? `<span class="lb tr">${esc(d.total)}</span>` : ''}`;
|
|
254
|
+
const rows = d.rows.map((r) => `<span class="t18 ac">${esc(r.name)}</span>${Array.from({ length: n }, (_, i) => { const c = (r.cells || [])[i]; return c ? `<div class="cell ${CELL[tone(c.tone)]}">${esc(c.text)}</div>` : '<div class="cell q">—</div>'; }).join('')}${has(d.total) ? `<span class="t18 ac tr">${esc(r.total)}</span>` : ''}`).join('');
|
|
255
|
+
return `<div class="heat" style="grid-template-columns:150px repeat(${n},minmax(0,1fr))${has(d.total) ? ' 110px' : ''};grid-template-rows:auto repeat(${d.rows.length},1fr)">${head}${rows}</div>`;
|
|
256
|
+
},
|
|
257
|
+
network(d) { return networkBody(d); },
|
|
258
|
+
schedule(d) {
|
|
259
|
+
const n = d.cols.length; const p = (100 / n).toFixed(3);
|
|
260
|
+
const grid = `background:repeating-linear-gradient(to right,transparent 0,transparent calc(${p}% - 1px),rgba(42,46,53,.08) calc(${p}% - 1px),rgba(42,46,53,.08) ${p}%)`;
|
|
261
|
+
const rows = d.rows.map((r) => `<div class="tr"><div class="nm">${esc(r.name)}</div><div class="tk" style="${grid}"><span class="tb ${TB[tone(r.tone)]}" style="left:${(r.from * 100 / n).toFixed(1)}%;width:${((r.to - r.from + 1) * 100 / n).toFixed(1)}%">${esc(r.text)}</span></div></div>`).join('');
|
|
262
|
+
return `<div class="tg col0"><div class="tr h"><div class="nm">Work</div><div class="tk" style="grid-template-columns:repeat(${n},minmax(0,1fr))">${d.cols.map((c) => `<span>${esc(c)}</span>`).join('')}</div></div>${rows}</div>`;
|
|
263
|
+
},
|
|
264
|
+
donut(d) {
|
|
265
|
+
const total = d.parts.reduce((a, x) => a + x.n, 0) || 1;
|
|
266
|
+
let at = 0;
|
|
267
|
+
const stops = d.parts.map((x) => { const a = at; at += (x.n / total) * 100; return `${RING[tone(x.tone)]} ${a.toFixed(1)}% ${at.toFixed(1)}%`; }).join(',');
|
|
268
|
+
const legend = d.parts.map((x) => `<div class="lrow grow"><span class="dot big-dot" style="background:${RING[tone(x.tone)]}"></span><span class="t26 w56">${esc(String(x.n))}</span><span class="tx t">${esc(x.label)}${has(x.names) ? `<span class="meta block">${esc(x.names)}</span>` : ''}</span></div>`).join('');
|
|
269
|
+
return `<div class="donut-row"><div class="ring" role="img" aria-label="${esc(d.parts.map((x) => `${x.n} ${x.label}`).join(', '))}" style="background:conic-gradient(${stops})"><div class="hole">${has(d.center) ? `<div class="t26">${esc(d.center)}</div>` : ''}${has(d.center_note) ? `<div class="meta">${esc(d.center_note)}</div>` : ''}</div></div><div class="fl rows st">${legend}</div></div>`;
|
|
270
|
+
},
|
|
271
|
+
bubbles(d) {
|
|
272
|
+
const pts = d.points.map((p) => { const s = Math.round(48 + 60 * (p.size == null ? 0.4 : p.size)); return `<div class="bub" style="left:${pct(p.x)}%;top:${(100 - pct(p.y)).toFixed(1)}%;width:${s}px;height:${s}px;margin:-${s / 2}px 0 0 -${s / 2}px;background:${FILL[tone(p.tone)]}">${esc(p.label)}</div>`; }).join('');
|
|
273
|
+
return `<div class="plot-wrap"><div class="lb">${esc(d.y || 'Outcome')} ↑</div><div class="plot">${pts}</div><div class="lb tr">${esc(d.x || 'More spend')} →</div></div>`;
|
|
274
|
+
},
|
|
275
|
+
people(d) {
|
|
276
|
+
const ps = d.people.map((p) => `<div class="col person"><div class="avl">${esc(p.initials || initials(p.name))}</div><h3 class="t18 mt6">${esc(p.name)}</h3>${has(p.role) ? `<span class="pill y">${esc(p.role)}</span>` : ''}${has(p.owns) ? `<p class="ink2">${esc(p.owns)}</p>` : ''}</div>`).join('');
|
|
277
|
+
return `<div class="cols3">${ps}</div>`;
|
|
278
|
+
},
|
|
279
|
+
statement(d) {
|
|
280
|
+
const figs = (d.figures || []).map((f) => `<div class="col"><div class="t26" style="color:${FILL[tone(f.tone)] === FILL.none ? 'var(--ink)' : FILL[tone(f.tone)]}">${esc(f.value)}</div><div class="t15 mt4">${esc(f.label)}</div>${has(f.source) ? `<div class="meta">${esc(f.source)}</div>` : ''}</div>`).join('');
|
|
281
|
+
return `<div class="stack"><p class="say">${esc(d.text)}</p>${figs ? `<div class="cols3 at-end">${figs}</div>` : ''}</div>`;
|
|
282
|
+
},
|
|
283
|
+
sources(d) {
|
|
284
|
+
const read = (d.read || []).map((r) => `<div class="lrow"><span class="tx t">${esc(r.name)}</span>${r.at ? `<span class="meta">${esc(dmy(r.at))}</span>` : ''}<span class="pill ok">Read</span></div>`).join('');
|
|
285
|
+
const miss = (d.missing || []).map((m) => `<div class="lrow"><span class="tx"><span class="t block">${esc(m.name)}</span>${has(m.why) ? `<span class="meta block">${esc(m.why)}</span>` : ''}</span><span class="pill q">no source</span></div>`).join('');
|
|
286
|
+
return `<div class="two-col">${read ? `<div class="col0"><div class="lb pb6">Read</div>${read}</div>` : ''}${read && miss ? '<div class="vr"></div>' : ''}${miss ? `<div class="col0"><div class="lb pb6">Not known yet</div>${miss}</div>` : ''}</div>`;
|
|
287
|
+
},
|
|
288
|
+
minutes(d) {
|
|
289
|
+
const max = Math.max(1, ...d.rows.map((r) => r.value || 0));
|
|
290
|
+
const rows = d.rows.map((r) => `<div class="mn"><div><div class="t15">${esc(r.name)}</div>${has(r.note) ? `<div class="meta">${esc(r.note)}</div>` : ''}</div>${bar((r.value || 0) / max, null, 'info', { h: 10 })}<div class="t18 tr">${esc(r.text || `${r.value} ${d.unit || ''}`.trim())}</div></div>`).join('');
|
|
291
|
+
return `<div class="fl rows">${rows}</div>`;
|
|
292
|
+
},
|
|
293
|
+
focus(d) {
|
|
294
|
+
const SH = ['#9FB8E0', '#6F92DC', '#2B55C9', '#1E449B'];
|
|
295
|
+
const steps = d.steps.map((s, i) => `<div class="step" style="border-top-color:${SH[i + (4 - d.steps.length)] || SH[3]}"><div class="lb">${esc(s.label)}</div><div class="t18 mt4">${esc(s.title)}</div>${has(s.text) ? `<div class="ink2 mt6">${esc(s.text)}</div>` : ''}</div>`).join('');
|
|
296
|
+
return `<div class="stack">${d.gate ? `<div class="dr gate"><span class="sp">✦</span><span class="f1"><b class="t26">${esc(d.gate.title)}</b>${has(d.gate.text) ? `<span class="meta block fs15 mt4">${esc(d.gate.text)}</span>` : ''}</span></div>` : ''}<div class="steps">${steps}</div></div>`;
|
|
297
|
+
},
|
|
298
|
+
next(d) {
|
|
299
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(str(d.date));
|
|
300
|
+
const when = m ? `<div class="when"><div class="lb">${MONTHS[+m[2] - 1]} · ${DAYS[new Date(ms(d.date)).getUTCDay()]}</div><div class="t26">${+m[3]}</div>${has(d.time) ? `<div class="t18 mt6 blue">${esc(d.time)}</div>` : ''}</div>` : '';
|
|
301
|
+
const items = d.items.map((x) => `<div class="lrow grow">${has(x.length) ? `<span class="meta w70">${esc(x.length)}</span>` : ''}<span class="tx t fs18">${esc(x.text)}</span></div>`).join('');
|
|
302
|
+
return `<div class="next-row">${when}<div class="fl"><div class="lb pb6">Draft agenda</div>${items}</div></div>`;
|
|
303
|
+
},
|
|
304
|
+
blocks(d) { return `<div class="blk">${d.blocks.map(blockHtml).join('')}</div>`; },
|
|
305
|
+
};
|
|
306
|
+
const initials = (name) => str(name).split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join('');
|
|
307
|
+
const dueText = (ymd, today) => { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(str(ymd)); return m ? `${DAYS[new Date(ms(ymd)).getUTCDay()].slice(0, 3)} ${+m[3]} ${MONS[+m[2] - 1]}${today && m[1] !== str(today).slice(0, 4) ? ' ' + m[1] : ''}` : str(ymd); };
|
|
308
|
+
|
|
309
|
+
/** One block of an engine section (p, list, table, bars), in the B2 frame. */
|
|
310
|
+
function blockHtml(b) {
|
|
311
|
+
if (!isObj(b)) return '';
|
|
312
|
+
if (b.kind === 'p') return `<p class="bp">${esc(b.text)}</p>`;
|
|
313
|
+
if (b.kind === 'list') return `<div class="bl-list">${(b.items || []).map((x) => `<div class="lrow">${dot('none')}<span class="tx">${esc(x)}</span></div>`).join('')}</div>`;
|
|
314
|
+
if (b.kind === 'table') {
|
|
315
|
+
const n = (b.columns || []).length || 1;
|
|
316
|
+
const g = `grid-template-columns:repeat(${n},minmax(0,1fr))`;
|
|
317
|
+
return `<div class="sec bt"><div class="th2" style="${g}">${(b.columns || []).map((c) => `<span>${esc(c)}</span>`).join('')}</div>${(b.rows || []).map((r) => `<div class="kr" style="${g}">${r.map((c, i) => `<div${i ? '' : ' class="n"'}>${esc(c)}</div>`).join('')}</div>`).join('')}</div>`;
|
|
318
|
+
}
|
|
319
|
+
if (b.kind === 'bars') return `<div class="bt">${(b.rows || []).map((r) => `<div class="mn"><div class="t15">${esc(r.label)}</div>${bar(r.share, null, 'info', { h: 10 })}<div class="t18 tr">${esc(r.text)}</div></div>`).join('')}</div>`;
|
|
320
|
+
return '';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* the Timeline: every span is drawn; the picked radio shows one. Quarter is the calendar quarter of today, 6 months runs from three months
|
|
324
|
+
* back, Year from four months back. The first view is the smallest that holds every row; a row outside the view says where it lies
|
|
325
|
+
* ("ended 17 Jul ←", "starts 11 Aug 2027 →"), so past work at risk never shows as an empty row. */
|
|
326
|
+
function windows(today) {
|
|
327
|
+
const t = new Date(ms(today));
|
|
328
|
+
const y = t.getUTCFullYear(); const m = t.getUTCMonth();
|
|
329
|
+
const mk = (y0, m0, n) => { const cols = []; for (let i = 0; i < n; i++) cols.push(MONS[(m0 + i + 120) % 12]); const a = Date.UTC(y0, m0, 1); const b = Date.UTC(y0, m0 + n, 1) - DAY; return { a, b, cols }; };
|
|
330
|
+
const q0 = m - (m % 3);
|
|
331
|
+
return { q: mk(y, q0, 3), h: mk(y, m - 3, 6), y: mk(y, m - 4, 12) };
|
|
332
|
+
}
|
|
333
|
+
/** The view the slide opens on, and the one the PDF and the PowerPoint file show: the smallest that holds every row, else Year. */
|
|
334
|
+
function timelineView(d, today) {
|
|
335
|
+
const W = windows(today);
|
|
336
|
+
const k = ['q', 'h', 'y'].find((x) => d.rows.every((r) => ms(r.start) >= W[x].a && ms(r.end) <= W[x].b)) || 'y';
|
|
337
|
+
return { key: k, ...W[k] };
|
|
338
|
+
}
|
|
339
|
+
function timelineToday(d, ctx) { return d.today || ctx.today || d.rows.map((r) => r.start).sort()[0]; }
|
|
340
|
+
/** One row's place in a window, in percent: { vis, left, width, label, out } (out: 'before' | 'after' | null). Shared with the PowerPoint file. */
|
|
341
|
+
function timelinePlace(r, w, today) {
|
|
342
|
+
const span = w.b + DAY - w.a;
|
|
343
|
+
const tail = join(r.tag, onSlide(r.end, today));
|
|
344
|
+
if (ms(r.end) < w.a) return { vis: false, out: 'before', label: `← ${join(r.tag, 'ended ' + onSlide(r.end, today))}` };
|
|
345
|
+
if (ms(r.start) > w.b) return { vis: false, out: 'after', label: `${join(r.tag, 'starts ' + onSlide(r.start, today))} →` };
|
|
346
|
+
const s = Math.max(ms(r.start), w.a); const e = Math.min(ms(r.end) + DAY, w.b + DAY);
|
|
347
|
+
const left = ((s - w.a) / span) * 100; const width = Math.min(Math.max(1.5, ((e - s) / span) * 100), 100 - left);
|
|
348
|
+
return { vis: true, out: null, left, width, label: tail, inside: width > 14 };
|
|
349
|
+
}
|
|
350
|
+
function timelineBody(d, ctx) {
|
|
351
|
+
const today = timelineToday(d, ctx);
|
|
352
|
+
const W = windows(today);
|
|
353
|
+
const first = timelineView(d, today).key;
|
|
354
|
+
const id = `rs${ctx.n}t`;
|
|
355
|
+
const risky = (r) => r.tone === 'watch' || r.tone === 'late' || r.tone === 'bad';
|
|
356
|
+
const span = (k) => {
|
|
357
|
+
const w = W[k]; const n = w.cols.length; const p = (100 / n).toFixed(3);
|
|
358
|
+
const tx = ((ms(today) - w.a) / (w.b + DAY - w.a)) * 100;
|
|
359
|
+
const grid = `background:repeating-linear-gradient(to right,transparent 0,transparent calc(${p}% - 1px),rgba(42,46,53,.08) calc(${p}% - 1px),rgba(42,46,53,.08) ${p}%)`;
|
|
360
|
+
const rows = d.rows.map((r) => {
|
|
361
|
+
const g = timelinePlace(r, w, today);
|
|
362
|
+
let mark = '';
|
|
363
|
+
if (g.out) mark = `<span class="tout ${g.out === 'before' ? 'ob' : 'oa'} ${TB[tone(r.tone)]}">${esc(g.label)}</span>`;
|
|
364
|
+
else {
|
|
365
|
+
const right = g.left + g.width > 70;
|
|
366
|
+
mark = `<span class="tb ${TB[tone(r.tone)]}" style="left:${g.left.toFixed(1)}%;width:${g.width.toFixed(1)}%">${g.inside ? esc(g.label) : ''}</span>`
|
|
367
|
+
+ (g.inside ? '' : `<span class="tbl" style="${right ? `right:calc(${(100 - g.left).toFixed(1)}% + 6px)` : `left:calc(${(g.left + g.width).toFixed(1)}% + 6px)`}">${esc(g.label)}</span>`);
|
|
368
|
+
}
|
|
369
|
+
return `<div class="tr${risky(r) ? '' : ' calm'}"><div class="nm">${esc(r.name)}</div><div class="tk" style="${grid}">${mark}${tx >= 0 && tx <= 100 ? `<i class="today" style="left:${tx.toFixed(1)}%"></i>` : ''}</div></div>`;
|
|
370
|
+
}).join('');
|
|
371
|
+
return `<div class="tg w-${k}"><div class="tr h"><div class="nm">Initiative</div><div class="tk" style="grid-template-columns:repeat(${n},minmax(0,1fr))">${w.cols.map((c) => `<span>${c}</span>`).join('')}</div></div>${rows}</div>`;
|
|
372
|
+
};
|
|
373
|
+
const radio = (k) => `<input type="radio" class="i-${k}" name="${id}" id="${id}${k}"${k === first ? ' checked' : ''}>`;
|
|
374
|
+
const anyRisk = d.rows.some(risky);
|
|
375
|
+
return `<div class="tl fl">${radio('q')}${radio('h')}${radio('y')}${anyRisk ? `<input type="radio" class="i-fa" name="${id}f" id="${id}a" checked><input type="radio" class="i-r" name="${id}f" id="${id}r">` : ''}`
|
|
376
|
+
+ `<div class="cl ctl">${anyRisk ? `<span class="seg" role="group" aria-label="Rows"><label class="sb l-fa" for="${id}a">All</label><label class="sb l-r" for="${id}r">At risk or late</label></span>` : ''}<span class="seg ml-auto" role="group" aria-label="Span"><label class="sb l-q" for="${id}q">Quarter</label><label class="sb l-h" for="${id}h">6 months</label><label class="sb l-y" for="${id}y">Year</label></span></div>`
|
|
377
|
+
+ `<div class="tlw">${span('q')}${span('h')}${span('y')}</div></div>`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* the network: nodes in columns by how deep they sit, curves between them */
|
|
381
|
+
function networkBody(d) {
|
|
382
|
+
const ids = d.nodes.map((n) => n.id);
|
|
383
|
+
const depth = new Map(ids.map((x) => [x, 0]));
|
|
384
|
+
for (let k = 0; k < ids.length; k++) for (const [a, b] of d.edges) if (depth.get(b) < depth.get(a) + 1) depth.set(b, Math.min(depth.get(a) + 1, ids.length));
|
|
385
|
+
const cols = Math.max(...depth.values()) + 1;
|
|
386
|
+
const byCol = Array.from({ length: cols }, () => []);
|
|
387
|
+
for (const n of d.nodes) byCol[depth.get(n.id)].push(n);
|
|
388
|
+
const W = 820; const H = 470; const nw = 176; const nh = 56;
|
|
389
|
+
const gx = cols > 1 ? (W - nw) / (cols - 1) : 0;
|
|
390
|
+
const pos = new Map();
|
|
391
|
+
byCol.forEach((list, c) => list.forEach((n, i) => pos.set(n.id, { x: Math.round(c * gx), y: Math.round(((i + 1) * H) / (list.length + 1) - nh / 2) })));
|
|
392
|
+
const paths = d.edges.map(([a, b]) => { const p = pos.get(a); const q = pos.get(b); const x1 = p.x + nw; const y1 = p.y + nh / 2; const x2 = q.x; const y2 = q.y + nh / 2; const mx = (x1 + x2) / 2; return `<path d="M${x1} ${y1} C${mx} ${y1} ${mx} ${y2} ${x2} ${y2}"/>`; }).join('');
|
|
393
|
+
const nodes = d.nodes.map((n) => { const p = pos.get(n.id); const t = tone(n.tone); return `<div class="node ${t === 'bad' ? 'r' : t === 'watch' || t === 'late' ? 'w' : ''}" style="left:${p.x}px;top:${p.y}px">${esc(n.label)}</div>`; }).join('');
|
|
394
|
+
return `<div class="net"><svg class="net-svg" width="${W}" height="${H}" aria-hidden="true">${paths}</svg>${nodes}</div>`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function insightHtml(ins) {
|
|
398
|
+
if (!isObj(ins) || !(has(ins.head) || has(ins.text) || (ins.points || []).length)) return '';
|
|
399
|
+
return `<aside class="ins"><span class="k">✦ Insight</span>${has(ins.head) ? `<h3 class="t18">${esc(ins.head)}</h3>` : ''}${has(ins.text) ? `<p>${esc(ins.text)}</p>` : ''}`
|
|
400
|
+
+ `${(ins.points || []).length ? `<ul>${ins.points.map((p) => `<li>${dot(p.tone)}<span>${esc(p.text)}</span></li>`).join('')}</ul>` : ''}${has(ins.note) ? `<div class="note"><b>Notes.</b> ${esc(ins.note)}</div>` : ''}</aside>`;
|
|
401
|
+
}
|
|
402
|
+
/** One slide in the B2 frame. ctx = { deck, total, today } */
|
|
403
|
+
function slideHtml(s, ctx = {}) {
|
|
404
|
+
const n = s.n || 1; const c = { ...ctx, n };
|
|
405
|
+
const name = LAYOUTS.includes(s.layout) ? s.layout : 'blocks';
|
|
406
|
+
let body;
|
|
407
|
+
try { body = BODY[name](s.data || {}, c); } catch { body = ''; }
|
|
408
|
+
const ins = insightHtml(s.insight);
|
|
409
|
+
const live = name === 'timeline' || name === 'actions';
|
|
410
|
+
return `<section class="pmos-slide rs" data-kind="${esc(s.kind || 'layout')}" data-layout="${name}" data-title="${esc(s.title)}"${live ? ' data-pmos-live="css"' : ''}>`
|
|
411
|
+
+ `<div class="p2"><div class="hd2">${has(s.eyebrow) ? `<div class="eb">${esc(s.eyebrow)}</div>` : ''}<h1 class="ti">${esc(s.title)}</h1>${has(s.sub) ? `<div class="sb2">${esc(s.sub)}</div>` : ''}</div>`
|
|
412
|
+
+ `<div class="bd2"><div class="sp2${ins ? '' : ' solo'}"><div class="mc">${body}</div>${ins}</div></div>`
|
|
413
|
+
+ `<div class="ft2"><span class="src">${esc(s.source || '')}</span>${has(ctx.deck) ? `<span class="dk">${esc(ctx.deck)}</span>` : ''}<span class="no">${n}${ctx.total ? ' / ' + ctx.total : ''}</span></div></div></section>`;
|
|
414
|
+
}
|
|
415
|
+
function deckHtml(deck) {
|
|
416
|
+
if (!deck || !Array.isArray(deck.slides)) return '';
|
|
417
|
+
const ctx = { deck: deck.deck || deck.name || '', total: deck.slides.length, today: deck.today || null };
|
|
418
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${esc(deck.name || 'Report')}</title><style>${CSS}</style></head><body>${deck.slides.map((s) => slideHtml(s, ctx)).join('')}</body></html>`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ── the style: the library's tokens and parts (canvas boards L01–L23), no child combinator ─────────────────────────────
|
|
422
|
+
const CSS = `.rs{--ink:#2A2E35;--ink2:#4A4F58;--ink3:#646A74;--well:#F4F3F0;--hair:rgba(42,46,53,.08);--hair2:rgba(42,46,53,.14);--blue:#4894EA;--blueink:#2F5FB3;--bluebg:#EEF5FC;--cta:#1E449B;--late:#A4461A;--latebg:#FBF0EA;--risk:#8A5A00;--riskbg:#FBF4E4;--red:#A8322A;--redbg:#FBEDEC;--ok:#3E6E49;--okbg:#EDF5EF;
|
|
423
|
+
width:1280px;height:720px;position:relative;overflow:hidden;box-sizing:border-box;font-family:"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif;font-size:15px;line-height:1.45;font-weight:400;color:var(--ink);background:linear-gradient(170deg,#FAF9F7 0%,#F2F4F7 60%,#E4EEF9 100%);-webkit-font-smoothing:antialiased}
|
|
424
|
+
.rs *{box-sizing:border-box}
|
|
425
|
+
.rs h1,.rs h3,.rs p{margin:0}
|
|
426
|
+
.rs b{font-weight:600}
|
|
427
|
+
.rs .block{display:block}
|
|
428
|
+
.rs .p2{position:absolute;left:10px;top:10px;right:10px;bottom:10px;background:#fff;border-radius:28px;box-shadow:0 30px 70px -36px rgba(42,46,53,.28);padding:22px 28px 0;display:flex;flex-direction:column}
|
|
429
|
+
.rs .hd2{padding-bottom:16px;border-bottom:1px solid var(--hair2)}
|
|
430
|
+
.rs .eb{font-size:13px;font-weight:600;color:var(--ink3)}
|
|
431
|
+
.rs .ti{font-size:26px;line-height:1.2;font-weight:600;letter-spacing:-.01em;margin-top:4px;max-width:1000px}
|
|
432
|
+
.rs .sb2{font-size:15px;color:var(--ink3);margin-top:6px;max-width:900px}
|
|
433
|
+
.rs .bd2{flex:1;min-height:0;padding:14px 0 12px}
|
|
434
|
+
.rs .ft2{height:46px;border-top:1px solid var(--hair);display:flex;align-items:center;gap:16px;font-size:13px;color:var(--ink3)}
|
|
435
|
+
.rs .ft2 .src{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rs .ft2 .dk{color:var(--ink2);font-weight:600}.rs .ft2 .no{color:var(--ink);font-weight:600;min-width:52px;text-align:right}
|
|
436
|
+
.rs .sp2{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:0 28px;height:100%}
|
|
437
|
+
.rs .sp2.solo{grid-template-columns:minmax(0,1fr)}
|
|
438
|
+
.rs .mc{display:flex;flex-direction:column;gap:14px;min-width:0;min-height:0}
|
|
439
|
+
.rs .t26{font-size:26px;font-weight:600;letter-spacing:-.01em;line-height:1.2}.rs .t18{font-size:18px;font-weight:600;line-height:1.3}.rs .t15{font-size:15px;font-weight:600}
|
|
440
|
+
.rs .meta{font-size:13px;color:var(--ink3);font-weight:400}.rs .lb{font-size:13px;font-weight:600;color:var(--ink3)}.rs .ink2{color:var(--ink2);margin-top:6px}
|
|
441
|
+
.rs .mt4{margin-top:4px}.rs .mt6{margin-top:6px}.rs .mt16{margin-top:16px}.rs .pb6{padding-bottom:6px}.rs .pb8{padding-bottom:8px}.rs .f1{flex:1;min-width:0}.rs .fs15{font-size:15px}.rs .fs18{font-size:18px}
|
|
442
|
+
.rs .tc{text-align:center}.rs .tr{text-align:right}.rs .ac{align-self:center}.rs .je{justify-self:end}.rs .blue{color:var(--blueink)}.rs .ml-auto{margin-left:auto}.rs .ml24{margin-left:24px}.rs .w190{width:190px;flex:none}.rs .w56{width:56px;flex:none}.rs .w70{width:70px;flex:none}
|
|
443
|
+
.rs .pill{display:inline-flex;align-items:center;gap:5px;height:22px;padding:0 9px;border-radius:999px;font-size:12px;font-weight:600;background:var(--well);color:var(--ink2);white-space:nowrap}
|
|
444
|
+
.rs .pill.l{background:var(--latebg);color:var(--late)}.rs .pill.ok{background:var(--okbg);color:var(--ok)}.rs .pill.w{background:var(--riskbg);color:var(--risk)}.rs .pill.y{background:var(--bluebg);color:var(--blueink)}.rs .pill.r{background:var(--redbg);color:var(--red)}.rs .pill.q{background:transparent;border:1px dashed var(--hair2);color:var(--ink3)}
|
|
445
|
+
.rs .dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex:none;margin-right:6px;vertical-align:middle}
|
|
446
|
+
.rs .d-ok{background:#5E9C6A}.rs .d-late{background:#D9773F}.rs .d-risk{background:#D4A13A}.rs .d-red{background:#D0473E}.rs .d-info{background:#4894EA}.rs .d-q{background:transparent;border:1.5px solid #B9BDC4}
|
|
447
|
+
.rs .big-dot{width:14px;height:14px}
|
|
448
|
+
.rs .strip{display:flex;align-items:center;gap:12px;background:var(--well);border-radius:16px;padding:8px 12px;font-size:15px}
|
|
449
|
+
.rs .strip .t{flex:1;color:var(--ink2)}
|
|
450
|
+
.rs .face{width:44px;height:44px;border-radius:50%;background:#4C8FE0;flex:none;position:relative;box-shadow:inset 0 -6px 0 rgba(0,0,0,.08)}
|
|
451
|
+
.rs .face::before{content:"";position:absolute;left:9px;top:11px;width:26px;height:22px;border-radius:50%;background:#FBF3E6}
|
|
452
|
+
.rs .face::after{content:"";position:absolute;left:16px;top:19px;width:4px;height:4px;border-radius:50%;background:#2A2E35;box-shadow:8px 0 0 #2A2E35}
|
|
453
|
+
.rs .stack{display:flex;flex-direction:column;gap:22px;height:100%}
|
|
454
|
+
.rs .stack0{display:flex;flex-direction:column;gap:14px;height:100%}
|
|
455
|
+
.rs .cols3{display:flex;flex:1;margin-left:-24px}
|
|
456
|
+
.rs .cols3.at-end{flex:none;margin-top:auto}
|
|
457
|
+
.rs .col{flex:1;padding:0 24px;border-left:1px solid var(--hair2);min-width:0}
|
|
458
|
+
.rs .person{display:flex;flex-direction:column;align-items:flex-start;gap:8px}
|
|
459
|
+
.rs .q4{font-size:70px;line-height:.6;color:#B9CDEB;font-weight:600}
|
|
460
|
+
.rs .q4.big{font-size:44px;line-height:1;margin-bottom:10px}
|
|
461
|
+
.rs .quote{font-size:18px;line-height:1.5;margin-top:6px}
|
|
462
|
+
.rs .say{font-size:26px;font-weight:600;line-height:1.35;letter-spacing:-.01em;max-width:900px}
|
|
463
|
+
.rs .tiles{display:grid;gap:0;flex:1;align-items:stretch}
|
|
464
|
+
.rs .tile{display:flex;flex-direction:column;gap:6px;padding:0 22px;border-left:1px solid var(--hair2)}
|
|
465
|
+
.rs .tile:first-child{border-left:0;padding-left:0}
|
|
466
|
+
.rs .tile .lb2{font-size:13px;color:var(--ink2)}.rs .tile .big{font-size:26px;font-weight:600;letter-spacing:-.01em;line-height:1.2}.rs .tile .d{font-size:13px;color:var(--blueink)}
|
|
467
|
+
.rs .bar2{height:6px;border-radius:3px;background:#ECEAE6;position:relative;width:100%}
|
|
468
|
+
.rs .bar2 i{display:block;height:100%;border-radius:3px;background:#8C929B}.rs .bar2 i.y{background:var(--blue)}.rs .bar2 i.o{background:#5E9C6A}.rs .bar2 i.rk{background:#D4A13A}.rs .bar2 i.rd{background:#D0473E}
|
|
469
|
+
.rs .bar2 em{position:absolute;top:-3px;width:2px;height:12px;background:var(--ink)}
|
|
470
|
+
.rs .fl{flex:1;display:flex;flex-direction:column;min-height:0;min-width:0}
|
|
471
|
+
.rs .rows .lg,.rs .rows .ag,.rs .rows .mn,.rs .rows .lrow{flex:1}
|
|
472
|
+
.rs .grow{flex:1}
|
|
473
|
+
.rs .gap14{gap:14px}
|
|
474
|
+
.rs .col0{display:flex;flex-direction:column;flex:1;min-width:0}
|
|
475
|
+
.rs .lg{display:grid;grid-template-columns:210px 76px 1fr 130px;gap:18px;align-items:center;border-top:1px solid var(--hair)}
|
|
476
|
+
.rs .ag{display:flex;align-items:center;gap:22px;border-top:1px solid var(--hair)}
|
|
477
|
+
.rs .ag .num{color:#B9CDEB;width:44px}
|
|
478
|
+
.rs .mn{display:grid;grid-template-columns:260px 1fr 90px;gap:18px;align-items:center;border-top:1px solid var(--hair);padding:10px 0}
|
|
479
|
+
.rs .two-col{display:flex;gap:36px;flex:1}
|
|
480
|
+
.rs .side{flex:1;display:flex;flex-direction:column;min-width:0}
|
|
481
|
+
.rs .vr{width:1px;background:var(--hair2);flex:none}
|
|
482
|
+
.rs .cmp{flex:1;border-top:1px solid var(--hair);padding:12px 0;display:flex;flex-direction:column;justify-content:center}
|
|
483
|
+
.rs .cmp .bl{display:flex;align-items:baseline;gap:14px}.rs .cmp .nm{margin:2px 0 8px}
|
|
484
|
+
.rs .lrow{display:flex;align-items:center;gap:12px;padding:10px 0;border-top:1px solid var(--hair);font-size:15px}
|
|
485
|
+
.rs .lrow.big-row{padding:16px 0;font-size:18px}
|
|
486
|
+
.rs .lrow .ic{width:32px;height:32px;border-radius:50%;background:var(--well);display:inline-flex;align-items:center;justify-content:center;color:var(--ink2);flex:none;font-size:13px;font-weight:600}
|
|
487
|
+
.rs .lrow .tx{flex:1;min-width:0}.rs .lrow .t,.rs .t{font-weight:600}
|
|
488
|
+
.rs .first-flat .lrow:first-child{border-top:0}
|
|
489
|
+
.rs .sec{overflow:hidden}
|
|
490
|
+
.rs .th2{display:grid;gap:14px;padding:8px 12px;margin:0 -12px;font-size:13px;color:var(--ink3);background:var(--well);border-radius:10px}
|
|
491
|
+
.rs .kr{display:grid;gap:14px;padding:12px 0;border-top:1px solid var(--hair);align-items:center;font-size:15px}
|
|
492
|
+
.rs .th2 + .kr{border-top:0}
|
|
493
|
+
.rs .kr .n{font-weight:600;line-height:1.3}.rs .kr .n small{display:block;font-weight:400;color:var(--ink3);font-size:13px;margin-top:2px}
|
|
494
|
+
.rs .dr{display:flex;align-items:center;gap:10px;background:var(--bluebg);border-radius:12px;padding:10px 10px 10px 14px;font-size:15px;color:var(--ink2)}
|
|
495
|
+
.rs .dr.pad{padding:16px 14px 16px 18px}.rs .dr.gate{padding:22px}
|
|
496
|
+
.rs .dr .sp{color:var(--blueink);font-weight:600;flex:none}.rs .dr .x{margin-left:auto;display:flex;gap:8px;align-items:center;flex:none}.rs .dr .u{font-size:12px;color:var(--ink3)}
|
|
497
|
+
.rs .cell{border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:600}
|
|
498
|
+
.rs .cell.ok{background:var(--okbg);color:var(--ok)}.rs .cell.w{background:var(--riskbg);color:var(--risk)}.rs .cell.r{background:var(--redbg);color:var(--red)}.rs .cell.y{background:var(--bluebg);color:var(--blueink)}.rs .cell.q{background:var(--well);color:var(--ink3);font-weight:400}
|
|
499
|
+
.rs .heat{display:grid;gap:10px;flex:1}
|
|
500
|
+
.rs .net{position:relative;flex:1;min-height:470px}
|
|
501
|
+
.rs .net-svg{position:absolute;left:0;top:0;fill:none;stroke:#9FB8E0;stroke-width:2}
|
|
502
|
+
.rs .node{position:absolute;width:176px;height:56px;border-radius:14px;background:#fff;border:1px solid var(--hair2);display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:600;text-align:center;padding:0 8px;line-height:1.2}
|
|
503
|
+
.rs .node.r{background:var(--redbg);border-color:transparent;color:var(--red)}.rs .node.w{background:var(--riskbg);border-color:transparent;color:var(--risk)}
|
|
504
|
+
.rs .tg{display:flex;flex-direction:column;border-top:1px solid var(--hair)}
|
|
505
|
+
.rs .tg .tr{display:flex;min-height:46px;border-top:1px solid var(--hair);text-align:left}
|
|
506
|
+
.rs .tg .nm{width:200px;flex:none;padding:8px 14px 8px 0;display:flex;align-items:center;font-size:13px;font-weight:600}
|
|
507
|
+
.rs .tg .tk{flex:1;position:relative}
|
|
508
|
+
.rs .tg .tr.h{min-height:34px;flex:none;border-top:0;font-size:13px;color:var(--ink3);background:var(--well);border-radius:10px}
|
|
509
|
+
.rs .tg .tr.h .nm{font-weight:400;padding-left:12px}.rs .tg .tr.h .tk{display:grid}.rs .tg .tr.h .tk span{padding:8px 8px 0}
|
|
510
|
+
.rs .tb{position:absolute;top:calc(50% - 13px);height:26px;border-radius:9px;background:#E6E8EC;display:flex;align-items:center;gap:6px;padding:0 8px;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;color:var(--ink2)}
|
|
511
|
+
.rs .tb.r{background:var(--riskbg);color:var(--risk)}.rs .tb.k{background:#D9DCE1;color:var(--ink2)}.rs .tb.g{background:var(--okbg);color:var(--ok)}.rs .tb.l{background:var(--redbg);color:var(--red)}
|
|
512
|
+
.rs .tbl,.rs .tout{position:absolute;top:calc(50% - 10px);height:20px;display:flex;align-items:center;font-size:12px;font-weight:600;white-space:nowrap;color:var(--ink2)}
|
|
513
|
+
.rs .tout{padding:0 8px;border-radius:9px;background:#E6E8EC}.rs .tout.ob{left:0}.rs .tout.oa{right:0}
|
|
514
|
+
.rs .tout.r{background:var(--riskbg);color:var(--risk)}.rs .tout.k{background:#D9DCE1}.rs .tout.g{background:var(--okbg);color:var(--ok)}.rs .tout.l{background:var(--redbg);color:var(--red)}
|
|
515
|
+
.rs .today{position:absolute;top:0;bottom:0;width:2px;background:#4894EA}
|
|
516
|
+
.rs .cl{display:flex;align-items:center;gap:8px;min-height:30px;margin-bottom:6px}
|
|
517
|
+
.rs .seg{display:inline-flex;padding:3px;border-radius:999px;background:var(--well);gap:2px}
|
|
518
|
+
.rs .sb{height:30px;padding:0 12px;border-radius:999px;font-size:13px;font-weight:600;color:var(--ink3);display:inline-flex;align-items:center;cursor:pointer}
|
|
519
|
+
.rs .btn{height:32px;padding:0 12px;border-radius:999px;display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;border:1px solid var(--hair2);background:#fff;color:var(--ink);white-space:nowrap;cursor:pointer}
|
|
520
|
+
.rs input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
|
|
521
|
+
.rs .tl .tg{display:none}
|
|
522
|
+
.rs .i-q:checked ~ .tlw .w-q,.rs .i-h:checked ~ .tlw .w-h,.rs .i-y:checked ~ .tlw .w-y{display:flex;flex:1}
|
|
523
|
+
.rs .tlw{flex:1;display:flex;flex-direction:column;min-height:0}
|
|
524
|
+
.rs .i-q:checked ~ .cl .l-q,.rs .i-h:checked ~ .cl .l-h,.rs .i-y:checked ~ .cl .l-y{background:#fff;color:var(--ink);box-shadow:0 1px 3px rgba(42,46,53,.12)}
|
|
525
|
+
.rs .i-fa:checked ~ .cl .l-fa,.rs .i-r:checked ~ .cl .l-r{background:#fff;color:var(--ink);box-shadow:0 1px 3px rgba(42,46,53,.12)}
|
|
526
|
+
.rs .i-r:checked ~ .tlw .calm{display:none}
|
|
527
|
+
.rs .tl input:focus-visible ~ .cl{outline:2px solid var(--blue);outline-offset:2px;border-radius:999px}
|
|
528
|
+
.rs .act{cursor:pointer}
|
|
529
|
+
.rs .acts{counter-reset:left}
|
|
530
|
+
.rs .acts .i-a{counter-increment:left}
|
|
531
|
+
.rs .acts .i-a:checked{counter-increment:none}
|
|
532
|
+
.rs .chk{width:28px;height:28px;border-radius:9px;border:2px solid #5E9C6A;display:inline-flex;align-items:center;justify-content:center;flex:none;transition:background-color .2s}
|
|
533
|
+
.rs .i-a:checked + .chk{background:#3E6E49}
|
|
534
|
+
.rs .i-a:checked + .chk::after{content:"✓";color:#fff;font-weight:600}
|
|
535
|
+
.rs .i-a:checked ~ .tx{text-decoration:line-through;opacity:.55}
|
|
536
|
+
.rs .i-a:focus-visible + .chk{outline:2px solid var(--blue);outline-offset:2px}
|
|
537
|
+
.rs .avl{width:84px;height:84px;border-radius:50%;background:var(--bluebg);color:var(--blueink);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;flex:none}
|
|
538
|
+
.rs .avl.av2{width:36px;height:36px;font-size:13px}
|
|
539
|
+
.rs .due{width:110px;text-align:right}
|
|
540
|
+
.rs .a-left::before{content:counter(left) " left, the earliest is due first"}
|
|
541
|
+
.rs .a-done{display:none}
|
|
542
|
+
.rs .acts:not(:has(.i-a:not(:checked))) .a-left{display:none}
|
|
543
|
+
.rs .acts:not(:has(.i-a:not(:checked))) .a-done{display:inline}
|
|
544
|
+
.rs .donut-row{display:flex;gap:40px;flex:1;align-items:center}
|
|
545
|
+
.rs .ring{position:relative;width:300px;height:300px;flex:none;border-radius:50%}
|
|
546
|
+
.rs .hole{position:absolute;inset:56px;border-radius:50%;background:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center}
|
|
547
|
+
.rs .st{align-self:stretch}
|
|
548
|
+
.rs .plot-wrap{display:flex;flex-direction:column;flex:1;gap:6px}
|
|
549
|
+
.rs .plot{position:relative;flex:1;border-left:1px solid var(--hair2);border-bottom:1px solid var(--hair2);margin:8px 40px 4px 8px}
|
|
550
|
+
.rs .bub{position:absolute;border-radius:50%;opacity:.88;display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;font-weight:600;text-align:center;line-height:1.1;padding:4px}
|
|
551
|
+
.rs .steps{display:flex;gap:18px;flex:1;align-items:flex-start}
|
|
552
|
+
.rs .step{flex:1;border-top:3px solid #9FB8E0;padding-top:12px}
|
|
553
|
+
.rs .next-row{display:flex;gap:28px;flex:1}
|
|
554
|
+
.rs .when{width:260px;flex:none;border-right:1px solid var(--hair2);padding-right:28px}
|
|
555
|
+
.rs .ins{display:flex;flex-direction:column;gap:12px;border-left:1px solid var(--hair2);padding:0 0 0 24px;min-height:0}
|
|
556
|
+
.rs .ins .k{font-size:13px;font-weight:600;color:var(--blueink)}
|
|
557
|
+
.rs .ins p{color:var(--ink2)}
|
|
558
|
+
.rs .ins ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:10px}
|
|
559
|
+
.rs .ins li{display:flex;gap:10px;align-items:flex-start;color:var(--ink2)}
|
|
560
|
+
.rs .ins li .dot{margin-top:7px}
|
|
561
|
+
.rs .note{margin-top:auto;background:var(--well);border-radius:12px;padding:10px 12px;font-size:13px;color:var(--ink3)}
|
|
562
|
+
.rs .note b{color:var(--ink2)}
|
|
563
|
+
.rs .blk{display:flex;flex-direction:column;gap:12px}
|
|
564
|
+
.rs .bp{font-size:18px;line-height:1.5;color:var(--ink2);max-width:1000px}
|
|
565
|
+
.rs .bt{margin-top:2px}
|
|
566
|
+
@keyframes rs-grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}
|
|
567
|
+
@keyframes rs-rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
|
568
|
+
@keyframes rs-fade{from{opacity:0}to{opacity:1}}
|
|
569
|
+
.rs .bar2 i,.rs .tb{transform-origin:left center;animation:rs-grow .9s cubic-bezier(.2,.7,.2,1) .25s both}
|
|
570
|
+
.rs .bar2 em{animation:rs-fade .4s ease 1s both}
|
|
571
|
+
.rs .hd2,.rs .ft2{animation:rs-fade .5s ease both}
|
|
572
|
+
.rs .kr,.rs .lrow,.rs .tile,.rs .dr,.rs .strip,.rs .lg,.rs .ag,.rs .mn,.rs .ins li,.rs .tg .tr,.rs .col,.rs .step{animation:rs-rise .55s cubic-bezier(.2,.7,.2,1) both}
|
|
573
|
+
.rs :is(.kr,.lrow,.tile,.tr,.lg,.ag,.mn,.col,.step):nth-child(2){animation-delay:.08s}.rs :is(.kr,.lrow,.tile,.tr,.lg,.ag,.mn,.col,.step):nth-child(3){animation-delay:.16s}.rs :is(.kr,.lrow,.tile,.tr,.lg,.ag,.mn,.col,.step):nth-child(4){animation-delay:.24s}.rs :is(.kr,.lrow,.tile,.tr,.lg,.ag,.mn,.col,.step):nth-child(5){animation-delay:.32s}.rs :is(.kr,.lrow,.tile,.tr,.lg,.ag,.mn):nth-child(6){animation-delay:.4s}
|
|
574
|
+
.rs .kr,.rs .lrow,.rs .tb,.rs .btn,.rs .sb{transition:background-color .2s ease,box-shadow .2s ease}
|
|
575
|
+
.rs .kr:hover,.rs .lrow:hover{background:rgba(244,243,240,.7)}
|
|
576
|
+
.rs .tb:hover{box-shadow:0 2px 8px -2px rgba(42,46,53,.3)}
|
|
577
|
+
@media (prefers-reduced-motion:reduce){.rs *{animation:none!important;transition:none!important}}
|
|
578
|
+
@media print{.rs *{animation:none!important;transition:none!important}.rs .ctl{display:none}}`;
|
|
579
|
+
|
|
580
|
+
module.exports = { LAYOUTS, TONES, SPEC, CSS, layoutError, textsOfLayout, mapLayout, linesOf, slideHtml, deckHtml, blockHtml, longDate, dmy, onSlide, timelineView, timelinePlace, timelineToday, FILL, RING };
|