create-pmos 0.123.0 → 0.125.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 +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/nine-stages.js +81 -0
- package/payload/scripts/workflow-state.js +2 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-one-home-test.js +131 -0
- package/workspace/fixtures/initiatives-api/list.json +171 -3
- package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
- package/workspace/fixtures/initiatives-api/sheet.json +57 -1
- package/workspace/fixtures/one-home-baseline.json +83 -0
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/notes.md +1 -0
- package/workspace/lib/engine/nine-stages.js +81 -0
- package/workspace/lib/engine/workflow-state.js +2 -0
- package/workspace/lib/facts-async.js +51 -0
- package/workspace/lib/item-steps.js +52 -0
- package/workspace/lib/jira-status.js +8 -3
- package/workspace/lib/jobs-to-be-done.js +52 -0
- package/workspace/lib/outbox-store.js +87 -0
- package/workspace/lib/pm-items.js +115 -0
- package/workspace/lib/route-modules.js +21 -0
- package/workspace/lib/routes-briefing.js +100 -0
- package/workspace/lib/routes-c3-init.js +53 -0
- package/workspace/lib/routes-c3-life.js +119 -0
- package/workspace/lib/routes-c3-strategy.js +169 -0
- package/workspace/lib/routes-c3-timeline.js +191 -0
- package/workspace/lib/routes-calendar-import.js +60 -0
- package/workspace/lib/routes-calendar-views.js +61 -0
- package/workspace/lib/routes-doc-health.js +79 -0
- package/workspace/lib/routes-doc-sheet.js +169 -0
- package/workspace/lib/routes-forecast.js +85 -0
- package/workspace/lib/routes-hooks.js +121 -0
- package/workspace/lib/routes-initiatives.js +3 -0
- package/workspace/lib/routes-intake.js +24 -0
- package/workspace/lib/routes-outbox.js +97 -0
- package/workspace/lib/routes-pm-items.js +216 -0
- package/workspace/lib/routes-records.js +61 -0
- package/workspace/lib/routes-releases.js +129 -0
- package/workspace/lib/routes-report-facts.js +84 -0
- package/workspace/lib/routes-report-families.js +32 -0
- package/workspace/lib/routes-roadmap-ops.js +141 -0
- package/workspace/lib/routes-runs.js +119 -0
- package/workspace/lib/routes-signals.js +50 -0
- package/workspace/lib/run-answers.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +26 -0
- package/workspace/public/pm/briefing-deck.js +415 -0
- package/workspace/public/pm/briefing-model.js +176 -0
- package/workspace/public/pm/c3-breakdown.js +240 -0
- package/workspace/public/pm/c3-brief.js +278 -0
- package/workspace/public/pm/c3-cal-strategy.js +528 -0
- package/workspace/public/pm/c3-case.js +173 -0
- package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
- package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
- package/workspace/public/pm/c3-demo-cal2.js +66 -0
- package/workspace/public/pm/c3-demo-cal3.js +173 -0
- package/workspace/public/pm/c3-demo-data.js +560 -0
- package/workspace/public/pm/c3-demo-init.js +373 -0
- package/workspace/public/pm/c3-demo-life.js +77 -0
- package/workspace/public/pm/c3-demo-misc2.js +46 -0
- package/workspace/public/pm/c3-demo-plan2.js +30 -0
- package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
- package/workspace/public/pm/c3-demo-rep2.js +92 -0
- package/workspace/public/pm/c3-demo-term.js +153 -0
- package/workspace/public/pm/c3-demo-time.js +49 -0
- package/workspace/public/pm/c3-demo-work-ix.js +80 -0
- package/workspace/public/pm/c3-demo-work2.js +13 -0
- package/workspace/public/pm/c3-demo.js +113 -0
- package/workspace/public/pm/c3-doc-sheet.js +314 -0
- package/workspace/public/pm/c3-dock.js +180 -0
- package/workspace/public/pm/c3-docs.js +290 -0
- package/workspace/public/pm/c3-expand.js +96 -0
- package/workspace/public/pm/c3-init-extras.js +170 -0
- package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
- package/workspace/public/pm/c3-misc-panels.js +89 -0
- package/workspace/public/pm/c3-plan-panels.js +163 -0
- package/workspace/public/pm/c3-plan.js +192 -0
- package/workspace/public/pm/c3-roadmap.js +249 -0
- package/workspace/public/pm/c3-sheet.js +107 -0
- package/workspace/public/pm/c3-shell.js +637 -0
- package/workspace/public/pm/c3-signals.js +196 -0
- package/workspace/public/pm/c3-strategy-panels.js +45 -0
- package/workspace/public/pm/c3-swipe.js +138 -0
- package/workspace/public/pm/c3-terminal-data.js +26 -0
- package/workspace/public/pm/c3-terminal-edge.js +91 -0
- package/workspace/public/pm/c3-terminal-icons.js +41 -0
- package/workspace/public/pm/c3-terminal-results.js +141 -0
- package/workspace/public/pm/c3-terminal.js +52 -0
- package/workspace/public/pm/c3-tickets.js +163 -0
- package/workspace/public/pm/c3-timeline.js +201 -0
- package/workspace/public/pm/c3-toast.js +16 -0
- package/workspace/public/pm/calendar-views.js +1015 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1480 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- package/workspace/public/pm/dest-notes.js +25 -3
- package/workspace/public/pm/dest-reports.js +47 -1
- package/workspace/public/pm/dest-today.js +55 -1
- package/workspace/public/pm/doc-health-panel.js +110 -0
- package/workspace/public/pm/doc-rail.js +9 -3
- package/workspace/public/pm/lifecycle-lens-model.js +228 -0
- package/workspace/public/pm/lifecycle-lens.js +261 -0
- package/workspace/public/pm/new-initiative-sheet.js +116 -21
- package/workspace/public/pm/outbox-panel.js +125 -0
- package/workspace/public/pm/pm-work-view.js +919 -0
- package/workspace/public/pm/releases-view.js +248 -0
- package/workspace/public/pm/report-families-view.js +503 -0
- package/workspace/public/pm/run-card.js +244 -0
- package/workspace/public/pm/sessions-panel.js +149 -0
- package/workspace/public/pm/shell-bell.js +162 -0
- package/workspace/public/pm/shell-model.js +83 -0
- package/workspace/public/pm/shell-phone.js +124 -0
- package/workspace/public/pm/shell-readaloud.js +53 -0
- package/workspace/public/pm/shell-recording.js +110 -0
- package/workspace/public/pm/shell-ticker.js +90 -0
- package/workspace/public/pm/slide-confirm.js +163 -0
- package/workspace/public/pm/timeline-forecast-view.js +164 -0
- package/workspace/public/pm/today-day.js +8 -2
- package/workspace/public/pm/tokens.css +1 -0
- package/workspace/server.js +5 -2
- package/workspace/test-files.json +8 -0
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/* pm/briefing-deck.js — the Briefing view (console3 wave C1), built on the console3 mockup's own markup and CSS.
|
|
2
|
+
* The mockup (prototype/pmos-console-handoff/mockups/console3.html) is the UI: the stance bar with the day-part switch (.stance,
|
|
3
|
+
* .seg.dp), the running line (.tick, .tkt), the card deck (.deck, .fc, .cf, .hero, .dk, .dkd), the flip to the case file (.fb,
|
|
4
|
+
* .case, .chgl, .opts, .slt, .cft), the tilt and the flip animation. The styles are pm/console3.css (see pm/console3-lift.md).
|
|
5
|
+
* This file keeps the data call (GET /api/briefing, the signal engine's cards) and the model (briefing-model.js) and draws
|
|
6
|
+
* what the engine sent: at most five cards, none invented. A field the engine does not supply is left out.
|
|
7
|
+
*
|
|
8
|
+
* Read-only. The suggested move and the options are text; the slide-to-confirm on the case file is a disabled placeholder until
|
|
9
|
+
* the outbox is mounted here. Nothing in this view sends anything.
|
|
10
|
+
* Hero kinds drawn: line, big-number, ring, bars. Text only: two-bars, rows, mini-graph (the engine gives them no values to
|
|
11
|
+
* plot, so their facts are listed as text and the kind is named).
|
|
12
|
+
* Not registered with the registry (Today has no view registry): dest-today.js imports mountBriefing/disposeBriefing. */
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
import { LENSES, TAG_STYLE, PART_STYLE, cardView, dayPartFor, nextCards, swipeDir, sizeFor, caseModelOf, rawOption } from './briefing-model.js';
|
|
15
|
+
import { BX, caseFile, toast } from './c3-brief.js';
|
|
16
|
+
import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
|
|
17
|
+
import { listenButton } from './shell-readaloud.js';
|
|
18
|
+
|
|
19
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
20
|
+
const roots = new WeakMap(); // host -> state
|
|
21
|
+
const reduced = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
const small = 'font-size:var(--fs-2)';
|
|
23
|
+
|
|
24
|
+
function svg(tag, attrs, ...kids) {
|
|
25
|
+
const n = document.createElementNS(NS, tag);
|
|
26
|
+
for (const k of Object.keys(attrs || {})) n.setAttribute(k, attrs[k]);
|
|
27
|
+
for (const c of kids) if (c) n.appendChild(c);
|
|
28
|
+
return n;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ── the hero, one per data shape: the mockup's hNum, hRing, hBars and hRows (console3.html lines 3095 to 3101) ──── */
|
|
32
|
+
|
|
33
|
+
function heroLine(v, p) {
|
|
34
|
+
const W = 340; const H = 60; const lo = Math.min(...p); const hi = Math.max(...p); const r = hi - lo || 1;
|
|
35
|
+
const X = (i) => 4 + i * (W - 8) / (p.length - 1); const Y = (y) => H - 6 - ((y - lo) / r) * (H - 26);
|
|
36
|
+
const pts = p.map((y, i) => X(i).toFixed(1) + ',' + Y(y).toFixed(1)).join(' ');
|
|
37
|
+
const last = p[p.length - 1];
|
|
38
|
+
return el('div', { class: 'hn' },
|
|
39
|
+
el('div', { class: 'a' }, el('b', null, String(last))),
|
|
40
|
+
el('div', { class: 'd', style: 'color:var(--muted-foreground)' }, 'from ' + p[0] + ' over ' + p.length + ' points'),
|
|
41
|
+
svg('svg', { viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'trend of ' + p.length + ' points, from ' + p[0] + ' to ' + last },
|
|
42
|
+
svg('polygon', { points: `4,${H - 4} ${pts} ${W - 4},${H - 4}`, fill: 'var(--acc)', 'fill-opacity': '.1' }),
|
|
43
|
+
svg('polyline', { class: 'draw', pathLength: '1', points: pts, fill: 'none', stroke: 'var(--acc)', 'stroke-width': '3', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }),
|
|
44
|
+
svg('circle', { cx: X(p.length - 1), cy: Y(last), r: '4.5', fill: 'var(--acc)', stroke: 'var(--card)', 'stroke-width': '2' })));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
/* ── the mockup's richer heroes (hChain, hRing, hNum, hRows, hBars, hTiles, hSegs and the multi-line chart, console3.html lines 3095 to
|
|
49
|
+
* 3101 and 3366): drawn only when a card carries `hero.rich`, which the engine does not send (the /c3.html demo mode does). ──────────── */
|
|
50
|
+
function gateRing(r) {
|
|
51
|
+
const cc = 2 * Math.PI * 38; const col = (r && r.tone) || 'var(--acc)'; const L = (r && r.lines) || [];
|
|
52
|
+
return svg('svg', { width: 96, height: 96, viewBox: '0 0 96 96', role: 'img', 'aria-label': L.join(' ') },
|
|
53
|
+
svg('circle', { cx: 48, cy: 48, r: 38, fill: 'none', stroke: 'var(--muted)', 'stroke-width': 7 }),
|
|
54
|
+
svg('circle', { class: 'rg', cx: 48, cy: 48, r: 38, fill: 'none', stroke: col, 'stroke-width': 7, 'stroke-linecap': 'round', 'stroke-dasharray': (cc * (r.frac || 0)) + ' ' + cc, transform: 'rotate(-90 48 48)' }),
|
|
55
|
+
txt(48, 50, L[0] || '', { 'text-anchor': 'middle', class: 't', style: 'font-size:var(--fs-3);font-weight:600' }), txt(48, 65, L[1] || '', { 'text-anchor': 'middle', style: 'font-size:var(--fs-1)' }));
|
|
56
|
+
}
|
|
57
|
+
const txt = (x, y, s, a) => { const t = svg('text', { x, y, ...(a || {}) }); t.textContent = s; return t; };
|
|
58
|
+
const rows = (a) => el('div', { class: 'hrw' }, ...a.map((r) => el('div', { class: 'hrr ' + (r[2] || '') }, c3ic(r[0], 'sm'), el('span', null, r[1]), r[3] ? el('i', null, r[3]) : null)));
|
|
59
|
+
|
|
60
|
+
function richHero(r) {
|
|
61
|
+
if (r.kind === 'chain') {
|
|
62
|
+
return el('div', { class: 'hg' }, gateRing(r.ring), el('div', { class: 'hch' }, ...r.steps.map((n) => el('div', { style: '--c:' + n[2] }, el('b', null, n[0]), el('span', null, n[1])))));
|
|
63
|
+
}
|
|
64
|
+
if (r.kind === 'ring') {
|
|
65
|
+
const body = r.lines
|
|
66
|
+
? [el('b', null, r.cap), ...r.lines.map((x) => el('span', { style: 'display:flex;gap:6px;align-items:center;white-space:nowrap;' + (x[2] ? '' : 'color:var(--warning)') }, c3ic(x[2] ? 'check' : x[0], 'sm'), x[1]))]
|
|
67
|
+
: [el('b', null, r.cap), r.sub ? el('span', null, r.sub) : null];
|
|
68
|
+
return el('div', { class: 'hr' }, c3ring(r.done, r.total, r.size || 96), el('div', { class: 'c' }, ...body));
|
|
69
|
+
}
|
|
70
|
+
if (r.kind === 'rows') return rows(r.rows);
|
|
71
|
+
if (r.kind === 'bars') {
|
|
72
|
+
return el('div', { class: 'hbs' }, ...r.bars.map((b, i) => el('div', { class: 'hbr', style: '--c:' + b[2] }, el('div', { class: 'l' }, el('span', null, b[0]), el('i', null, b[3])),
|
|
73
|
+
el('div', { class: 'tk' }, el('u', { style: 'width:' + Math.max(2, b[1] * 100) + '%;animation-delay:' + (i * 90) + 'ms' })))));
|
|
74
|
+
}
|
|
75
|
+
if (r.kind === 'tiles') return el('div', { class: 'htl', style: '--n:' + r.tiles.length }, ...r.tiles.map((t) => el('div', { style: '--c:' + t[2] }, el('b', null, t[0]), el('span', null, t[1]))));
|
|
76
|
+
if (r.kind === 'num') {
|
|
77
|
+
const a = r.arr; const W = 340; const H = 60; const lo = Math.min(...a, r.target); const hi = Math.max(...a, r.target); const q = hi - lo || 1;
|
|
78
|
+
const X = (i) => 4 + i * (W - 8) / (a.length - 1); const Y = (v) => H - 6 - ((v - lo) / q) * (H - 26);
|
|
79
|
+
const pts = a.map((v, i) => X(i).toFixed(1) + ',' + Y(v).toFixed(1)).join(' ');
|
|
80
|
+
return el('div', { class: 'hn' }, el('div', { class: 'a' }, el('b', null, r.big), el('span', null, r.unit)), el('div', { class: 'd', style: 'color:' + r.tone }, r.delta),
|
|
81
|
+
svg('svg', { viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Trend against target' },
|
|
82
|
+
svg('line', { x1: 4, x2: W - 4, y1: Y(r.target), y2: Y(r.target), stroke: 'var(--ring)', 'stroke-dasharray': '4 4' }), txt(W - 4, Y(r.target) - 4, 'target ' + r.target + r.unit, { 'text-anchor': 'end' }),
|
|
83
|
+
svg('polygon', { points: `4,${H - 4} ${pts} ${W - 4},${H - 4}`, fill: r.tone, 'fill-opacity': '.1' }),
|
|
84
|
+
svg('polyline', { class: 'draw', pathLength: '1', points: pts, fill: 'none', stroke: r.tone, 'stroke-width': 3, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }),
|
|
85
|
+
svg('circle', { cx: X(a.length - 1), cy: Y(a[a.length - 1]), r: 4.5, fill: r.tone, stroke: 'var(--card)', 'stroke-width': 2 })));
|
|
86
|
+
}
|
|
87
|
+
if (r.kind === 'segs') {
|
|
88
|
+
const W = 340; const H = 84; const tot = r.segs.reduce((t, x) => t + x[1], 0) || 1; let x0 = 0;
|
|
89
|
+
const s = svg('svg', { viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Effort in minutes per item' });
|
|
90
|
+
r.segs.forEach((g, i) => {
|
|
91
|
+
const w = (W - (r.segs.length - 1) * 4) * (g[1] / tot); const lab = g[0].length > Math.max(5, Math.floor(w / 6.5)) ? g[0].slice(0, Math.max(4, Math.floor(w / 6.5) - 1)) + '…' : g[0];
|
|
92
|
+
s.append(svg('rect', { class: 'grow', style: 'animation-delay:' + (i * 90) + 'ms', x: x0, y: 20, width: w, height: 30, rx: 9, fill: g[2], opacity: '.92' }), txt(x0 + w / 2, 39, g[1] + ' min', { 'text-anchor': 'middle', style: 'fill:#fff;font-weight:500' }), txt(x0 + w / 2, 68, lab, { 'text-anchor': 'middle' }));
|
|
93
|
+
x0 += w + 4;
|
|
94
|
+
});
|
|
95
|
+
s.append(txt(0, 12, 'total ' + r.total, {}));
|
|
96
|
+
return s;
|
|
97
|
+
}
|
|
98
|
+
if (r.kind === 'axis') { // the mockup's "Today with the due times" axis (08:00 to 18:00): a focus window, due-time diamonds and now
|
|
99
|
+
const X = (h) => 8 + (h - 8) / 10 * 324; const s = svg('svg', { viewBox: '0 0 340 92', role: 'img', 'aria-label': 'Today with the due times of your items' });
|
|
100
|
+
s.append(svg('rect', { x: X(r.focus[0]).toFixed(1), y: 30, width: (X(r.focus[1]) - X(r.focus[0])).toFixed(1), height: 30, rx: 8, fill: 'var(--purple)', 'fill-opacity': '.12', stroke: 'var(--purple)', 'stroke-dasharray': '4 3' }), txt(((X(r.focus[0]) + X(r.focus[1])) / 2).toFixed(1), 49, 'focus', { 'text-anchor': 'middle', style: 'fill:var(--purple)' }),
|
|
101
|
+
svg('line', { x1: 8, x2: 332, y1: 45, y2: 45, stroke: 'var(--border)', 'stroke-width': 2 }));
|
|
102
|
+
[8, 10, 12, 14, 16, 18].forEach((h) => s.append(txt(X(h), 84, String(h).padStart(2, '0') + ':00', { 'text-anchor': h === 8 ? 'start' : h === 18 ? 'end' : 'middle' }), svg('line', { x1: X(h), x2: X(h), y1: 62, y2: 68, stroke: 'var(--ring)' })));
|
|
103
|
+
r.marks.forEach((m) => { const x = X(m[0]); const g = svg('g', {}, svg('path', { d: `M${x} 36 l8 9 -8 9 -8 -9z`, fill: m[2], stroke: 'var(--card)', 'stroke-width': 2 }), txt(x, m[3] || 19, m[1], { 'text-anchor': 'middle', class: 't' })); s.append(g); });
|
|
104
|
+
s.append(svg('line', { x1: X(r.now), x2: X(r.now), y1: 30, y2: 62, stroke: 'var(--foreground)', 'stroke-width': 2 }));
|
|
105
|
+
return s;
|
|
106
|
+
}
|
|
107
|
+
if (r.kind === 'days') { // the mockup's "items per day over the next seven working days": a tile per day, a dot where an item lands
|
|
108
|
+
const D = r.days; const w = 40.57; const s = svg('svg', { viewBox: '0 0 340 92', role: 'img', 'aria-label': 'Items per day over the next seven working days' });
|
|
109
|
+
D.forEach((d, i) => { const x = 4 + i * 48.57; s.append(svg('rect', { x, y: 8, width: w, height: 62, rx: 10, fill: i === 0 ? 'var(--accw)' : 'var(--muted)' }), txt(x + w / 2, 84, d[0], { 'text-anchor': 'middle', class: i === 0 ? 't' : '' }),
|
|
110
|
+
d[1] ? svg('circle', { cx: x + w / 2, cy: 52, r: 6, fill: 'var(--acc)' }) : txt(x + w / 2, 44, '·', { 'text-anchor': 'middle', style: 'fill:var(--ring)' })); });
|
|
111
|
+
return s;
|
|
112
|
+
}
|
|
113
|
+
if (r.kind === 'multi') {
|
|
114
|
+
const W = 340; const H = 96; const all = r.lines.flatMap((l) => l.pts); const lo = Math.min(...all); const hi = Math.max(...all); const N = Math.max(...r.lines.map((l) => (l.from || 0) + l.pts.length)) - 1;
|
|
115
|
+
const X = (i) => 6 + i / N * (W - 12); const Y = (v) => H - 14 - (v - lo) / (hi - lo || 1) * (H - 26);
|
|
116
|
+
const s = svg('svg', { viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'North star against plan' });
|
|
117
|
+
r.lines.forEach((l) => s.append(svg('polyline', { points: l.pts.map((v, i) => X((l.from || 0) + i).toFixed(1) + ',' + Y(v).toFixed(1)).join(' '), fill: 'none', stroke: l.color, 'stroke-width': l.width, 'stroke-dasharray': l.dash || null, 'stroke-linecap': 'round' })));
|
|
118
|
+
const act = r.lines[1]; s.append(svg('circle', { cx: X(act.pts.length - 1), cy: Y(act.pts[act.pts.length - 1]), r: 5, fill: 'var(--acc)', stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
119
|
+
s.append(txt(X(act.pts.length - 1) - 6, H - 2, 'now', { 'text-anchor': 'end' }), txt(W - 6, Y(r.lines[0].pts[r.lines[0].pts.length - 1]) - 6, 'plan', { 'text-anchor': 'end' }),
|
|
120
|
+
txt(W - 6, Y(r.lines[2].pts[r.lines[2].pts.length - 1]) + 14, 'at today\'s pace', { 'text-anchor': 'end', style: 'fill:var(--warning)' }));
|
|
121
|
+
return s;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hero(v) {
|
|
127
|
+
if (v.rich) { const h = richHero(v.rich); if (h) return h; }
|
|
128
|
+
const k = v.hero.kind; const t = v.terms || {};
|
|
129
|
+
if (k === 'line' && v.hero.points && v.hero.points.length >= 3) return heroLine(v, v.hero.points);
|
|
130
|
+
if (k === 'ring' && t.impact != null) {
|
|
131
|
+
const f = Math.max(0, Math.min(1, t.impact));
|
|
132
|
+
return el('div', { class: 'hr' }, c3ring(Math.round(f * 100), 100, 96, Math.round(f * 100) + '%'), el('div', { class: 'c' }, el('b', null, 'impact ' + Math.round(f * 100) + '%')));
|
|
133
|
+
}
|
|
134
|
+
if (k === 'bars' && (t.impact != null || t.urgency != null)) {
|
|
135
|
+
const row = (n, x, c, i) => (x == null ? null : el('div', { class: 'hbr', style: '--c:' + c },
|
|
136
|
+
el('div', { class: 'l' }, el('span', null, n), el('i', null, x.toFixed(2))),
|
|
137
|
+
el('div', { class: 'tk' }, el('u', { style: 'width:' + Math.max(2, Math.round(Math.max(0, Math.min(1, x)) * 100)) + '%;animation-delay:' + (i * 90) + 'ms' }))));
|
|
138
|
+
return el('div', { class: 'hbs', 'aria-label': 'score terms' }, row('impact', t.impact, 'var(--acc)', 0), row('urgency', t.urgency, 'var(--warning)', 1), row('irreversible', t.irreversible, 'var(--destructive)', 2));
|
|
139
|
+
}
|
|
140
|
+
if (k === 'big-number') {
|
|
141
|
+
const n = v.headline.match(/\d+(\.\d+)?/);
|
|
142
|
+
const big = n ? n[0] : (v.deadline && v.deadline.days != null ? String(Math.round(v.deadline.days)) : null);
|
|
143
|
+
if (big) return el('div', { class: 'hn' }, el('div', { class: 'a' }, el('b', null, big), v.deadline && !n ? el('span', null, 'days') : null));
|
|
144
|
+
}
|
|
145
|
+
// two-bars, rows, mini-graph (and any kind without values): the facts as text rows, named so the gap is visible
|
|
146
|
+
return el('div', { class: 'hrw', 'data-hero-text': k }, ...(v.facts.length ? v.facts : [v.headline]).map((f) => el('div', { class: 'hrr' }, c3ic('info', 'sm'), el('span', null, f))));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── one card: the mockup's cardHtml (line 2324): .fc > .ff (.tg + .cf) and .fb (.fh + .case) ──────────────────────── */
|
|
150
|
+
|
|
151
|
+
const tagStyle = (v) => TAG_STYLE[v.tag] || TAG_STYLE['Just in'];
|
|
152
|
+
const pct = (x) => Math.round(Math.max(0, Math.min(1, x)) * 100);
|
|
153
|
+
|
|
154
|
+
function eyebrow(v) {
|
|
155
|
+
const T = tagStyle(v);
|
|
156
|
+
if (v.eyebrow) return [el('span', { class: 'tg', style: '--tc:var(--muted-foreground)' }, c3ic(v.eyebrow.icon, 'sm'), v.eyebrow.label), v.pov ? el('span', { class: 'tag pov' }, v.pov) : null];
|
|
157
|
+
const tg = el('span', { class: 'tg' + (T.filled ? ' f' : ''), title: T.q }, c3ic(T.icon, 'sm'), v.tag);
|
|
158
|
+
return v.pov ? [tg, el('span', { class: 'tag pov' }, v.pov)] : tg;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* the mockup's .kp tiles (line 765): numbers the card carries, only the ones it carries */
|
|
162
|
+
function kpis(v) {
|
|
163
|
+
const t = v.terms || {};
|
|
164
|
+
const a = [];
|
|
165
|
+
if (t.impact != null) a.push([pct(t.impact) + '%', 'impact', 'var(--acc)']);
|
|
166
|
+
if (t.urgency != null) a.push([pct(t.urgency) + '%', 'urgency', 'var(--warning)']);
|
|
167
|
+
if (t.score != null) a.push([String(Math.round(t.score * 100) / 100), 'score', 'var(--success)']);
|
|
168
|
+
return a.slice(0, 3).map((k) => el('div', { class: 'kp', style: '--c:' + k[2] }, el('b', null, k[0]), el('span', null, k[1])));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function front(v) {
|
|
172
|
+
const rec = v.move ? el('span', { class: 'cfr' }, c3ic('spark', 'sm'), 'Suggested: ' + (v.move.length > 46 ? v.move.slice(0, 45) + '…' : v.move) + (v.conf != null ? ' · ' + Math.round(v.conf) + '%' : ''))
|
|
173
|
+
: el('span', { class: 'cfr n' }, 'Tap to see the story ', c3ic('next', 'sm'));
|
|
174
|
+
/* a card with factsX (the demo's mockup cards) draws its own icon per fact and no deadline line; an engine card is drawn as before */
|
|
175
|
+
const facts = v.factsX ? v.factsX.map((f) => el('span', null, c3ic(f.icon, 'sm'), f.text)) : v.facts.map((f) => el('span', null, c3ic('info', 'sm'), f));
|
|
176
|
+
if (v.deadline && !v.factsX) facts.push(el('span', null, c3ic('clock', 'sm'), 'deadline: ' + (v.deadline.days == null ? 'unknown' : v.deadline.text)));
|
|
177
|
+
return el('div', { class: 'ff' },
|
|
178
|
+
el('div', null, eyebrow(v)),
|
|
179
|
+
el('div', { class: 'cf' },
|
|
180
|
+
el('div', { class: 'cfa' }, el('h2', null, v.headline), v.sub ? el('div', { class: 'cfs' }, v.sub) : v.why ? el('div', { class: 'cfs', 'data-bf-why': '' }, 'why shown: ' + v.why) : null),
|
|
181
|
+
el('div', { class: 'cfh' }, el('div', { class: 'hero', 'data-hero': v.hero.kind }, hero(v))),
|
|
182
|
+
el('div', { class: 'cfk' }, ...(v.kpisX ? v.kpisX.map((k) => el('div', { class: 'kp', style: '--c:' + (k.c || 'var(--acc)') }, el('b', null, k.v), el('span', null, k.l))) : kpis(v))),
|
|
183
|
+
el('div', { class: 'cff' }, el('div', { class: 'cfx' }, ...facts), rec)));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* the case file: .fh, then .case (c3-case.js: .sit, .chgl evidence, .opts with effects and a confidence figure, .slt, .cft). The slide is live only where
|
|
187
|
+
* something can carry the option out: in the demo (BX.sim) it records the choice in memory and toasts with an Undo; in real mode an engine card has no
|
|
188
|
+
* route and no channel, so its case is `outward` and the slide is drawn disabled with the reason (c3-brief.js, briefing-model.js caseModelOf). */
|
|
189
|
+
function back(S, v) {
|
|
190
|
+
const T = tagStyle(v); const raw = caseModelOf(v, { sim: BX.sim }); const key = v.key || '';
|
|
191
|
+
let cardEl = null; let box = null;
|
|
192
|
+
const doneChips = (c) => {
|
|
193
|
+
const o = (raw.opts || raw.options || []).find((q) => String(q.id) === String(BX.cdone[key]));
|
|
194
|
+
c.querySelector('.opts') && c.querySelector('.opts').remove(); c.querySelector('.c3slide') && c.querySelector('.c3slide').remove();
|
|
195
|
+
const chips = el('div', { class: 'chips2', 'data-bf': 'done' }, el('span', { class: 'st ok' }, c3ic('check', 'sm'), o ? (o.l || o.label) : 'Done'),
|
|
196
|
+
el('button', { type: 'button', class: 'chip2 lk', 'data-bf': 'undo', onclick: (e) => { e.stopPropagation(); undo(); } }, c3ic('undo', 'sm'), 'undo'));
|
|
197
|
+
const ft = c.querySelector('.cft'); if (ft) ft.before(chips); else c.append(chips);
|
|
198
|
+
};
|
|
199
|
+
const build = () => {
|
|
200
|
+
const done = BX.cdone[key];
|
|
201
|
+
let c;
|
|
202
|
+
if (done && raw.doneModel) c = caseFile(raw.doneModel(done));
|
|
203
|
+
else {
|
|
204
|
+
c = caseFile(raw, { picked: BX.pick['c' + key], onPick: (id) => { BX.pick['c' + key] = id; }, onConfirm: choose });
|
|
205
|
+
if (done) doneChips(c);
|
|
206
|
+
}
|
|
207
|
+
c.setAttribute('data-bf-case', '');
|
|
208
|
+
return c;
|
|
209
|
+
};
|
|
210
|
+
const refresh = () => { const old = box.querySelector('.case'); const c = build(); if (old) { if (old.c3case) old.c3case.dispose(); old.replaceWith(c); } else box.append(c); };
|
|
211
|
+
const mark = () => { if (!cardEl) return; const on = !!BX.cdone[key]; cardEl.classList.toggle('ok', on); const r = cardEl.querySelector(':scope > .rsv'); if (on && !r) cardEl.querySelector('.flipg').before(el('span', { class: 'rsv' }, c3ic('check', 'sm'), 'done')); else if (!on && r) r.remove(); };
|
|
212
|
+
function undo() { delete BX.cdone[key]; mark(); refresh(); }
|
|
213
|
+
function choose({ option }) {
|
|
214
|
+
const ro = rawOption(raw, option.id) || {};
|
|
215
|
+
BX.cdone[key] = option.id; mark(); refresh();
|
|
216
|
+
const decide = String(ro.do || '').startsWith('decide:');
|
|
217
|
+
toast(ro.msg || (decide ? 'Decision logged. Follow-up drafts are in the briefing. Nothing was sent.' : 'Done: ' + option.label), undo);
|
|
218
|
+
if (decide && cardEl) flipTo(S, cardEl, false);
|
|
219
|
+
}
|
|
220
|
+
box = el('div', { class: 'fb' },
|
|
221
|
+
el('div', { class: 'fh', 'data-bf': 'back', 'data-a': 'cflip', title: 'Flip back (Esc)' }, eyebrow(v), el('span', { class: 'why2' }, T.q.split(': ')[1], c3ic('undo', 'sm'))), build());
|
|
222
|
+
box.bind = (c) => { cardEl = c; mark(); };
|
|
223
|
+
return box;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function card(S, v, i, n) {
|
|
227
|
+
const T = tagStyle(v); const bk = back(S, v);
|
|
228
|
+
const c = el('article', {
|
|
229
|
+
class: 'fc' + (i === 0 ? ' on' : ''), tabindex: '0', role: 'button', 'aria-expanded': 'false', 'aria-description': 'card ' + (i + 1) + ' of ' + n,
|
|
230
|
+
'data-a': 'cflip', 'data-bf': 'flip', 'data-bf-card': v.key || '', 'data-tag': T.key, 'data-tag-label': v.tag, style: '--tc:' + (v.eyebrow ? 'var(--muted-foreground)' : T.color),
|
|
231
|
+
}, front(v), el('span', { class: 'flipg', 'aria-hidden': 'true' }, c3ic('undo', 'sm')), bk);
|
|
232
|
+
bk.bind(c);
|
|
233
|
+
/* named by its own headline (aria-labelledby) rather than a long aria-label: the card's name is its text, and the position is its description */
|
|
234
|
+
const hd = c.querySelector('h2'); if (hd) { hd.id = 'fc-h-' + String(v.key || i).replace(/\W+/g, '-') + '-' + i; c.setAttribute('aria-labelledby', hd.id); } else c.setAttribute('aria-label', v.tag + ': ' + v.headline);
|
|
235
|
+
return c;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ── flip, tilt, deck sync: the mockup's flipTo (line 2403), tilt (line 2414) and deckSync (line 2163) ───────────────── */
|
|
239
|
+
|
|
240
|
+
function flipTo(S, c, force) {
|
|
241
|
+
if (!c) return;
|
|
242
|
+
const deck = c.parentNode;
|
|
243
|
+
if (!c.classList.contains('on') && !c.classList.contains('back')) { deck.scrollTo({ left: c.offsetLeft - deck.children[0].offsetLeft, behavior: reduced() ? 'auto' : 'smooth' }); return; }
|
|
244
|
+
const toBack = force !== undefined ? force : !c.classList.contains('back');
|
|
245
|
+
deck.querySelectorAll('.fc.back').forEach((o) => { if (o !== c) { o.classList.remove('back'); o.setAttribute('aria-expanded', 'false'); } });
|
|
246
|
+
const sw = () => { c.classList.toggle('back', toBack); c.setAttribute('aria-expanded', String(toBack)); if (toBack && c.scrollIntoView) c.scrollIntoView({ block: 'nearest', behavior: reduced() ? 'auto' : 'smooth' }); };
|
|
247
|
+
S.flip = toBack ? c.dataset.bfCard || '1' : null;
|
|
248
|
+
if (reduced()) { sw(); return; }
|
|
249
|
+
c.classList.add('fl-out');
|
|
250
|
+
setTimeout(() => { c.classList.remove('fl-out'); sw(); c.classList.add('fl-in'); setTimeout(() => c.classList.remove('fl-in'), 320); }, 170);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function deckSync(S, deck) {
|
|
254
|
+
const f = deck.children[0]; if (!f) return;
|
|
255
|
+
const w = f.offsetWidth + 12;
|
|
256
|
+
const i = Math.max(0, Math.min(deck.children.length - 1, Math.round(deck.scrollLeft / w)));
|
|
257
|
+
S.i = i;
|
|
258
|
+
[].forEach.call(deck.children, (c, j) => c.classList.toggle('on', j === i));
|
|
259
|
+
S.root.querySelectorAll('.dkd').forEach((x, j) => x.setAttribute('aria-current', String(j === i)));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function scrollToCard(S, deck, i) {
|
|
263
|
+
const c = deck.children[i]; if (!c) return;
|
|
264
|
+
deck.scrollTo({ left: c.offsetLeft - deck.children[0].offsetLeft, behavior: reduced() ? 'auto' : 'smooth' });
|
|
265
|
+
c.focus({ preventScroll: true });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function wireDeck(S, deck, cards) {
|
|
269
|
+
deck.addEventListener('scroll', () => deckSync(S, deck));
|
|
270
|
+
let y0 = null;
|
|
271
|
+
deck.addEventListener('pointerdown', (e) => { y0 = e.target.closest('button,.fb .case') ? null : e.clientY; });
|
|
272
|
+
deck.addEventListener('pointerup', (e) => {
|
|
273
|
+
if (y0 === null) return; const dir = swipeDir(e.clientY - y0); y0 = null;
|
|
274
|
+
const c = deck.children[S.i]; if (dir && c) flipTo(S, c, dir > 0); // up opens the case file, down closes it (the mockup's help text)
|
|
275
|
+
});
|
|
276
|
+
deck.addEventListener('pointercancel', () => { y0 = null; });
|
|
277
|
+
deck.addEventListener('click', (e) => {
|
|
278
|
+
const c = e.target.closest('.fc'); if (!c || e.target.closest('button')) return;
|
|
279
|
+
if (c.classList.contains('back') && !e.target.closest('.fh')) return;
|
|
280
|
+
flipTo(S, c);
|
|
281
|
+
});
|
|
282
|
+
deck.addEventListener('keydown', (e) => {
|
|
283
|
+
const c = e.target.closest && e.target.closest('.fc');
|
|
284
|
+
if (e.key === 'Escape' && S.flip) { const b = deck.querySelector('.fc.back'); if (b) { e.stopPropagation(); flipTo(S, b, false); } return; }
|
|
285
|
+
if (!c) return;
|
|
286
|
+
if (e.key === 'Enter' || e.key === ' ') { if (e.target === c) { e.preventDefault(); flipTo(S, c); } return; }
|
|
287
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { e.preventDefault(); scrollToCard(S, deck, nextCards(cards, S.i, e.key === 'ArrowRight' ? 1 : -1)); }
|
|
288
|
+
});
|
|
289
|
+
// the mockup's tilt: the card on top leans toward the pointer
|
|
290
|
+
let tilt = null;
|
|
291
|
+
deck.addEventListener('mousemove', (e) => {
|
|
292
|
+
if (reduced()) return;
|
|
293
|
+
const c = e.target.closest && e.target.closest('.fc.on:not(.back)');
|
|
294
|
+
if (tilt && tilt !== c) { ['--rx', '--ry', '--mx', '--my'].forEach((p) => tilt.style.removeProperty(p)); tilt = null; }
|
|
295
|
+
if (!c) return;
|
|
296
|
+
const r = c.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width; const yy = (e.clientY - r.top) / r.height;
|
|
297
|
+
c.style.setProperty('--ry', ((x - 0.5) * 5).toFixed(2) + 'deg'); c.style.setProperty('--rx', ((0.5 - yy) * 4).toFixed(2) + 'deg');
|
|
298
|
+
c.style.setProperty('--mx', (x * 100).toFixed(0) + '%'); c.style.setProperty('--my', (yy * 100).toFixed(0) + '%'); tilt = c;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ── state, load, draw ── */
|
|
303
|
+
/* One read per lens and zoom for the whole page: a page that mounts the deck on several tabs asks the server (a cold read of the
|
|
304
|
+
* repository takes minutes) once, and every deck draws the same answer. A failed read is dropped so the next mount asks again. */
|
|
305
|
+
const reads = new Map();
|
|
306
|
+
function readLens(lens, zoom, scope) {
|
|
307
|
+
const k = lens + '|' + zoom + (scope ? '|' + scope : '');
|
|
308
|
+
if (!reads.has(k)) {
|
|
309
|
+
const p = fetch('/api/briefing?lens=' + encodeURIComponent(lens) + '&zoom=' + zoom + (scope ? '&scope=' + encodeURIComponent(scope) : '')).then((r) => r.json())
|
|
310
|
+
.then((j) => (j && j.ok ? j : { ok: false, error: (j && j.error) || 'not available' }), (e) => ({ ok: false, error: String(e && e.message || e) }));
|
|
311
|
+
reads.set(k, p);
|
|
312
|
+
p.then((j) => { if (!j.ok) reads.delete(k); else setTimeout(() => reads.delete(k), 60000); });
|
|
313
|
+
}
|
|
314
|
+
return reads.get(k);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function load(S, lens) {
|
|
318
|
+
if (S.data[lens] !== undefined) return;
|
|
319
|
+
S.data[lens] = null;
|
|
320
|
+
S.data[lens] = await readLens(lens, S.zoom, S.ctx && S.ctx.scope);
|
|
321
|
+
if (S.dead) return; // a deck built before its host is attached still draws: it only touches its own root
|
|
322
|
+
if (lens === S.lens) draw(S);
|
|
323
|
+
else { const old = S.root.querySelector('.stance'); if (old) old.replaceWith(stance(S)); } // another lens arrived: update the switch, keep the open card
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const DAYPART = [['m', 'Morning', '08:12'], ['n', 'Midday', '12:30'], ['a', 'Afternoon', '16:05'], ['e', 'Evening', '18:30']]; // LENSES order: Plan, Unblock, Close, Reflect
|
|
327
|
+
/* the mockup's stanceBar (line 2332): the lens icon, its name and line, and the day-part switch as .seg.dp icon buttons */
|
|
328
|
+
function stance(S) {
|
|
329
|
+
const P = PART_STYLE[S.lens] || PART_STYLE.Plan;
|
|
330
|
+
return el('div', { class: 'stance', 'data-bf': 'lens-bar' },
|
|
331
|
+
el('span', { class: 'ki' }, c3ic(P.icon)),
|
|
332
|
+
el('div', null, el('b', { title: S.lens === S.clock ? 'Follows the clock: this is the part of the day it is now.' : null }, S.lens), el('span', { class: 'dim' }, P.sub)),
|
|
333
|
+
listenButton(() => { const x = S.data[S.lens]; return x && x.ok ? x.cards[S.i] || x.cards[0] : null; }), // the mockup's speaker (data-a=listen) reads the card on top
|
|
334
|
+
el('div', { class: 'seg dp', role: 'group', 'aria-label': 'Part of the day', title: 'Follows the clock. Tap to preview another part of the day.' },
|
|
335
|
+
...LENSES.map((l, qi) => {
|
|
336
|
+
const Q = PART_STYLE[l]; const N = DAYPART[qi]; // the mockup's PARTS (line 2206): the part is named by its time of day, the lens by what it does
|
|
337
|
+
return el('button', { type: 'button', 'aria-pressed': String(l === S.lens), 'data-bf-lens': l, 'data-a': 'tod', 'data-k': N[0], title: N[1] + ' · ' + N[2], 'aria-label': N[1],
|
|
338
|
+
onClick: () => { S.lens = l; S.i = 0; S.flip = null; load(S, l); draw(S); if (S.ctx && S.ctx.onLens) S.ctx.onLens(l); } }, c3ic(Q.icon, 'sm'));
|
|
339
|
+
})));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* the mockup's running line (.tick > .tkt, lines 539 to 545): the engine's ticker strings, twice so the marquee loops */
|
|
343
|
+
function ticker(items) {
|
|
344
|
+
const one = (hide) => items.map((t) => el('span', { class: 'tki', 'aria-hidden': hide ? 'true' : null }, el('b', { style: 'color:var(--acc)' }, c3ic('bolt', 'sm')), String(t)));
|
|
345
|
+
return el('div', { class: 'tick', role: 'status', 'data-bf': 'ticker', title: items.join(' | ') }, el('div', { class: 'tkt' }, ...one(false), ...one(true)));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function draw(S) {
|
|
349
|
+
const d = S.data[S.lens];
|
|
350
|
+
const kids = [stance(S)];
|
|
351
|
+
if (!d) kids.push(el('p', { class: 'dim mono', style: small, role: 'status' }, 'Reading the repository'));
|
|
352
|
+
else if (!d.ok) kids.push(el('p', { class: 'dim mono', style: small, role: 'status', 'data-bf': 'error' }, 'The briefing could not be read: ' + d.error));
|
|
353
|
+
else {
|
|
354
|
+
if (d.ticker && d.ticker.length && !(S.ctx && S.ctx.noTicker)) kids.push(ticker(d.ticker));
|
|
355
|
+
if (d.banner) {
|
|
356
|
+
const b = cardView(d.banner); const T = tagStyle(b);
|
|
357
|
+
kids.push(el('div', { class: 'incident', role: 'status', 'data-bf': 'banner' }, el('span', { class: 'tg f', style: '--tc:var(--destructive)' }, c3ic(T.icon, 'sm'), b.tag), el('span', null, b.headline)));
|
|
358
|
+
}
|
|
359
|
+
if (!d.cards.length) kids.push(el('div', { class: 'fold', 'data-bf': 'empty' }, c3ic('check', 'sm'), 'No signals for this lens. Nothing needs you.'));
|
|
360
|
+
else {
|
|
361
|
+
if (S.i >= d.cards.length) S.i = 0;
|
|
362
|
+
const views = d.cards.map(cardView);
|
|
363
|
+
const deck = el('div', { class: 'deck', tabindex: '-1', role: 'group', 'aria-roledescription': 'card deck', 'aria-label': S.lens + ' briefing, size ' + sizeFor(S.host.clientWidth || 500), 'data-bf': 'deck' }, ...views.map((v, i) => card(S, v, i, views.length)));
|
|
364
|
+
const dots = el('div', { class: 'dk', role: 'group', 'aria-label': 'Cards' }, ...views.map((v, i) => el('button', { type: 'button', class: 'dkd', 'aria-label': 'Card ' + (i + 1) + ': ' + v.tag, 'aria-current': String(i === 0), onClick: () => scrollToCard(S, deck, i) })));
|
|
365
|
+
wireDeck(S, deck, d.cards);
|
|
366
|
+
kids.push(el('section', { class: 'cdeck' }, deck, views.length > 1 ? dots : null));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
S.root.replaceChildren(...kids);
|
|
370
|
+
S.flip = null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function mountBriefing(host, ctx) {
|
|
374
|
+
if (!host) return;
|
|
375
|
+
let S = roots.get(host);
|
|
376
|
+
if (S) { if (!S.root.isConnected) host.replaceChildren(S.root); return; }
|
|
377
|
+
ensureC3Css();
|
|
378
|
+
const root = el('section', { class: 'c3 brief', 'data-board': 'briefing', 'aria-label': 'Briefing' });
|
|
379
|
+
host.replaceChildren(root);
|
|
380
|
+
const clock = dayPartFor(new Date());
|
|
381
|
+
S = { host, root, ctx: ctx || {}, lens: clock, clock, zoom: 'week', i: 0, flip: null, data: {}, dead: false };
|
|
382
|
+
S.stopPhone = c3Phone(root);
|
|
383
|
+
roots.set(host, S);
|
|
384
|
+
draw(S);
|
|
385
|
+
if (S.ctx.onLens) S.ctx.onLens(S.lens); // the page may follow the lens from the start (Reflect: "Carries over", "What I could stop")
|
|
386
|
+
// the shown lens first, then the others for their preview line
|
|
387
|
+
load(S, S.lens).then(() => Promise.all(LENSES.filter((l) => l !== S.lens).map((l) => load(S, l))));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Demo: a tab whose deck follows another control (the Work lens) changes the scope the deck asks for. Real mode never calls it. */
|
|
391
|
+
export function setBriefingScope(host, scope) {
|
|
392
|
+
const S = roots.get(host);
|
|
393
|
+
if (!S || S.ctx.scope === scope) return;
|
|
394
|
+
S.ctx.scope = scope; S.data = {}; S.i = 0; S.flip = null; draw(S);
|
|
395
|
+
load(S, S.lens).then(() => Promise.all(LENSES.filter((l) => l !== S.lens).map((l) => load(S, l))));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* Open one card on its case file (the bell's "go to the card", console3.html nopen 2533). True when the card is on this deck. */
|
|
399
|
+
export function showCard(host, key) {
|
|
400
|
+
const S = roots.get(host); const deck = S && S.root.querySelector('.deck'); if (!deck) return false;
|
|
401
|
+
const i = [...deck.children].findIndex((c) => c.dataset.bfCard === String(key)); if (i < 0) return false;
|
|
402
|
+
deck.scrollTo({ left: deck.children[i].offsetLeft - deck.children[0].offsetLeft, behavior: 'auto' }); deckSync(S, deck);
|
|
403
|
+
flipTo(S, deck.children[i], true); deck.children[i].focus({ preventScroll: true }); return true;
|
|
404
|
+
}
|
|
405
|
+
/* Esc anywhere on the Briefing turns the open card back (the mockup binds it on the document, line 2463). True when one was open. */
|
|
406
|
+
export function flipBack(host) {
|
|
407
|
+
const S = roots.get(host); const b = S && S.root.querySelector('.fc.back'); if (!b) return false;
|
|
408
|
+
flipTo(S, b, false); return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function disposeBriefing(host) {
|
|
412
|
+
const S = roots.get(host);
|
|
413
|
+
if (S) { S.dead = true; if (S.stopPhone) S.stopPhone(); }
|
|
414
|
+
roots.delete(host);
|
|
415
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* pm/briefing-model.js — pure model of the Briefing deck (console3 wave C1). No DOM, no fetch.
|
|
2
|
+
* The signal engine (scripts/signals) picks and scores the cards; this file only shapes one card for drawing.
|
|
3
|
+
* Every string here comes from a card field. A field the engine does not supply gives null, never a guess. */
|
|
4
|
+
|
|
5
|
+
export const LENSES = ['Plan', 'Unblock', 'Close', 'Reflect'];
|
|
6
|
+
export const TAGS = ['Critical', 'Just in', 'Opportunity', 'Small win', 'Big win'];
|
|
7
|
+
/* A tag is a shape plus a label, never a hue alone. The deck draws the shape; `mark` names it. */
|
|
8
|
+
export const TAG_MARK = { Critical: 'triangle', 'Just in': 'bolt', Opportunity: 'diamond', 'Small win': 'dot', 'Big win': 'flag' };
|
|
9
|
+
/* How the mockup draws a tag (console3.html lines 2206 and 2207): its icon, its colour token, whether the tag is filled, and the
|
|
10
|
+
* one-line reason shown on the case file. `key` is the value of the card's data-tag, which console3.css styles. */
|
|
11
|
+
export const TAG_STYLE = {
|
|
12
|
+
Critical: { key: 'critical', icon: 'alert', color: 'var(--destructive)', filled: true, q: 'Critical: you lose something if this waits.' },
|
|
13
|
+
'Just in': { key: 'justin', icon: 'bolt', color: 'var(--acc)', filled: false, q: 'Just in: new since you last looked, value not yet known.' },
|
|
14
|
+
Opportunity: { key: 'opp', icon: 'spark', color: 'var(--purple)', filled: false, q: 'Opportunity: upside that closes if you wait.' },
|
|
15
|
+
'Small win': { key: 'small', icon: 'up', color: 'var(--success)', filled: false, q: 'Small win: progress you can bank in one gesture.' },
|
|
16
|
+
'Big win': { key: 'big', icon: 'flag', color: 'var(--success)', filled: true, q: 'Big win: a milestone worth telling others.' },
|
|
17
|
+
};
|
|
18
|
+
/* The stance bar of each lens: icon and the line under its name (console3.html line 2206). */
|
|
19
|
+
export const PART_STYLE = {
|
|
20
|
+
Plan: { icon: 'sun', sub: 'Decide what gates the day' },
|
|
21
|
+
Unblock: { icon: 'bolt', sub: 'What is stuck, what is next' },
|
|
22
|
+
Close: { icon: 'check', sub: 'Send and finish before you leave' },
|
|
23
|
+
Reflect: { icon: 'moon', sub: 'What moved, what carries over' },
|
|
24
|
+
};
|
|
25
|
+
export const HEROES = ['line', 'big-number', 'ring', 'bars', 'two-bars', 'rows', 'mini-graph'];
|
|
26
|
+
|
|
27
|
+
/** 'L' at 500 and up, 'M' from 400 to 499, 'S' under 400. */
|
|
28
|
+
export function sizeFor(width) {
|
|
29
|
+
const w = Number(width);
|
|
30
|
+
if (!(w >= 400)) return 'S';
|
|
31
|
+
return w >= 500 ? 'L' : 'M';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The lens the clock suggests: before 11 Plan, 11 to 14 Unblock, 14 to 17 Close, from 17 Reflect. */
|
|
35
|
+
export function dayPartFor(date) {
|
|
36
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
37
|
+
const h = d.getHours();
|
|
38
|
+
return h < 11 ? 'Plan' : h < 14 ? 'Unblock' : h < 17 ? 'Close' : 'Reflect';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const num = (v) => (typeof v === 'number' && isFinite(v) ? v : null);
|
|
42
|
+
|
|
43
|
+
/** What the deck draws for one engine card. */
|
|
44
|
+
export function cardView(card) {
|
|
45
|
+
const c = card || {};
|
|
46
|
+
const tag = TAGS.includes(c.tag) ? c.tag : 'Just in';
|
|
47
|
+
const h = c.hero && typeof c.hero === 'object' ? c.hero : {};
|
|
48
|
+
const kind = HEROES.includes(h.kind) ? h.kind : 'big-number';
|
|
49
|
+
const m = c.move || null;
|
|
50
|
+
const rec = m && m.suggested ? m.suggested : null;
|
|
51
|
+
const dl = c.deadline || null;
|
|
52
|
+
const w = c.why || null;
|
|
53
|
+
return {
|
|
54
|
+
key: c.key || null,
|
|
55
|
+
tag, mark: TAG_MARK[tag], pinned: !!c.pinned,
|
|
56
|
+
headline: String(c.headline || ''),
|
|
57
|
+
hero: { kind, points: Array.isArray(h.points) ? h.points.map(num).filter((x) => x !== null) : null },
|
|
58
|
+
facts: (Array.isArray(c.facts) ? c.facts : []).filter(Boolean).map(String).slice(0, 3),
|
|
59
|
+
/* text only: no control in this wave sends anything */
|
|
60
|
+
move: rec ? String(rec.label || rec.id || '') : null,
|
|
61
|
+
options: m && Array.isArray(m.options) ? m.options.map((o) => ({ id: o.id, label: String(o.label || o.id || ''), slipAfter: num(o.slipAfter), recommended: o === rec || (rec && o.id === rec.id) })) : [],
|
|
62
|
+
unsure: m && m.note ? String(m.note) : null,
|
|
63
|
+
deadline: dl ? { days: num(dl.days), source: String(dl.source || 'unknown'), text: String(dl.text || '') } : null,
|
|
64
|
+
deadlineSource: dl ? String(dl.source || 'unknown') : null,
|
|
65
|
+
waitsFor: c.waitsFor ? String(c.waitsFor) : null,
|
|
66
|
+
why: w && w.text ? String(w.text) : null,
|
|
67
|
+
/* Optional, additive fields the engine does not send (the /c3.html demo mode serves them, from the console3 mockup's sample cards).
|
|
68
|
+
* A card without them is drawn exactly as before. */
|
|
69
|
+
sub: c.sub ? String(c.sub) : null,
|
|
70
|
+
pov: c.pov ? String(c.pov) : null,
|
|
71
|
+
eyebrow: c.eyebrow && typeof c.eyebrow === 'object' && c.eyebrow.label ? { label: String(c.eyebrow.label), icon: String(c.eyebrow.icon || 'target') } : null,
|
|
72
|
+
kpisX: Array.isArray(c.kpis) ? c.kpis.filter((k) => Array.isArray(k)).slice(0, 3).map((k) => ({ v: String(k[0]), l: String(k[1]), c: k[2] ? String(k[2]) : null })) : null,
|
|
73
|
+
factsX: Array.isArray(c.factsX) ? c.factsX.filter((f) => Array.isArray(f)).slice(0, 3).map((f) => ({ icon: String(f[0]), text: String(f[1]) })) : null,
|
|
74
|
+
rich: h.rich && typeof h.rich === 'object' ? h.rich : null,
|
|
75
|
+
conf: rec ? num(rec.conf) : null,
|
|
76
|
+
terms: w ? { impact: num(w.impact), urgency: num(w.urgency), irreversible: num(w.irreversible), score: num(w.score) } : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Index of the card a swipe lands on. dir +1 is up (next), -1 is down (previous); the stack wraps. */
|
|
81
|
+
export function nextCards(cards, i, dir = 1) {
|
|
82
|
+
const n = Array.isArray(cards) ? cards.length : 0;
|
|
83
|
+
if (!n) return 0;
|
|
84
|
+
const k = Number.isInteger(i) ? i : 0;
|
|
85
|
+
return (((k + (dir < 0 ? -1 : 1)) % n) + n) % n;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The swipe direction of a pointer drag: dy is end minus start in pixels. Under 40px is a tap, not a swipe. */
|
|
89
|
+
export function swipeDir(dy, min = 40) {
|
|
90
|
+
if (!isFinite(dy) || Math.abs(dy) < min) return 0;
|
|
91
|
+
return dy < 0 ? 1 : -1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── the case file of a card, a need or a draft (console3.html caseHtml 2275, CARDC 2215, NEEDC 2249, DRAFTC 2259) ──────────────────────────
|
|
95
|
+
* `registerCases` is the hook for sample content: the /c3.html demo registers the mockup's cases (c3-demo-brief-ix.js). Real mode registers
|
|
96
|
+
* none, so every case is worked out from the engine card's own fields: facts are the evidence, options are the options, and nothing is added.
|
|
97
|
+
* A real card has no route that carries an option out and no channel, so its case is `outward` with its own reason: the slide is drawn
|
|
98
|
+
* disabled and says why (c3-case.js slideFor). Nothing here writes. */
|
|
99
|
+
const REG = { card: {}, need: {}, draft: {}, agent: {}, people: {}, notifs: [], playbooks: [], docs: [] };
|
|
100
|
+
export function registerCases(o) {
|
|
101
|
+
for (const k of Object.keys(o || {})) { if (Array.isArray(REG[k])) REG[k] = o[k].slice(); else REG[k] = Object.assign(REG[k] || {}, o[k]); }
|
|
102
|
+
}
|
|
103
|
+
export const registered = (kind, key) => (REG[kind] && !Array.isArray(REG[kind]) ? REG[kind][key] || null : null);
|
|
104
|
+
export const registry = (kind) => REG[kind];
|
|
105
|
+
|
|
106
|
+
/** The raw option (as the case was written, with its `do` and `msg`) behind a normalized option id. */
|
|
107
|
+
export const rawOption = (m, id) => ((m && (m.opts || m.options)) || []).find((o, i) => String(o.id !== undefined ? o.id : 'o' + i) === String(id)) || null;
|
|
108
|
+
|
|
109
|
+
export const NO_ROUTE = 'No channel connected, and no route on this page carries it out. The suggestion stays text; nothing is sent or changed.';
|
|
110
|
+
|
|
111
|
+
/** The case of one engine card (v = cardView(card)). Pure; every string comes from a card field. */
|
|
112
|
+
const OUTWARD = /\b(send|tell|warn|reply|replies|email|message|notify|invite|ask)\b/i;
|
|
113
|
+
export function caseModelOf(v, o = {}) {
|
|
114
|
+
const reg = v && v.key && registered('card', v.key);
|
|
115
|
+
if (reg) return typeof reg === 'function' ? reg() : reg;
|
|
116
|
+
const c = v || {}; const sim = !!o.sim;
|
|
117
|
+
const ev = (c.factsX && c.factsX.length ? c.factsX.map((f) => ({ icon: f.icon, text: f.text })) : c.facts.map((f) => ({ icon: 'info', text: f })));
|
|
118
|
+
const opts = (c.options || []).map((o) => ({
|
|
119
|
+
id: o.id || o.label, label: o.label, rec: !!o.recommended, outward: !sim || OUTWARD.test(o.label || ''),
|
|
120
|
+
effects: o.slipAfter != null ? [{ icon: 'clock', text: 'slip after: ' + o.slipAfter + ' d', tone: o.slipAfter > 0 ? 'warn' : '' }] : [],
|
|
121
|
+
}));
|
|
122
|
+
const dl = c.deadline;
|
|
123
|
+
return {
|
|
124
|
+
id: 'card:' + (c.key || ''), tc: (TAG_STYLE[c.tag] || TAG_STYLE['Just in']).color,
|
|
125
|
+
situation: c.waitsFor ? 'Waiting on ' + c.waitsFor + '.' : c.sub || c.headline, evidence: ev, options: opts,
|
|
126
|
+
confidence: c.conf != null ? c.conf : null, unsure: c.unsure || '',
|
|
127
|
+
ifYouWait: dl ? (dl.days == null ? 'No deadline is known: ' + dl.text : dl.text) : '', why: c.why || '',
|
|
128
|
+
outward: !sim, outboxReason: NO_ROUTE,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The case of a "Needs you" row. A registered case wins; else the card's own, else one worked out from the row. */
|
|
133
|
+
export function needCaseOf(n) {
|
|
134
|
+
const reg = registered('need', n.key);
|
|
135
|
+
if (reg) return reg;
|
|
136
|
+
if (n.view) return caseModelOf(n.view, { sim: !!n.sim }); // a real row is one engine card: its case is the card's own
|
|
137
|
+
return {
|
|
138
|
+
id: 'need:' + n.key, situation: n.headline,
|
|
139
|
+
evidence: [{ icon: 'clock', text: 'Due ' + (n.time || 'soon'), source: n.src || '' }],
|
|
140
|
+
options: [], outward: true, outboxReason: NO_ROUTE,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The case of a Review draft row (the mockup's genDraft when none is registered). */
|
|
145
|
+
export function draftCaseOf(x) {
|
|
146
|
+
const reg = registered('draft', x.id);
|
|
147
|
+
if (reg) return reg;
|
|
148
|
+
return {
|
|
149
|
+
id: 'draft:' + x.id, quote: x.body || '', situation: 'Built from ' + (x.basis || 'the decision log') + '. Nothing is sent until you send it.',
|
|
150
|
+
evidence: [{ icon: 'doc', text: 'Basis: ' + (x.basis || 'decision log'), source: 'sources' }],
|
|
151
|
+
cta: 'Slide to approve',
|
|
152
|
+
options: [
|
|
153
|
+
{ id: 'ok', label: 'Approve', rec: true, do: 'ready', effects: [{ icon: 'out', text: 'Nothing is sent until you send it', tone: 'ok' }] },
|
|
154
|
+
{ id: 'sb', label: 'Send back with a note', do: 'back', effects: [{ icon: 'undo', text: 'The agent revises it' }] },
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** What the bell lists: the briefing's `notifs` (the demo sends them) or one entry per card. `go` names the target. */
|
|
160
|
+
export function notifsOf(b) {
|
|
161
|
+
if (b && Array.isArray(b.notifs)) return b.notifs;
|
|
162
|
+
return (b && Array.isArray(b.cards) ? b.cards : []).filter((c) => c && c.headline).map((c) => ({ key: String(c.key || c.headline), tag: TAGS.includes(c.tag) ? c.tag : 'Just in', headline: String(c.headline), when: '', unread: false, go: { card: c.key || null } }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The next index of a list of rows for j (+1) / k (-1); `at` is the index of the focused row or -1. */
|
|
166
|
+
export function rowStep(count, at, dir) {
|
|
167
|
+
if (count <= 0) return -1;
|
|
168
|
+
if (dir > 0) return Math.min(count - 1, at + 1);
|
|
169
|
+
return Math.max(0, at < 0 ? 0 : at - 1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The palette's results (console3.html line 1945): `items` are { label, kind, run }; the filter is a case-blind substring, at most 9. */
|
|
173
|
+
export function filterPalette(items, q, max = 9) {
|
|
174
|
+
const s = String(q || '').trim().toLowerCase();
|
|
175
|
+
return (items || []).filter((it) => !s || String(it.label).toLowerCase().includes(s) || String(it.kind || '').toLowerCase().includes(s)).slice(0, max);
|
|
176
|
+
}
|