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.
Files changed (127) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/nine-stages.js +81 -0
  6. package/payload/scripts/workflow-state.js +2 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/c3-one-home-test.js +131 -0
  10. package/workspace/fixtures/initiatives-api/list.json +171 -3
  11. package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
  12. package/workspace/fixtures/initiatives-api/sheet.json +57 -1
  13. package/workspace/fixtures/one-home-baseline.json +83 -0
  14. package/workspace/guide/layout.md +2 -0
  15. package/workspace/guide/notes.md +1 -0
  16. package/workspace/lib/engine/nine-stages.js +81 -0
  17. package/workspace/lib/engine/workflow-state.js +2 -0
  18. package/workspace/lib/facts-async.js +51 -0
  19. package/workspace/lib/item-steps.js +52 -0
  20. package/workspace/lib/jira-status.js +8 -3
  21. package/workspace/lib/jobs-to-be-done.js +52 -0
  22. package/workspace/lib/outbox-store.js +87 -0
  23. package/workspace/lib/pm-items.js +115 -0
  24. package/workspace/lib/route-modules.js +21 -0
  25. package/workspace/lib/routes-briefing.js +100 -0
  26. package/workspace/lib/routes-c3-init.js +53 -0
  27. package/workspace/lib/routes-c3-life.js +119 -0
  28. package/workspace/lib/routes-c3-strategy.js +169 -0
  29. package/workspace/lib/routes-c3-timeline.js +191 -0
  30. package/workspace/lib/routes-calendar-import.js +60 -0
  31. package/workspace/lib/routes-calendar-views.js +61 -0
  32. package/workspace/lib/routes-doc-health.js +79 -0
  33. package/workspace/lib/routes-doc-sheet.js +169 -0
  34. package/workspace/lib/routes-forecast.js +85 -0
  35. package/workspace/lib/routes-hooks.js +121 -0
  36. package/workspace/lib/routes-initiatives.js +3 -0
  37. package/workspace/lib/routes-intake.js +24 -0
  38. package/workspace/lib/routes-outbox.js +97 -0
  39. package/workspace/lib/routes-pm-items.js +216 -0
  40. package/workspace/lib/routes-records.js +61 -0
  41. package/workspace/lib/routes-releases.js +129 -0
  42. package/workspace/lib/routes-report-facts.js +84 -0
  43. package/workspace/lib/routes-report-families.js +32 -0
  44. package/workspace/lib/routes-roadmap-ops.js +141 -0
  45. package/workspace/lib/routes-runs.js +119 -0
  46. package/workspace/lib/routes-signals.js +50 -0
  47. package/workspace/lib/run-answers.js +69 -0
  48. package/workspace/package-lock.json +2 -2
  49. package/workspace/package.json +1 -1
  50. package/workspace/public/c3.html +26 -0
  51. package/workspace/public/pm/briefing-deck.js +415 -0
  52. package/workspace/public/pm/briefing-model.js +176 -0
  53. package/workspace/public/pm/c3-breakdown.js +240 -0
  54. package/workspace/public/pm/c3-brief.js +278 -0
  55. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  56. package/workspace/public/pm/c3-case.js +173 -0
  57. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  58. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  59. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  60. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  61. package/workspace/public/pm/c3-demo-data.js +560 -0
  62. package/workspace/public/pm/c3-demo-init.js +373 -0
  63. package/workspace/public/pm/c3-demo-life.js +77 -0
  64. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  65. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  66. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  67. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  68. package/workspace/public/pm/c3-demo-term.js +153 -0
  69. package/workspace/public/pm/c3-demo-time.js +49 -0
  70. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  71. package/workspace/public/pm/c3-demo-work2.js +13 -0
  72. package/workspace/public/pm/c3-demo.js +113 -0
  73. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  74. package/workspace/public/pm/c3-dock.js +180 -0
  75. package/workspace/public/pm/c3-docs.js +290 -0
  76. package/workspace/public/pm/c3-expand.js +96 -0
  77. package/workspace/public/pm/c3-init-extras.js +170 -0
  78. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  79. package/workspace/public/pm/c3-misc-panels.js +89 -0
  80. package/workspace/public/pm/c3-plan-panels.js +163 -0
  81. package/workspace/public/pm/c3-plan.js +192 -0
  82. package/workspace/public/pm/c3-roadmap.js +249 -0
  83. package/workspace/public/pm/c3-sheet.js +107 -0
  84. package/workspace/public/pm/c3-shell.js +637 -0
  85. package/workspace/public/pm/c3-signals.js +196 -0
  86. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  87. package/workspace/public/pm/c3-swipe.js +138 -0
  88. package/workspace/public/pm/c3-terminal-data.js +26 -0
  89. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  90. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  91. package/workspace/public/pm/c3-terminal-results.js +141 -0
  92. package/workspace/public/pm/c3-terminal.js +52 -0
  93. package/workspace/public/pm/c3-tickets.js +163 -0
  94. package/workspace/public/pm/c3-timeline.js +201 -0
  95. package/workspace/public/pm/c3-toast.js +16 -0
  96. package/workspace/public/pm/calendar-views.js +1015 -0
  97. package/workspace/public/pm/console3-lift.md +220 -0
  98. package/workspace/public/pm/console3.css +1480 -0
  99. package/workspace/public/pm/dest-initiatives.js +16 -3
  100. package/workspace/public/pm/dest-mywork.js +8 -1
  101. package/workspace/public/pm/dest-notes.js +25 -3
  102. package/workspace/public/pm/dest-reports.js +47 -1
  103. package/workspace/public/pm/dest-today.js +55 -1
  104. package/workspace/public/pm/doc-health-panel.js +110 -0
  105. package/workspace/public/pm/doc-rail.js +9 -3
  106. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  107. package/workspace/public/pm/lifecycle-lens.js +261 -0
  108. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  109. package/workspace/public/pm/outbox-panel.js +125 -0
  110. package/workspace/public/pm/pm-work-view.js +919 -0
  111. package/workspace/public/pm/releases-view.js +248 -0
  112. package/workspace/public/pm/report-families-view.js +503 -0
  113. package/workspace/public/pm/run-card.js +244 -0
  114. package/workspace/public/pm/sessions-panel.js +149 -0
  115. package/workspace/public/pm/shell-bell.js +162 -0
  116. package/workspace/public/pm/shell-model.js +83 -0
  117. package/workspace/public/pm/shell-phone.js +124 -0
  118. package/workspace/public/pm/shell-readaloud.js +53 -0
  119. package/workspace/public/pm/shell-recording.js +110 -0
  120. package/workspace/public/pm/shell-ticker.js +90 -0
  121. package/workspace/public/pm/slide-confirm.js +163 -0
  122. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  123. package/workspace/public/pm/today-day.js +8 -2
  124. package/workspace/public/pm/tokens.css +1 -0
  125. package/workspace/server.js +5 -2
  126. package/workspace/test-files.json +8 -0
  127. package/workspace/test-lists/browser-suites-green.json +5 -0
@@ -0,0 +1,1015 @@
1
+ /* pm/calendar-views.js — the Calendar views Day, Week, Month, Quarter and Year (console3 wave C2), built on the console3
2
+ * mockup's own markup and CSS. The mockup (prototype/pmos-console-handoff/mockups/console3.html) is the UI: the zoom bar
3
+ * (.calbar, .lens, .seg, .ib), the Today chip (.tdy), the lens header (.lh), the day timeline (.vday, .vb), the month grid
4
+ * (.cal, .cd), the progress chart (svg.pg: progSVG, line 2712), the direction card (.fc.dir with .dial and .sevs), the key
5
+ * result rows (.krg, .krt) and the swipe and key gestures. The styles are pm/console3.css (see pm/console3-lift.md).
6
+ * This file keeps the data call and draws what the server sent. It reads GET /api/calendar-views only and writes nothing.
7
+ * A KR without a metric reads "no metric" and is not scored; an empty lens says why; nothing is invented.
8
+ *
9
+ * mountCalendarViews(host, ctx) is safe to call on every render. disposeCalendarViews(host) drops the host's state. */
10
+
11
+ import { el } from './dom.js';
12
+ import { pmGet } from './http.js';
13
+ import { ensureC3Css, c3ic, c3ib, c3ring, c3Phone } from './pm-work-view.js';
14
+ import { toast } from './c3-toast.js';
15
+ import { goInitiative } from './c3-init-extras.js';
16
+ import { openSheet, closeSheet } from './c3-sheet.js';
17
+ import { renderCase } from './c3-case.js';
18
+ import { makeXp, attachExpand } from './c3-expand.js';
19
+ import { mountQuarter, mountYear, dispose as disposeStrategy } from './c3-cal-strategy.js'; // the Quarter and Year panes
20
+ import { simDock, simStart, simState, isDemo } from './shell-recording.js';
21
+
22
+ /* ── shared state of the /c3.html page (console3 wave C3) ──────────────────────────────────────────────────────────
23
+ * `live` holds every mounted instance, so a press in the Briefing's ribbon can select a meeting on the Calendar tab's instance.
24
+ * `IX()` is the demo fixtures (c3-demo-cal-ix.js sets window.__c3calix); in real mode it is null and only the calendar's own fields are shown.
25
+ * The page's tab buttons carry data-c3-tab; pressing one is the only hook into the shell (c3-shell.js is not edited). */
26
+ const live = new Set();
27
+ let SHARED_SEL = null; // a meeting chosen on the Briefing, read when the Calendar tab mounts
28
+ const IX = () => (typeof window !== 'undefined' && window.__c3calix) || null;
29
+ const baseId = (e) => String((e && e.id) || '').split('@')[0];
30
+ const gotoTab = (id) => { const b = document.querySelector('[data-c3-tab="' + id + '"]'); if (b) b.click(); };
31
+ const isC3 = (r) => !!(r && r.ctx && (r.ctx.ribbon || r.ctx.plan));
32
+
33
+ const NS = 'http://www.w3.org/2000/svg';
34
+ const VIEWS = [['day', 'Day'], ['week', 'Week'], ['month', 'Month'], ['quarter', 'Quarter'], ['year', 'Year']];
35
+ /* the mockup's ribbon (console3.html line 1435): Today | Week | Quarter | Year | 5 yrs. Each reads the calendar view in [1]; 5 yrs has no calendar view, so it reads the year. */
36
+ const RIB = [['today', 'Today', 'day'], ['week', 'Week', 'week'], ['quarter', 'Quarter', 'quarter'], ['year', 'Year', 'year'], ['five', '5 yrs', 'year']];
37
+ const SHORT = { day: 'Day', week: 'Wk', month: 'Mo', quarter: 'Qtr', year: 'Yr' }; // the mockup's CMS (line 2829)
38
+ /* The mockup's ST table (line 2704): colour and icon of a status. The server's words map onto its keys. */
39
+ const ST = {
40
+ ok: { l: 'On time', c: 'var(--success)', i: 'check' }, risk: { l: 'At risk', c: 'var(--warning)', i: 'alert' },
41
+ late: { l: 'Delayed', c: 'var(--destructive)', i: 'clock' }, over: { l: 'Overdue', c: 'var(--destructive)', i: 'alert' },
42
+ none: { l: 'No plan', c: 'var(--muted-foreground)', i: 'moon' },
43
+ };
44
+ const STKEY = { 'on time': 'ok', 'at risk': 'risk', delayed: 'late', overdue: 'over', 'no plan': 'none' };
45
+ const MN = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
46
+ const DN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
47
+ const roots = new WeakMap(); // host -> { root, view, date, data, error, loading }
48
+ const small = 'font-size:var(--fs-2)';
49
+
50
+ const pad = (n) => String(n).padStart(2, '0');
51
+ const todayStr = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
52
+ const ymd = (s) => String(s).slice(0, 10).split('-').map(Number);
53
+ const utc = (s) => { const [y, m, d] = ymd(s); return Date.UTC(y, m - 1, d); };
54
+ const dowOf = (s) => new Date(utc(s)).getUTCDay();
55
+ const shift = (view, date, dir) => {
56
+ const [y, m, d] = ymd(date);
57
+ const t = view === 'day' ? new Date(y, m - 1, d + dir) : view === 'week' ? new Date(y, m - 1, d + 7 * dir)
58
+ : view === 'month' ? new Date(y, m - 1 + dir, 1) : view === 'quarter' ? new Date(y, m - 1 + 3 * dir, 1) : new Date(y + dir, 0, 1);
59
+ return `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`;
60
+ };
61
+ /* the mockup's date strings, console3.html: Thu 8 Oct (weekday, day, month; no comma, not locale-ordered) */
62
+ const dayLabel = (s) => { const [y, m, d] = ymd(s); return DN[new Date(y, m - 1, d).getDay()] + ' ' + d + ' ' + MN[m - 1]; };
63
+ const cellLabel = (c, unit) => unit === 'day' ? dayLabel(c.start) : unit === 'week' ? 'Week of ' + dayLabel(c.start)
64
+ : new Date(...c.start.split('-').map((n, i) => (i === 1 ? n - 1 : Number(n)))).toLocaleDateString(undefined, { month: 'long' });
65
+ const rangeText = (range, view) => (view === 'quarter' ? 'Q' + (Math.floor((Number(range.start.slice(5, 7)) - 1) / 3) + 1) + " '" + range.start.slice(2, 4) + ' · ' + MN[Number(range.start.slice(5, 7)) - 1] + ' to ' + MN[Number(range.end.slice(5, 7)) - 1] : view === 'month' ? new Date(utc(range.start)).toLocaleDateString('en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' }) : range.start === range.end ? dayLabel(range.start) : `${dayLabel(range.start)} to ${dayLabel(range.end)}`);
66
+
67
+ function svg(tag, attrs, ...kids) {
68
+ const n = document.createElementNS(NS, tag);
69
+ for (const k of Object.keys(attrs || {})) n.setAttribute(k, attrs[k]);
70
+ for (const c of kids) if (c) n.appendChild(c);
71
+ return n;
72
+ }
73
+ const text = (x, y, s, attrs) => { const t = svg('text', { x, y, ...(attrs || {}) }); t.textContent = s; return t; };
74
+
75
+ /* On a phone the chart rows are too short to press (30px of a 720px chart on a 390px screen), so each initiative is also a 44px button under it. */
76
+ function phoneRows(rows, r, host) {
77
+ return el('ul', { class: 'list', role: 'list', style: 'margin:0', 'data-cv': 'init-rows' }, rows.map((g) => { const k = STKEY[g.status] || 'none'; const S = ST[k];
78
+ return el('li', { class: 'prow', style: '--c:' + S.c }, el('button', { type: 'button', class: 'nrow', style: 'width:100%;min-height:44px;border:0;background:none;text-align:left;display:flex;justify-content:space-between;gap:8px;align-items:center', 'data-a': 'goinit', 'data-k': g.id, onClick: () => openInit(host, r, g.id, g.title) },
79
+ el('b', null, g.title || g.id), el('span', { class: 'mono', style: small + ';color:' + S.c }, S.l + (g.slip_days > 0 ? ' +' + g.slip_days + ' d' : '')))); }));
80
+ }
81
+ const isPhoneR = (r) => !!(r && r.root && r.root.classList.contains('ph'));
82
+
83
+ /* ── the bar: the mockup's calHead3 (line 2584, with the Today chip of line 2790) ─────────────────────────────────── */
84
+
85
+ /* The Today chip. On today's range it is the quiet .tdy; away from it, the .tdy.on button that goes back. */
86
+ function todayChip(host, r) {
87
+ const range = r.data && r.data.range; const today = (r.data && r.data.today) || todayStr();
88
+ const here = range ? range.start <= today && today <= range.end : r.date === todayStr();
89
+ if (here) return el('span', { class: 'tdy', 'aria-label': 'You are on today', 'data-cv': 'today' }, c3ic('sun', 'sm'), 'Today');
90
+ const ahead = range ? range.start > today : r.date > todayStr();
91
+ return el('button', { type: 'button', class: 'tdy on', 'data-cv': 'today', title: 'Back to today (t)', onClick: () => go(host, r, { date: todayStr(), anim: ahead ? 'sp' : 'sn' }) },
92
+ ahead ? c3ic('prev', 'sm') : null, 'Today', ahead ? null : c3ic('next', 'sm'));
93
+ }
94
+
95
+ function bar(host, r, phone) {
96
+ const rib = !!(r.ctx && r.ctx.ribbon);
97
+ const L = rib ? RIB : VIEWS; const cur = rib ? r.zoom : r.view;
98
+ const i = L.findIndex((v) => v[0] === cur);
99
+ const to = (j, anim) => go(host, r, rib ? { view: L[j][2], zoom: L[j][0], anim } : { view: L[j][0], anim });
100
+ const zoom = (k) => { const j = i + k; if (j >= 0 && j < L.length) to(j, k < 0 ? 'zi' : 'zo'); };
101
+ const range = r.data && r.data.range;
102
+ return el('div', { class: 'calbar' },
103
+ el('div', { class: 'lens' },
104
+ c3ib('Zoom in (+)', 'zin', { disabled: i === 0, 'data-a': rib ? 'rzoom' : 'czoom', 'data-k': '-1', onClick: () => zoom(-1) }, 'sm'),
105
+ el('div', { class: 'seg', role: 'group', 'aria-label': rib ? 'Time horizon' : 'Calendar view', 'data-cv': 'switch' },
106
+ L.map(([id, label], j) => el('button', { type: 'button', 'aria-pressed': String(cur === id), 'data-cv-view': id, 'data-a': rib ? 'ribbon' : 'calmode', 'data-k': id, onClick: () => to(j, j < i ? 'zi' : 'zo') },
107
+ phone && SHORT[id] ? SHORT[id] : label))),
108
+ c3ib('Zoom out (-)', 'zout', { disabled: i === L.length - 1, 'data-a': rib ? 'rzoom' : 'czoom', 'data-k': '1', onClick: () => zoom(1) }, 'sm')),
109
+ el('div', { class: 'chips2' },
110
+ c3ib('Previous (swipe right)', 'prev', { 'data-cv': 'prev', 'data-a': 'calprev', onClick: () => go(host, r, { date: shift(r.view, r.date, -1), anim: 'sp' }) }, 'sm'),
111
+ el('b', { class: 'mono', style: small + ';min-width:96px;text-align:center', 'data-cv': 'range', 'aria-live': 'polite' }, range ? rangeText(range, r.view) : ''),
112
+ c3ib('Next (swipe left)', 'next', { 'data-cv': 'next', 'data-a': 'calnext', onClick: () => go(host, r, { date: shift(r.view, r.date, 1), anim: 'sn' }) }, 'sm'),
113
+ todayChip(host, r)));
114
+ }
115
+
116
+ /* ── the lens header: the mockup's calLensHead (line 2582), from what the server sent ─────────────────────────────── */
117
+
118
+ function lensHead(data) {
119
+ if (data.lens_head) { // optional (demo): the mockup's calLensHead line and ring
120
+ const L = data.lens_head;
121
+ return el('div', { class: 'lh' }, el('span', { class: 'ki' }, c3ic('cal')), el('div', { style: 'min-width:0' }, el('b', null, L.h), el('div', { class: 'dim' }, L.s)), c3ring(L.ring[0], Math.max(1, L.ring[1]), 44));
122
+ }
123
+ const n = (data.cells || []).reduce((t, c) => t + c.events.length, 0);
124
+ const rows = (data.gantt || []).filter((g) => g.status !== 'no plan'); // only a row with a promise can be on time or late
125
+ const ontime = rows.filter((g) => g.status === 'on time').length;
126
+ const b = n ? `${n} event${n === 1 ? '' : 's'} in ${data.unit === 'day' ? 'this range' : 'the ' + data.unit + 's shown'}`
127
+ : data.events_source === 'none' ? 'No calendar is cached yet' : 'Nothing is scheduled in this range';
128
+ const s = rows.length ? `${ontime} of ${rows.length} initiative${rows.length === 1 ? '' : 's'} on time against the promise.` : 'No initiative has a date in this range.';
129
+ return el('div', { class: 'lh' }, el('span', { class: 'ki' }, c3ic('cal')),
130
+ el('div', { style: 'min-width:0' }, el('b', null, b), el('div', { class: 'dim' }, s)),
131
+ c3ring(ontime, rows.length, 44));
132
+ }
133
+
134
+ /* ── events: the day timeline, the week list, the month grid, the quarter and year rows ──────────────────────────── */
135
+
136
+ const timeOf = (at) => { const m = /T(\d{2}):(\d{2})/.exec(String(at || '')); return m ? Number(m[1]) + Number(m[2]) / 60 : null; };
137
+ const hhmm = (x) => `${pad(Math.floor(x))}:${pad(Math.round((x % 1) * 60))}`;
138
+
139
+ /* The mockup's vday (line 1400): hour lines, .vb blocks placed by time, .vnow for now. Events without a time go in a list. */
140
+ function dayTimeline(data, ribbon, r, host) {
141
+ const evs = (data.cells[0] && data.cells[0].events) || [];
142
+ const timed = evs.filter((e) => timeOf(e.at) !== null).sort((a, b) => timeOf(a.at) - timeOf(b.at));
143
+ const untimed = evs.filter((e) => timeOf(e.at) === null);
144
+ const end = (e) => { const t = timeOf(e.end); return t !== null && String(e.end).slice(0, 10) === String(e.at).slice(0, 10) && t > timeOf(e.at) ? t : timeOf(e.at) + 1; };
145
+ let t0 = Math.min(8, ...timed.map((e) => Math.floor(timeOf(e.at)))); let t1 = Math.max(18, ...timed.map((e) => Math.ceil(end(e))));
146
+ if (data.span) { t0 = data.span[0]; t1 = data.span[1]; } // optional (demo): the mockup's 08:00 to 17:30 day
147
+ const pph = 38; const T = (t) => (t - t0) * pph;
148
+ const v = el('div', { class: 'vday', style: 'height:' + ((t1 - t0) * pph - 1) + 'px' });
149
+ for (let t = t0; t <= t1 - (data.span ? 1.5 : 2); t += 2) v.appendChild(el('div', { class: 'hr', style: 'top:' + T(t) + 'px' }, el('span', null, hhmm(t))));
150
+ /* Optional, additive fields the server does not send (the /c3.html demo mode serves them): data.free_gaps = [[from, to, label]] draws the
151
+ * mockup's hatched free windows (.vf); an event's `mock_routine` and `mock_hot` give the muted and the red block, and `mock_decide_by` the red line (.vdl). (The prefix keeps them apart from the
152
+ * calendar memory's own event fields, which already include `routine`.) */
153
+ (data.free_gaps || []).forEach(([a, b, lab]) => { if (lab) v.appendChild(el('div', { class: 'vf', style: `top:${T(a) + 2}px;height:${(b - a) * pph - 4}px` }, lab)); });
154
+ const selE = r ? selEvent(r) : null; const selId = selE ? selE.id : (timed.find((e) => e.mock_hot) || {}).id;
155
+ const c3 = isC3(r);
156
+ timed.forEach((e, i) => {
157
+ const s = timeOf(e.at); const f = end(e);
158
+ v.appendChild(el('button', { type: 'button', class: 'vb' + (e.mock_routine ? ' rt' : '') + (e.mock_hot ? ' hot' : '') + (selId && e.id === selId ? ' sel' : ''), style: `top:${T(s) + 1}px;height:${(f - s) * pph - 3}px;animation-delay:${i * 70}ms`, 'data-cv-cell': e.id, 'data-a': ribbon ? 'rsel' : 'msel', 'data-k': baseId(e), title: e.title || e.id,
159
+ ...(c3 ? { 'aria-pressed': String(e.id === selId), onClick: () => selectMeeting(host, r, e.id) } : {}) },
160
+ el('b', null, e.title || e.id), f - s >= 0.75 ? el('small', null, hhmm(s) + (e.kind ? ' · ' + e.kind : '')) : null));
161
+ if (e.mock_decide_by) { const d = timeOf(e.at); v.appendChild(el('div', { class: 'vdl', style: 'top:' + T(d) + 'px' }, el('span', null, 'decide by ' + hhmm(d)))); }
162
+ });
163
+ const today = data.today; const now = new Date();
164
+ if (data.range.start === today && now.getHours() >= t0 && now.getHours() < t1) v.appendChild(el('div', { class: 'vnow', style: 'top:' + T(now.getHours() + now.getMinutes() / 60).toFixed(1) + 'px' }));
165
+ const rows = untimed.map((e) => el('li', { class: 'nrow', 'data-cv-cell': e.id },
166
+ el('span', { class: 'ki' }, c3ic('cal')), el('div', { class: 'nb' }, el('span', { class: 'tt' }, e.title || e.id), el('div', { class: 'chips' }, el('span', { class: 'tag' }, 'all day' + (e.kind ? ' · ' + e.kind : ''))))));
167
+ // the mockup's loadHtml (line 1410): meeting hours against the free hours of an eight to eighteen day
168
+ const mh = timed.reduce((t, e) => t + (end(e) - timeOf(e.at)), 0); const free = Math.max(0, 10 - mh);
169
+ const load = el('div', { 'data-cv': 'load' }, el('div', { class: 'lbar' }, el('i', { style: 'width:' + Math.min(100, mh / 10 * 100) + '%;background:var(--acc)' }), el('i', { style: 'flex:1;background:var(--muted)' })),
170
+ el('div', { class: 'chips2 dim mono', style: 'font-size:var(--fs-1);justify-content:space-between;margin-top:4px' }, el('span', null, mh.toFixed(1) + ' h meetings'), el('span', null, free.toFixed(1) + ' h free')));
171
+ const sel = ribbon && r && selE ? rdetNode(host, r, selE) : null;
172
+ if (c3) scrubWire(v, timed, end, t0, pph);
173
+ const parts = [timed.length ? v : null, sel, rows.length ? el('ul', { class: 'list', role: 'list' }, rows) : null, timed.length ? load : null];
174
+ if (ribbon) return el('div', { class: 'rpane', 'data-z': 'today', style: 'display:grid;gap:var(--sp-3)' }, lensHead(data), ...parts);
175
+ return el('section', { class: 'card glass ribbon', 'data-cv': 'deck' }, ...parts);
176
+ }
177
+
178
+ /* The mockup's weekListPh (line 2620): one .nrow per day, the day as a mono .ki, the events as chips. */
179
+ function listRows(data, r, host) {
180
+ const cells = (data.cells || []).filter((c) => c.events.length);
181
+ const rows = cells.map((c) => el('li', { class: 'nrow', 'data-cv-cell': c.key, 'data-today': c.start <= data.today && data.today <= c.end ? 'true' : null, tabindex: '0', style: 'grid-template-columns:auto minmax(0,1fr);cursor:pointer;padding-inline:8px',
182
+ onClick: () => { if (data.unit === 'day') go(host, r, { view: 'day', date: c.start, anim: 'zi' }); } },
183
+ el('span', { class: 'ki mono', style: 'width:auto;padding:0 8px;' + small }, data.unit === 'day' ? dayLabel(c.start) : cellLabel(c, data.unit)),
184
+ el('div', { class: 'nb' },
185
+ ...c.events.slice(0, 5).map((e) => (data.unit === 'day' && timeOf(e.at) !== null
186
+ ? el('button', { type: 'button', class: 'tt', 'data-cv-ev': e.id, 'data-a': 'calblk', 'data-k': Number(c.start.slice(8)) + '|' + baseId(e), title: 'Open ' + (e.title || e.id) + ' on ' + dayLabel(c.start), style: 'display:block;font-weight:400;border:0;background:none;padding:0;text-align:left;cursor:pointer;color:inherit', onClick: (ev) => { ev.stopPropagation(); go(host, r, { view: 'day', date: c.start, anim: 'zi' }); r.sel = e.id; } }, e.title || e.id)
187
+ : el('div', { class: 'tt', style: 'display:block;font-weight:400' }, e.title || e.id))),
188
+ c.events.length > 5 ? el('div', { class: 'chips2 dim mono', style: small }, `+${c.events.length - 5} more`) : null)));
189
+ return el('section', { class: 'card', 'data-cv': 'deck' }, el('ul', { class: 'list', role: 'list', style: 'margin:0' }, rows));
190
+ }
191
+
192
+ /* The mockup's monthView (line 1555): a Monday-first .cal grid of .cd day buttons; the shade is the number of events. */
193
+ /* the mockup's day marks (MS, line 1509 and mglyph): a glyph per release, promise, report or learning that day; a calendar with no marks names the count */
194
+ function monthMarks(c, n) {
195
+ const mk = c.events.filter((e) => e.kind === 'milestone').map((e) => markOf(e.title)).filter(Boolean);
196
+ if (mk.length) return mk.slice(0, 4).map((k) => el('span', { style: 'color:' + (MKS[k] || MKS.report)[1] }, (MKS[k] || MKS.report)[0]));
197
+ return IX() ? [] : [n ? n + (n === 1 ? ' event' : ' events') : ''];
198
+ }
199
+ function monthGrid(data, r, host) {
200
+ const first = data.range.start; const off = (dowOf(first) + 6) % 7;
201
+ const cells = data.cells || []; const max = Math.max(1, ...cells.map((c) => c.events.length));
202
+ const h = el('div', { class: 'cal' }, ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((n) => el('div', { class: 'dh' }, n)));
203
+ for (let i = 0; i < off; i++) h.appendChild(el('div', { class: 'cd out', 'aria-hidden': 'true' }));
204
+ cells.forEach((c) => {
205
+ const n = c.events.length; const wd = dowOf(c.start);
206
+ h.appendChild(el('button', { type: 'button', class: 'cd' + (wd === 0 || wd === 6 ? ' we' : '') + (c.start === data.today ? ' today' : ''), 'data-cv-cell': c.key, 'data-a': 'calday', 'data-k': String(Number(c.start.slice(8))), 'data-today': c.start === data.today ? 'true' : null,
207
+ title: n ? c.events.map((e) => e.title || e.id).join(', ') : 'No events', onClick: () => go(host, r, { view: 'day', date: c.start, anim: 'zi' }) },
208
+ el('span', { class: 'n' }, String(Number(c.start.slice(8)))), el('span', { class: 'm', 'aria-hidden': 'true' }, ...monthMarks(c, n)), n ? el('span', { class: 'heat', style: 'height:' + Math.round(n / max * 100) + '%' }) : null));
209
+ });
210
+ return el('section', { class: 'card', 'data-cv': 'deck' }, h, el('div', { class: 'legend' }, el('span', null, 'shade = events that day')));
211
+ }
212
+
213
+ function events(data, r, host) {
214
+ if (isC3(r) && (r.view === 'quarter' || r.view === 'year') && data.strategy) return null; // the mockup's quarter and year are the strategy lens: no list of meetings
215
+ const n = (data.cells || []).reduce((t, c) => t + c.events.length, 0);
216
+ if (!n) {
217
+ return el('section', { class: 'card', 'data-cv': 'deck' },
218
+ el('div', { class: 'fold', 'data-cv': 'no-events' }, c3ic('moon', 'sm'), data.events_source === 'none' ? 'No calendar is cached yet, so there are no meetings to show.' : 'Nothing is scheduled in this range.'));
219
+ }
220
+ return data.unit === 'day' && data.range.start === data.range.end ? dayTimeline(data, false, r, host) : data.range.days > 7 && data.unit === 'day' ? monthGrid(data, r, host) : listRows(data, r, host);
221
+ }
222
+
223
+ /* ── promised against forecast: the mockup's progSVG (line 2712), drawn from the server's gantt rows ───────────────── */
224
+
225
+ function gantt(data, r, host) {
226
+ const every = data.gantt || [];
227
+ const all = every.filter((g) => g.status !== 'no plan'); // a row with no plan has no bar to draw: it is counted, not drawn
228
+ const noPlan = every.length - all.length;
229
+ const head = el('h3', null, c3ic('target'), 'Promised against forecast ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'when things land, and whether they will'));
230
+ const nope = noPlan ? el('div', { class: 'dim mono', style: small, 'data-cv': 'no-plan' }, noPlan + ' initiative' + (noPlan === 1 ? ' has' : 's have') + ' no plan date, so no bar.') : null;
231
+ if (!all.length) return el('section', { class: 'card', 'data-cv': 'gantt' }, head, el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No initiative has a date in this range.'), nope);
232
+ const R = all.slice(0, 10);
233
+ const a = utc(data.range.start); const N = Math.round((utc(data.range.end) - a) / 864e5) + 1;
234
+ const W = 720; const LW = 150; const SW = 104; const CW = W - LW - SW; const dw = CW / N; const rh = 30; const top = 28; const H = top + R.length * rh + 8;
235
+ const X = (s) => LW + Math.max(0, Math.min(N, (utc(s) - a) / 864e5)) * dw; // the left edge of a day
236
+ const Xe = (s) => X(s) + dw; // the right edge of a day
237
+ const s = svg('svg', { class: 'pg', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Progress of commitments: planned and forecast bars, the promised date, and the status of each initiative' });
238
+ // the day ticks: every day for a short range, Mondays for up to nine weeks, the first of each month beyond that
239
+ for (let i = 0; i < N; i++) {
240
+ const t = new Date(a + i * 864e5); const w = t.getUTCDay(); const d = t.getUTCDate();
241
+ const lab = N <= 14 ? (DN[w] + ' ' + d) : N <= 62 ? (w === 1 ? String(d) : null) : (d === 1 ? MN[t.getUTCMonth()] : null);
242
+ if (lab) { s.appendChild(svg('line', { x1: LW + i * dw, x2: LW + i * dw, y1: 18, y2: H - 4, stroke: 'var(--border)' })); s.appendChild(text(LW + i * dw + 3, 14, lab)); }
243
+ }
244
+ const today = data.today;
245
+ if (today >= data.range.start && today <= data.range.end) {
246
+ const tx = X(today);
247
+ s.appendChild(svg('rect', { x: tx, y: 20, width: dw, height: H - 24, fill: 'var(--accw)', rx: 6 }));
248
+ s.appendChild(svg('line', { x1: tx + dw / 2, x2: tx + dw / 2, y1: 20, y2: H - 4, stroke: 'var(--foreground)', 'stroke-width': 2 }));
249
+ }
250
+ const clip = svg('clipPath', { id: 'cvc' + data.range.start }, svg('rect', { x: LW, y: 18, width: CW, height: H - 18 }));
251
+ s.appendChild(svg('defs', {}, clip));
252
+ R.forEach((g, i) => {
253
+ const y = top + i * rh; const cy = y + rh / 2; const k = STKEY[g.status] || 'none'; const S = ST[k];
254
+ const nm = (g.title || g.id || '').length > 17 ? (g.title || g.id).slice(0, 16) + '…' : (g.title || g.id || '');
255
+ const row = svg('g', { 'data-cv-row': g.id, 'data-status': g.status, ...(isPhoneR(r) ? {} : { role: 'button', tabindex: '0', 'data-a': 'goinit', 'data-k': g.id, 'aria-label': 'Open ' + (g.title || g.id) + ': ' + g.status }), style: isPhoneR(r) ? '' : 'cursor:pointer', opacity: k === 'none' ? '.6' : '1' });
256
+ row.appendChild(svg('rect', { x: 0, y, width: W, height: rh, fill: 'transparent' }));
257
+ const openIt = () => { if (!goInitiative(g.id)) openInit(host, r, g.id, g.title); }; // the mockup's goinit: the Initiatives tab with this initiative chosen
258
+ if (!isPhoneR(r)) row.addEventListener('click', openIt); if (!isPhoneR(r)) row.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openIt(); } });
259
+ row.appendChild(text(0, cy + 4, nm, { class: 't' }));
260
+ const bars = svg('g', { 'clip-path': `url(#cvc${data.range.start})` });
261
+ const has = (q) => q && (q.start || q.end);
262
+ if (has(g.planned)) { const x0 = X(g.planned.start || g.planned.end); const x1 = Xe(g.planned.end || g.planned.start); bars.appendChild(svg('rect', { x: x0, y: cy - 8, width: Math.max(2, x1 - x0), height: 6, rx: 3, fill: 'var(--ring)', 'fill-opacity': '.6' })); }
263
+ if (has(g.forecast)) { const x0 = X(g.forecast.start || g.forecast.end); const x1 = Xe(g.forecast.end || g.forecast.start); bars.appendChild(svg('rect', { x: x0, y: cy, width: Math.max(2, x1 - x0), height: 6, rx: 3, fill: S.c })); }
264
+ if (g.promised && g.promised >= data.range.start && g.promised <= data.range.end) {
265
+ const px = X(g.promised) + dw / 2;
266
+ bars.appendChild(svg('path', { d: `M${px} ${cy - 9} l7 9 -7 9 -7 -9z`, fill: 'var(--card)', stroke: S.c, 'stroke-width': 2 }, svg('title', {})));
267
+ bars.lastChild.firstChild.textContent = 'Promised ' + g.promised;
268
+ }
269
+ row.appendChild(bars);
270
+ row.appendChild(text(W - SW + 10, cy + 4, S.l + (g.slip_days > 0 ? ` +${g.slip_days} d` : ''), { style: 'fill:' + S.c + ';font-weight:500' }));
271
+ const t = svg('title', {}); t.textContent = `${g.title || g.id}: ${g.status}${g.slip_days > 0 ? ', ' + g.slip_days + ' days past the plan' : ''}`; row.appendChild(t);
272
+ s.appendChild(row);
273
+ });
274
+ return el('section', { class: 'card', 'data-cv': 'gantt' }, head, s,
275
+ isPhoneR(r) ? phoneRows(R, r, host) : null,
276
+ all.length > R.length ? el('div', { class: 'dim mono', style: small }, `+${all.length - R.length} more initiatives not drawn`) : null, nope,
277
+ el('div', { class: 'pgl' }, el('span', null, '▬ planned'), el('span', null, '▬ forecast, coloured by status'), el('span', null, '◇ promised'), el('span', null, '| today'),
278
+ el('span', { style: 'color:var(--success)' }, '■ on time'), el('span', { style: 'color:var(--warning)' }, '■ at risk'), el('span', { style: 'color:var(--destructive)' }, '■ delayed or overdue')));
279
+ }
280
+
281
+ /* ── the strategy lens for Quarter and Year: the mockup's direction card (dirCard, line 2958) and key result rows (krCard, line 2863) ── */
282
+
283
+ function strategy(data) {
284
+ const xrow = (row, detail) => { const xp = makeXp(el('div', { class: 'dim', style: 'padding:6px 0;font-size:var(--fs-2)' }, detail)); row.setAttribute('tabindex', '0'); row.style.cursor = 'pointer'; attachExpand(row, xp, { key: row.getAttribute('data-cv-kr') }); return el('div', { class: 'krw' }, row, xp); };
285
+ const s = data.strategy; if (!s) return null;
286
+ const pct = (v) => Math.round(v * 100) + '%';
287
+ const warns = s.deviations.filter((d) => d.early_warning).length;
288
+ // the dial is derived from the lens's own rules: a pivot signal, else an early warning, else (when something is scored) on course
289
+ const state = s.pivot.signal ? 'pivot' : warns ? 'adjust' : s.north_star.scored ? 'persevere' : null;
290
+ const D = [['persevere', 'Persevere', 'var(--success)'], ['adjust', 'Adjust', 'var(--warning)'], ['pivot', 'Pivot', 'var(--destructive)']];
291
+ const A = data.view === 'quarter';
292
+ const tc = state === 'pivot' ? 'var(--destructive)' : state === 'adjust' ? 'var(--warning)' : state === 'persevere' ? 'var(--success)' : 'var(--muted-foreground)';
293
+ const dir = el('article', { class: 'fc solo on dir', 'data-tag': 'x', 'data-cv': 'north-star', 'data-signal': String(!!s.pivot.signal), style: '--tc:' + tc },
294
+ el('div', { class: 'ff' },
295
+ el('div', null, el('span', { class: 'tg', style: '--tc:' + tc }, c3ic('target', 'sm'), A ? 'Quarter direction' : 'Year direction')),
296
+ el('div', null, el('h2', null, s.north_star.progress == null ? 'Not scored' : 'North star: ' + pct(s.north_star.progress) + ' of the way'),
297
+ el('div', { class: 'dim', style: 'margin-top:4px' }, `${s.north_star.scored} of ${s.north_star.total} key results scored`)),
298
+ el('div', { class: 'dial', role: 'img', 'aria-label': 'Direction: ' + (state || 'not scored') },
299
+ D.map((d) => el('span', { class: d[0] === state ? 'on' : '', style: '--c:' + d[2] }, el('i'), d[1]))),
300
+ el('div', { class: 'sevs' },
301
+ el('span', { style: 'color:var(--warning)' }, c3ic('clock', 'sm'), warns + ' early warning' + (warns === 1 ? '' : 's')),
302
+ el('span', null, c3ic('info', 'sm'), s.unscored.length + ' without a metric'),
303
+ s.pivot.signal ? el('span', { style: 'color:var(--destructive)' }, c3ic('alert', 'sm'), 'pivot signal') : null)));
304
+ const note = (t, cls, attrs) => el('div', { class: 'fold' + (cls ? ' ' + cls : ''), ...attrs }, c3ic(cls === 'w' ? 'alert' : 'info', 'sm'), t);
305
+ const rows = [
306
+ ...s.deviations.map((d) => {
307
+ const c = d.early_warning ? 'var(--warning)' : 'var(--success)';
308
+ return xrow(el('div', { class: 'krg', style: '--c:' + c, 'data-cv-kr': d.kr, 'data-early-warning': String(!!d.early_warning) },
309
+ el('div', null, el('b', null, d.kr), el('small', null, d.metric || '')),
310
+ el('div', null, el('div', { class: 'krt', title: 'How far behind the line' }, el('i', { style: 'width:' + Math.max(0, Math.min(100, Math.round(d.shortfall * 100))) + '%' })),
311
+ el('small', null, `expected ${d.expected} · actual ${d.actual} · behind the line by ${Math.round(d.shortfall * 100)}%`)),
312
+ el('div', { class: 'chip-s', style: '--c:' + c }, d.early_warning ? 'Early warning' : 'On the line')),
313
+ (d.early_warning ? 'An early warning: it is behind the expected line. ' : 'On the expected line. ') + 'Expected ' + d.expected + ', actual ' + d.actual + (d.metric ? ' (' + d.metric + ')' : '') + '. A pivot needs 3 checks behind the line and not closing; this is not one by itself.');
314
+ }),
315
+ ...s.unscored.map((u) => xrow(el('div', { class: 'krg', style: '--c:var(--muted-foreground)', 'data-cv-kr': u.kr, 'data-no-metric': 'true' },
316
+ el('div', null, el('b', null, u.kr), el('small', null, 'not scored')), el('div', null, el('small', null, 'No metric, baseline, target or date, so there is no line to compare.')),
317
+ el('div', { class: 'chip-s', style: '--c:var(--muted-foreground)' }, 'no metric')), 'Add a metric, a baseline, a target and a date to the key result in its file, and it is scored against a line.')),
318
+ ];
319
+ const kr = el('section', { class: 'card', 'data-cv': 'strategy' },
320
+ el('h3', null, c3ic('target'), 'Key results ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'where they are against the line')),
321
+ s.reason ? note(s.reason, '', { 'data-cv': 'lens-reason' }) : null,
322
+ rows.length ? el('div', null, rows) : null,
323
+ note(s.pivot.reason || '', s.pivot.signal ? 'w' : '', { 'data-cv': 'pivot', 'data-signal': String(!!s.pivot.signal) }),
324
+ el('div', { class: 'pgl' }, el('span', null, '▬ how far behind the expected line'), el('span', null, 'a pivot needs 3 checks behind the line and not closing')));
325
+ return el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, dir), el('div', { class: 'bcol r' }, kr)));
326
+ }
327
+
328
+
329
+ /* ── the ribbon (the Briefing's middle card): the mockup's weekStrip, quarterStrip, yearPane and fivePane (console3.html lines 1378 to 1434) ── */
330
+
331
+ const tspan = (n) => { const m = /T(\d{2}):(\d{2})/.exec(String(n || '')); return m ? Number(m[1]) + Number(m[2]) / 60 : null; };
332
+ const hoursOf = (c) => c.events.reduce((t, e) => { const a = tspan(e.at); const b = tspan(e.end); return a !== null && b !== null && b > a && e.kind !== 'milestone' ? t + (b - a) : t; }, 0);
333
+ /* the mockup's marker glyphs (MKS, line 1509): ship, promise, report, learn. Read from a milestone's title; `mock_week` (demo) names them. */
334
+ const MKS = { ship: ['◆', 'var(--acc)'], promise: ['◇', 'var(--foreground)'], report: ['▪', 'var(--muted-foreground)'], learn: ['○', 'var(--success)'], copy: ['▪', 'var(--muted-foreground)'] };
335
+ const markOf = (t) => (/release|freeze|ship/i.test(t) ? 'ship' : /promise|forecast/i.test(t) ? 'promise' : /status|update|report|review/i.test(t) ? 'report' : /interview|insight|learning|discovery|plan/i.test(t) ? 'learn' : null);
336
+ const hrsLinked = (c) => c.events.reduce((t, e) => { const a = tspan(e.at); const b = tspan(e.end); return a !== null && b !== null && b > a && e.kind !== 'milestone' && !e.mock_routine && !e.routine ? t + (b - a) : t; }, 0);
337
+ /* the next working days: from `mock_week` (demo) or from the calendar's own events (this week and the next) */
338
+ function weekDays(r) {
339
+ if (r.data && r.data.mock_week) return r.data.mock_week;
340
+ const today = (r.data && r.data.today) || todayStr();
341
+ return [...((r.data && r.data.cells) || []), ...((r.data2 && r.data2.cells) || [])].filter((c) => { const w = dowOf(c.start); return w !== 0 && w !== 6 && c.start >= today; }).slice(0, 7)
342
+ .map((c) => ({ date: c.start, d: DN[dowOf(c.start)] + ' ' + Number(c.start.slice(8)), v: Math.round(hoursOf(c) * 10) / 10, lk: Math.round(hrsLinked(c) * 10) / 10,
343
+ mk: c.events.filter((e) => e.kind === 'milestone').map((e) => [markOf(e.title), e.title]).filter((x) => x[0]) }));
344
+ }
345
+ /* the header of each pane (the mockup's lensInfo, line 2379): from `ribbon_heads` (demo), else worked out from what the server sent */
346
+ function paneHead(z, r, days) {
347
+ const d = r.data; const H = d.ribbon_heads && d.ribbon_heads[z];
348
+ if (H) return H;
349
+ if (z === 'week') {
350
+ if (!days.length) return null;
351
+ const tot = days.reduce((t, x) => t + x.v, 0); const lk = days.reduce((t, x) => t + x.lk, 0); const hv = days.reduce((m, x) => (x.v > m.v ? x : m), days[0]);
352
+ return { i: 'cal', h: hv.d.split(' ')[0] + ' is the heaviest day: ' + hv.v + ' h of meetings', s: Math.round(lk) + ' of ' + Math.round(tot) + ' h are tied to a promise or a decision. The rest is routine.', ring: [Math.round(lk), Math.max(1, Math.round(tot))] };
353
+ }
354
+ const rows = (d.gantt || []).filter((g) => g.status !== 'no plan'); const on = rows.filter((g) => g.status === 'on time').length;
355
+ return { i: 'init', h: rows.length ? on + ' of ' + rows.length + ' initiatives on time' : 'No initiative has a date in this range', s: 'Promised dates against forecast.', ring: rows.length ? [on, rows.length] : null };
356
+ }
357
+ const headNode = (H) => (H ? el('div', { class: 'lh' }, el('span', { class: 'ki' }, c3ic(H.i)), el('div', { style: 'min-width:0' }, el('b', null, H.h), el('div', { class: 'dim' }, H.s)), H.ring ? c3ring(H.ring[0], Math.max(1, H.ring[1]), 44) : null) : null);
358
+
359
+ /* the date of a day named by weekday and number (the demo's "Fri 9"), looked up in the fortnight from today */
360
+ function dateOfDay(today, label) { const n = Number(String(label).split(' ')[1]); for (let i = 0; i < 14; i++) { const t = shift('day', today, i); if (Number(t.slice(8)) === n) return t; } return today; }
361
+
362
+ function weekStrip(r, host) {
363
+ const days = weekDays(r);
364
+ if (!days.length) return el('p', { class: 'dim mono', style: small, 'data-cv': 'no-events' }, 'No calendar is cached yet, so there are no meeting hours to draw.');
365
+ const W = 380; const H = 122; const Y = (v) => 78 - v / 6 * 52;
366
+ const s = svg('svg', { class: 'chart', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Meeting hours for the next seven working days, split into time tied to a promise or decision and routine time', 'data-cv': 'week-strip' });
367
+ s.appendChild(svg('line', { x1: 6, x2: W - 6, y1: Y(6), y2: Y(6), stroke: 'var(--border)' })); s.appendChild(text(W - 6, Y(6) - 4, '6 h', { 'text-anchor': 'end' }));
368
+ days.forEach((x, i) => {
369
+ const cx = 30 + i * 53;
370
+ s.appendChild(svg('rect', { x: cx - 11, y: Y(x.v), width: 22, height: 78 - Y(x.v), rx: 4, fill: 'var(--ring)', opacity: '.55' }));
371
+ if (x.lk) s.appendChild(svg('rect', { x: cx - 11, y: Y(x.lk), width: 22, height: 78 - Y(x.lk), rx: 4, fill: 'var(--acc)' }));
372
+ s.appendChild(text(cx, Y(x.v) - 5, String(x.v), { 'text-anchor': 'middle', class: i === 0 ? 't' : '' })); s.appendChild(text(cx, 96, x.d, { 'text-anchor': 'middle', class: i === 0 ? 't' : '' }));
373
+ const dt = x.date || dateOfDay(r.data.today || todayStr(), x.d);
374
+ const hit = svg('rect', { class: 'hit', x: cx - 26, y: 16, width: 52, height: 104, role: 'button', tabindex: '0', 'data-a': 'calday', 'data-k': (x.d || '').split(' ')[1], 'aria-label': 'Open ' + x.d + ' in Calendar' });
375
+ const goDay = () => openDay(dt, null); hit.addEventListener('click', goDay); hit.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); goDay(); } });
376
+ (x.mk || []).forEach((m, k) => { const g = svg('g', {}); const gl = MKS[m[0]] || MKS.report; g.appendChild(text(cx + (k - (x.mk.length - 1) / 2) * 14, 113, gl[0], { 'text-anchor': 'middle', style: 'font-size:var(--fs-3);fill:' + gl[1] })); const t = svg('title', {}); t.textContent = m[1]; g.appendChild(t); s.appendChild(g); });
377
+ s.appendChild(hit);
378
+ });
379
+ return el('div', null, s, el('div', { class: 'legend' }, el('span', { style: 'color:var(--acc)' }, '■ tied to a promise or decision'), el('span', null, '■ routine'), el('span', null, '◆ release'), el('span', null, '◇ promise'), el('span', null, '▪ report')));
380
+ }
381
+
382
+ /* initiatives against their promised dates over the range the server answered (the quarter, or the year) */
383
+ function ganttStrip(data, label, r, host) {
384
+ const rows = (data.gantt || []).filter((g) => g.status !== 'no plan'); const noPlan = (data.gantt || []).length - rows.length;
385
+ if (!rows.length) return el('p', { class: 'dim mono', style: small }, 'No initiative has a date in this range.');
386
+ const a = utc(data.range.start); const N = Math.round((utc(data.range.end) - a) / 864e5) + 1;
387
+ const mm = data.mock_marks; // optional (demo): the mockup's leadership dates under the rows, { dates, label }
388
+ const W = 380; const x0 = 92; const x1 = 372; const X = (d) => x0 + Math.max(0, Math.min(N, (utc(d) - a) / 864e5)) / N * (x1 - x0); const rh = 30; const top = 24; const H = top + rows.length * rh + (mm ? 36 : 12);
389
+ const s = svg('svg', { class: 'chart', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': label + ': initiatives against promised dates' });
390
+ for (let i = 0; i < N; i++) { const t = new Date(a + i * 864e5); if (t.getUTCDate() === 1) { s.appendChild(svg('line', { x1: X(t.toISOString().slice(0, 10)), x2: X(t.toISOString().slice(0, 10)), y1: 16, y2: H - 8, stroke: 'var(--border)' })); s.appendChild(text(X(t.toISOString().slice(0, 10)) + 4, 11, MN[t.getUTCMonth()])); } }
391
+ const today = data.today;
392
+ rows.forEach((g, k) => {
393
+ const y = top + k * rh; const f = (g.forecast && (g.forecast.end || g.forecast.start)) || g.promised; const st = (g.planned && g.planned.start) || (g.forecast && g.forecast.start) || data.range.start;
394
+ const end = f && g.promised && f > g.promised ? f : (g.promised || f);
395
+ const nm = (g.title || g.id || ''); s.appendChild(text(0, y + 17, nm.length > 13 ? nm.slice(0, 12) + '…' : nm, { class: 't' }));
396
+ if (end) {
397
+ const cut = today >= st && today <= end ? today : end;
398
+ s.appendChild(svg('rect', { x: X(st), y: y + 7, width: Math.max(2, X(cut) - X(st)), height: 12, rx: 4, fill: 'var(--acc)' }));
399
+ if (cut < end) s.appendChild(svg('rect', { x: X(cut), y: y + 7, width: Math.max(0, X(end) - X(cut)), height: 12, rx: 4, fill: 'var(--accw)' }));
400
+ }
401
+ if (g.promised && f && f > g.promised) s.appendChild(svg('rect', { x: X(g.promised), y: y + 23, width: X(f) - X(g.promised), height: 3, rx: 1.5, fill: 'var(--destructive)' }));
402
+ if (g.promised) s.appendChild(svg('path', { d: `M${X(g.promised)} ${y + 2} l6 6 -6 6 -6 -6z`, fill: 'var(--background)', stroke: 'var(--foreground)', 'stroke-width': 2 }));
403
+ const hit = svg('rect', { class: 'hit', x: 0, y, width: W, height: rh, ...(isPhoneR(r) ? {} : { role: 'button', tabindex: '0', 'data-a': 'goinit', 'data-k': g.id, 'aria-label': 'Open ' + nm + ': ' + g.status }) }); const t = svg('title', {}); t.textContent = nm + ': ' + g.status; hit.appendChild(t); s.appendChild(hit);
404
+ const openIt = () => { if (!goInitiative(g.id)) openInit(host, r, g.id, nm); }; if (!isPhoneR(r)) hit.addEventListener('click', openIt); if (!isPhoneR(r)) hit.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openIt(); } });
405
+ });
406
+ if (mm) { mm.dates.forEach((d) => s.appendChild(text(X(d), H - 22, '▪', { 'text-anchor': 'middle', style: 'font-size:var(--fs-3);fill:var(--muted-foreground)' }))); s.appendChild(text((X(mm.dates[0]) + X(mm.dates[mm.dates.length - 1])) / 2, H - 10, mm.label, { 'text-anchor': 'middle' })); }
407
+ if (today >= data.range.start && today <= data.range.end) { s.appendChild(svg('line', { x1: X(today), x2: X(today), y1: 16, y2: H - 8, stroke: 'var(--foreground)', 'stroke-width': 2 })); s.appendChild(text(X(today), 11, 'today', { 'text-anchor': 'middle', class: 't' })); }
408
+ return el('div', null, s, isPhoneR(r) ? phoneRows(rows, r, host) : null, el('div', { class: 'legend' }, el('span', null, '◇ promised'), el('span', { style: 'color:var(--destructive)' }, '▬ slip'), mm ? el('span', null, '▪ leadership') : null), noPlan && !data.ribbon_heads ? el('div', { class: 'dim mono', style: small }, noPlan + ' initiative' + (noPlan === 1 ? ' has' : 's have') + ' no plan date, so no bar.') : null);
409
+ }
410
+
411
+ const RAGCLS = { bad: 'dotc bad', warn: 'dotc warn', ok: 'dotc ok', park: 'dotc' };
412
+ /* The mockup's yearPane: a row per pillar, a column per quarter. Drawn from data.mock_year (demo); the calendar route sends no pillars. */
413
+ function yearPane(m, r, host) {
414
+ const g = el('div', { style: 'display:grid;grid-template-columns:84px repeat(4,minmax(0,1fr));gap:6px;align-items:center' }, el('span'),
415
+ ...m.quarters.map((q) => el('span', { class: 'mono dim', style: 'font-size:var(--fs-1);text-align:center' }, q)));
416
+ m.pillars.forEach((p) => {
417
+ g.append(el('span', { class: 'chips2', style: 'font-size:var(--fs-2);font-weight:500' }, c3ic(p.icon, 'sm'), p.n));
418
+ p.cells.forEach((cell) => g.append(el('span', { style: 'display:flex;justify-content:center;flex-wrap:wrap;min-height:24px;align-items:center' },
419
+ ...cell.map(([n, rag]) => { if (rag === null) return el('span', { class: 'dotp', title: n }); const id = IX() && IX().LINK && IX().LINK[n]; const dot = el('span', { class: RAGCLS[rag] || 'dotc' });
420
+ return id ? el('button', { type: 'button', class: 'dotb', title: n, 'aria-label': 'Open ' + n, onClick: () => openInit(host, r, id, n) }, dot) : el('span', { class: 'dotb', title: n }, dot); }))));
421
+ });
422
+ return el('div', null, g, el('div', { class: 'legend' }, el('span', null, '● in flight, coloured by status'), el('span', null, '◌ planned')));
423
+ }
424
+ /* The mockup's fivePane: the weekly-actives cone and a pillar by year grid. Drawn from data.mock_five (demo). */
425
+ function fivePane(m) {
426
+ const W = 380; const H = 156; const x0 = 34; const x1 = 366; const X = (y) => x0 + (y - 2025) / 6 * (x1 - x0); const Y = (v) => 124 - v / 130 * 100; const f = (y) => Math.max(0, (y - 2026.8) * 0.07);
427
+ const tgt = [[2026.8, 12.4], [2027, 16], [2028, 35], [2029, 60], [2030, 90], [2031, 120]];
428
+ const up = tgt.map((p) => X(p[0]).toFixed(1) + ',' + Y(p[1] * (1 + f(p[0]))).toFixed(1)); const lo = tgt.slice().reverse().map((p) => X(p[0]).toFixed(1) + ',' + Y(p[1] * (1 - f(p[0]))).toFixed(1));
429
+ const s = svg('svg', { class: 'chart', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Weekly active accounts, 12.4k now, target 120k in 2031, with a widening range' });
430
+ [0, 60, 120].forEach((v) => { s.appendChild(svg('line', { x1: x0, x2: x1, y1: Y(v), y2: Y(v), stroke: 'var(--border)' })); s.appendChild(text(x0 - 5, Y(v) + 4, v + 'k', { 'text-anchor': 'end' })); });
431
+ [2025, 2027, 2029, 2031].forEach((y) => s.appendChild(text(X(y), 144, String(y), { 'text-anchor': 'middle' })));
432
+ s.appendChild(svg('polygon', { points: up.join(' ') + ' ' + lo.join(' '), fill: 'var(--accw)' }));
433
+ s.appendChild(svg('polyline', { fill: 'none', stroke: 'var(--acc)', 'stroke-opacity': '.55', 'stroke-width': 2, points: tgt.map((p) => X(p[0]) + ',' + Y(p[1])).join(' ') }));
434
+ s.appendChild(svg('polyline', { fill: 'none', stroke: 'var(--acc)', 'stroke-width': 2, 'stroke-linecap': 'round', points: [[2025, 9.5], [2026, 11.2], [2026.8, 12.4]].map((p) => X(p[0]) + ',' + Y(p[1])).join(' ') }));
435
+ s.appendChild(svg('circle', { cx: X(2026.8), cy: Y(12.4), r: 4, fill: 'var(--acc)', stroke: 'var(--card)', 'stroke-width': 2 })); s.appendChild(text(X(2026.8) - 6, Y(12.4) + 15, '12.4k', { 'text-anchor': 'end', class: 't' }));
436
+ s.appendChild(svg('circle', { cx: X(2031), cy: Y(120), r: 4, fill: 'var(--background)', stroke: 'var(--acc)', 'stroke-width': 2 })); s.appendChild(text(X(2031) - 8, Y(120) - 8, '120k', { 'text-anchor': 'end', class: 't' }));
437
+ const g = el('div', { style: 'display:grid;grid-template-columns:84px repeat(5,minmax(0,1fr));gap:4px 5px;align-items:center;margin-top:6px' }, el('span'), ...m.years.map((y) => el('span', { class: 'mono dim', style: 'font-size:var(--fs-1);text-align:center' }, y)));
438
+ m.pillars.forEach((p) => { g.append(el('span', { class: 'chips2', style: 'font-size:var(--fs-2);font-weight:500' }, c3ic(p.icon, 'sm'), p.n)); p.cols.forEach((c, i) => g.append(el('span', { class: 'yc c' + (i + 1 > 4 ? 4 : i + 1), title: c[1] }, c[0]))); });
439
+ return el('div', null, s, g, el('div', { class: 'legend' }, el('span', null, 'solid = in detail'), el('span', null, 'tint = planned'), el('span', null, 'outline = direction'), el('span', null, 'dashed = bet')));
440
+ }
441
+
442
+ function ribbonCard(host, r) {
443
+ const phone = r.root.classList.contains('ph');
444
+ let pane;
445
+ if (r.error) pane = el('p', { class: 'dim mono', style: small, role: 'status', 'data-cv': 'error' }, 'The calendar could not be read: ' + r.error);
446
+ else if (!r.data) pane = el('p', { class: 'dim mono', style: small, role: 'status', 'data-cv': 'loading' }, 'Reading the repository');
447
+ else {
448
+ const d = r.data;
449
+ if (r.zoom === 'today') pane = (d.cells || []).reduce((t, c) => t + c.events.length, 0) ? dayTimeline(d, true, r, host) : el('div', { class: 'rpane', 'data-z': 'today' }, lensHead(d), el('div', { class: 'fold', 'data-cv': 'no-events' }, c3ic('moon', 'sm'), d.events_source === 'none' ? 'No calendar is cached yet, so there are no meetings to show.' : 'Nothing is scheduled in this range.'));
450
+ else if (r.zoom === 'week') pane = el('div', { class: 'rpane', 'data-z': 'week' }, headNode(paneHead('week', r, weekDays(r))), weekStrip(r, host));
451
+ else if (r.zoom === 'quarter') pane = el('div', { class: 'rpane', 'data-z': 'quarter' }, headNode(paneHead('quarter', r)), ganttStrip(d, 'This quarter', r, host));
452
+ else if (r.zoom === 'year') pane = d.mock_year ? el('div', { class: 'rpane', 'data-z': 'year' }, headNode(paneHead('year', r)), yearPane(d.mock_year, r, host)) : el('div', { class: 'rpane', 'data-z': 'year' }, headNode(paneHead('year', r)), ganttStrip(d, 'This year', r, host));
453
+ else pane = d.mock_five ? el('div', { class: 'rpane', 'data-z': 'five' }, headNode(paneHead('five', r)), fivePane(d.mock_five))
454
+ : el('div', { class: 'rpane', 'data-z': 'five' }, el('p', { class: 'dim mono', style: small, 'data-cv': 'five-note' }, 'Five years has no calendar data yet, so the year is shown.'), ganttStrip(d, 'This year', r, host));
455
+ }
456
+ return el('section', { class: 'card glass ribbon', style: 'gap:8px', 'data-cv': 'ribbon' }, bar(host, r, phone), el('div', { class: 'calbody' + (r.anim && !r.loading ? ' ' + r.anim : '') }, pane));
457
+ }
458
+
459
+ /* ── the meeting card, the stage, the case sheets and the click-throughs (console3 wave C3) ──────────────────────────────────────────
460
+ * The mockup (console3.html: rdet line 2387, meetCard 2586, beforeView 1582, duringView 1586, afterExtra 1590, the click handler 2446 and 2640)
461
+ * selects a meeting from the timeline and redraws its card; a meeting card flips to a case file with the options; a day, a week block, a
462
+ * gantt row or a pulse row opens something. Here a press selects in place (no read), the options and the initiative case files open as the
463
+ * shared sheet (c3-sheet.js + c3-case.js), and a jump to another tab presses that tab's own button. Nothing is sent: an option that
464
+ * would tell a person is outward, so its slide stays disabled ("no channel connected"). */
465
+
466
+ const NO_CHANNEL = 'No channel is connected, so this would only stay a draft.';
467
+ const endH = (e) => { const s = timeOf(e.at); const t = timeOf(e.end); return t !== null && String(e.end).slice(0, 10) === String(e.at).slice(0, 10) && t > s ? t : s + 1; };
468
+ const nowH = () => { const d = new Date(); return d.getHours() + d.getMinutes() / 60; };
469
+ const dayEvents = (r) => { const c = r.data && r.data.cells && r.data.cells[0]; return c && r.data.unit === 'day' && r.data.range.start === r.data.range.end ? c.events.filter((e) => timeOf(e.at) !== null && e.kind !== 'milestone') : []; };
470
+
471
+ /** The meeting that is selected: the one pressed, else the one the page is waiting on, else the next that is not routine. */
472
+ function selEvent(r) {
473
+ const ev = dayEvents(r); if (!ev.length) return null;
474
+ const f = (r.sel && (ev.find((e) => e.id === r.sel) || ev.find((e) => baseId(e) === r.sel))) || ev.find((e) => e.mock_hot);
475
+ return f || ev.find((e) => !e.mock_routine && !e.routine && timeOf(e.at) >= nowH()) || ev[0];
476
+ }
477
+
478
+ const PRS = { ready: ['check', 'Prep ready', 'ok'], review: ['eye', 'Prep drafted, needs your look', 'warn'], blocked: ['alert', 'Blocked on your decision', 'bad'] };
479
+ const PRS_REAL = { ready: ['check', 'Prep done', 'ok'], review: ['eye', 'Prep in progress', 'warn'], blocked: ['alert', 'Prep at risk: not enough time left', 'bad'] };
480
+ const NOPREP = ['info', 'No prep is recorded for this meeting', ''];
481
+
482
+ function realCase(m, steps, done) {
483
+ return { id: 'meet:' + m.key, tc: 'var(--acc)', sit: m.title + ' at ' + m.time + '. ' + m.line, move: '',
484
+ chg: steps.map((t, i) => [i < done ? 'check' : 'clock', t, i < done ? 'prep done' : 'prep open', i < done ? 'ok' : 'warn']),
485
+ unsure: steps.length ? '' : 'The calendar holds no prep, purpose or notes for this meeting.' };
486
+ }
487
+
488
+ /** What the card shows for a meeting: the demo fixtures when they exist, else only what the calendar memory holds for the event. */
489
+ function meetModel(e) {
490
+ const ix = IX(); const fx = ix && ix.MEET && ix.MEET[baseId(e)];
491
+ const s = timeOf(e.at); const f = endH(e);
492
+ const m = { id: e.id, key: baseId(e) || e.id, title: e.title || e.id, time: hhmm(s) + ' to ' + hhmm(f), from: s };
493
+ if (fx) return { ...m, line: fx.line, why: fx.why, prep: fx.prep, att: fx.att, attN: fx.att.length, prepRows: fx.prepRows, case: fx.case, demo: true };
494
+ const steps = Array.isArray(e.prep) ? e.prep : []; const done = Math.min(Number(e.prep_done) || 0, steps.length);
495
+ const prep = !steps.length ? null : done >= steps.length ? 'ready' : e.ready === 'risk' ? 'blocked' : 'review';
496
+ const kind = e.kind && !/^(ready|prep ready|review prep|blocked.*)$/i.test(e.kind) ? e.kind : '';
497
+ const bits = [e.with ? 'With ' + e.with : '', e.open ? 'Open: ' + e.open : '', e.notes ? String(e.notes) : '', typeof e.attendees === 'number' ? e.attendees + ' attendees' : '', kind].filter(Boolean);
498
+ m.line = bits.join(' · ') || 'No purpose or notes are recorded for this meeting.';
499
+ return { ...m, prep, att: [], attN: typeof e.attendees === 'number' ? e.attendees : null, prepRows: steps.map((t, i) => ['spark', 'Step ' + (i + 1), t, i < done]), case: realCase(m, steps, done), demo: false };
500
+ }
501
+ const prState = (m) => (m.prep ? (m.demo ? PRS : PRS_REAL)[m.prep] : NOPREP);
502
+
503
+ /* the options sheet of a meeting or an initiative: the shared case file. `store` is the instance's record of what was confirmed. */
504
+ function showCase({ title, icon, model, meta, onDone, onGo, tail }) {
505
+ const raw = model.opts || [];
506
+ const node = renderCase({ ...model, outboxReason: NO_CHANNEL }, {
507
+ outbox: false,
508
+ onGo: (go) => { closeSheet(); if (onGo) onGo(go); },
509
+ onConfirm: ({ option }) => {
510
+ const o = raw.find((x) => x.id === option.id) || {};
511
+ closeSheet();
512
+ if (onDone) onDone(option, o);
513
+ toast({ text: o.msg || 'Done. Nothing outside changed.', timeout: 4200 });
514
+ },
515
+ });
516
+ return openSheet({ title, icon, body: [meta || null, node, tail || null], onClose: () => { if (node.c3case) node.c3case.dispose(); } });
517
+ }
518
+
519
+ function avs(m) {
520
+ return [...m.att.map((w) => el('span', { class: 'av' }, w)), !m.att.length && m.attN ? el('span', { class: 'chip2', title: m.attN + ' attendees' }, c3ic('user', 'sm'), String(m.attN)) : null];
521
+ }
522
+
523
+ function openMeetCase(host, r, e) {
524
+ const m = meetModel(e);
525
+ const real = !m.demo;
526
+ const tail = real ? el('div', { class: 'chips2', 'data-cv': 'case-off' },
527
+ el('button', { type: 'button', class: 'chip2', disabled: true, title: NO_CHANNEL }, c3ic('clock', 'sm'), 'Move it'),
528
+ el('button', { type: 'button', class: 'chip2', disabled: true, title: NO_CHANNEL }, c3ic('user', 'sm'), 'Ask the organiser')) : null;
529
+ showCase({ title: m.title, icon: 'cal', model: m.case, tail,
530
+ meta: el('div', { class: 'chips2 dim mono', style: small }, m.time, ...avs(m)),
531
+ onDone: (opt) => { r.done[m.key] = { id: opt.id, label: opt.label }; refreshSel(host, r); } });
532
+ }
533
+
534
+ function openInit(host, r, id, name) {
535
+ const ix = IX(); const g = ((r && r.data && r.data.gantt) || []).find((x) => x.id === id) || null;
536
+ const title = (g && g.title) || name || id;
537
+ let model = ix && ix.INIT && ix.INIT[id];
538
+ if (!model) {
539
+ const f = g && g.forecast && (g.forecast.end || g.forecast.start); const bits = [];
540
+ if (g) { if (g.promised) bits.push(['target', 'Promised ' + g.promised, 'Plan']); if (f) bits.push(['clock', 'Forecast ' + f + (g.slip_days > 0 ? ' (+' + g.slip_days + ' days)' : ''), 'Forecast', g.slip_days > 0 ? 'warn' : 'ok']); if (g.planned && (g.planned.start || g.planned.end)) bits.push(['cal', 'Planned ' + (g.planned.start || '') + ' to ' + (g.planned.end || ''), 'Plan']); }
541
+ model = { id: 'init:' + id, sit: g ? (STKEY[g.status] ? ST[STKEY[g.status]].l : g.status) + '. ' + (bits.length ? 'Promised against forecast, from the repository.' : 'No dates are recorded for this initiative.') : 'The initiative is not in this range.', chg: bits, go: { action: 'tab', key: 'init', label: 'Open Initiatives' } };
542
+ } else model = { ...model, id: 'init:' + id };
543
+ showCase({ title, icon: 'init', model, onGo: (go) => { if (go && go.key) gotoTab(go.key); },
544
+ onDone: (opt) => { if (r) { r.done['init:' + id] = { id: opt.id, label: opt.label }; } } });
545
+ }
546
+
547
+ /* the selected meeting's card under the timeline: the mockup's rdet */
548
+ function rdetNode(host, r, e) {
549
+ const m = meetModel(e); const d = r.done[m.key];
550
+ const pr = d ? ['check', 'Decided: ' + d.label, 'ok'] : prState(m);
551
+ const open = () => openMeetCase(host, r, e);
552
+ const status = pr[2] === 'ok' || !pr[2] ? el('span', { class: 'st ' + pr[2], style: 'font-weight:400' }, pr[1])
553
+ : el('button', { type: 'button', class: 'st ' + pr[2] + ' lk', style: 'font-weight:400;border:0;background:none;padding:0;text-decoration:underline;cursor:pointer;text-align:left', title: 'Open the options: decide, ask or move', 'data-cv': 'decide', onClick: open }, pr[1]);
554
+ return el('div', { class: 'rdet', 'data-cv': 'selected', 'data-cv-sel': m.key },
555
+ el('div', { class: 'chips2', style: 'justify-content:space-between' }, el('b', { style: 'font-size:var(--fs-3)' }, m.title), el('span', { class: 'mono dim', style: small }, m.time)),
556
+ el('div', { class: 'rr' }, c3ic('target', 'sm'), el('span', null, m.line)),
557
+ el('div', { class: 'rr' }, c3ic(pr[0], 'sm'), status,
558
+ el('span', { class: 'chips2', style: 'margin-left:auto' }, ...avs(m),
559
+ el('button', { type: 'button', class: 'chip2 lk', 'data-cv': 'options', title: 'Options: decide, ask or move', 'aria-label': 'Options for ' + m.title, onClick: open }, c3ic('scale', 'sm')),
560
+ r.ctx && r.ctx.ribbon ? el('button', { type: 'button', class: 'chip2 lk', 'data-cv': 'to-cal', title: 'Open in Calendar', 'aria-label': 'Open ' + m.title + ' in Calendar', onClick: () => openDay(todayOf(r), baseId(e) || e.id) }, c3ic('next', 'sm')) : null)));
561
+ }
562
+ const todayOf = (r) => (r.data && r.data.today) || todayStr();
563
+
564
+ /* a day (and a meeting) on the Calendar tab's own instance; if that tab has not mounted yet its instance reads PENDING when it does */
565
+ let PENDING = null;
566
+ function openDay(date, sel) {
567
+ const cal = [...live].find((x) => x.ctx && x.ctx.plan);
568
+ if (cal) { cal.sel = sel || null; cal.phase = null; go(cal.host, cal, { view: 'day', date, anim: 'zi' }); }
569
+ else PENDING = { date, sel: sel || null };
570
+ gotoTab('cal');
571
+ }
572
+
573
+ /* the selection moves without a read: the classes, the card and the stage are redrawn from the data in hand */
574
+ function selectMeeting(host, r, id) { r.sel = id; r.phase = null; refreshSel(host, r); }
575
+ function refreshSel(host, r) {
576
+ const e = selEvent(r); if (!e) return;
577
+ r.root.querySelectorAll('.vb[data-cv-cell]').forEach((b) => { const on = b.dataset.cvCell === e.id; b.classList.toggle('sel', on); b.setAttribute('aria-pressed', String(on)); });
578
+ const old = r.root.querySelector('[data-cv="selected"]'); if (old) old.replaceWith(rdetNode(host, r, e));
579
+ const st = r.root.querySelector('[data-cv="stage"]'); if (st) { const n = stageNode(host, r); if (n) st.replaceWith(n); }
580
+ }
581
+
582
+ /* the hover scrub line of the day timeline (mockup line 2176): the time under the pointer and what is there */
583
+ function scrubWire(v, timed, end, t0, pph) {
584
+ const sc = el('div', { class: 'vsc' }, el('span')); v.appendChild(sc);
585
+ const label = (t) => {
586
+ const cur = timed.find((e) => t >= timeOf(e.at) && t < end(e));
587
+ if (cur) return hhmm(t) + ' · ' + (cur.title || cur.id);
588
+ const nx = timed.filter((e) => timeOf(e.at) > t).sort((a, b) => timeOf(a.at) - timeOf(b.at))[0];
589
+ return hhmm(t) + ' · free' + (nx ? ' for ' + Math.round((timeOf(nx.at) - t) * 60) + ' min' : '');
590
+ };
591
+ v.addEventListener('pointermove', (e) => { if (e.pointerType === 'touch') return; const rc = v.getBoundingClientRect(); const y = Math.max(0, Math.min(rc.height, e.clientY - rc.top)); sc.style.top = y + 'px'; sc.firstChild.textContent = label(t0 + y / pph); sc.style.opacity = '1'; });
592
+ v.addEventListener('pointerleave', () => { sc.style.opacity = '0'; });
593
+ }
594
+
595
+ /* ── the stage under a day's cards on the Calendar tab: the mockup's Before, During and After (stageHtml line 2598) ── */
596
+
597
+ const autoPhase = (e) => (endH(e) <= nowH() ? 'after' : timeOf(e.at) <= nowH() ? 'during' : 'before');
598
+ const NOREC = 'Nothing on a page can record. Recording happens in the main app\'s note dock.';
599
+ const IX2 = () => (typeof window !== 'undefined' && window.__c3cal2) || null; // c3-demo-cal2.js (demo only): the After notes, the Week grid, the progress rows
600
+ const simOn = () => simState().mode !== 'idle';
601
+ const noRecNode = () => el('div', { class: 'fold', 'data-cv': 'norec' }, c3ic('mic', 'sm'), NOREC + ' ', el('a', { href: '/', target: '_blank', rel: 'noopener', 'data-cv': 'main-app' }, 'Open the main app'));
602
+
603
+ /* the Record button of the During and After stages (the mockup's data-a="nlive"). In demo it drives the simulated recorder, which the top bar's
604
+ * recording group shows (shell-recording.js). In real mode it is disabled: a page records nothing, so it says where recording happens. */
605
+ function recordBtn(m) {
606
+ const demo = isDemo(); const on = demo && simOn();
607
+ return el('button', { type: 'button', class: 'ib pri', style: 'width:auto;padding:0 14px;gap:6px;display:inline-flex;align-items:center', 'data-cv': 'record', disabled: !demo,
608
+ title: demo ? (on ? 'Stop and write final notes' : 'Record (a simulation: the demo records nothing)') : NOREC + ' Use the link below.',
609
+ onClick: () => { if (simOn()) simDock.stopNow(); else simStart(m.title); } }, c3ic(on ? 'stop' : 'mic'), on ? 'Stop' : 'Record');
610
+ }
611
+ const liveBody = () => [el('div', { class: 'wave', 'aria-hidden': 'true' }, ...new Array(28).fill(0).map(() => el('i'))),
612
+ el('div', { class: 'term', 'data-cv': 'live-text' }, (IX2() && IX2().NOTES.live) || 'Dara: the keys are the real blocker, not the scope.\nYou: then we cut to three flows and keep 24 Oct.\n[highlight] decision: cut to three flows\n[caught] action: Dara confirms sandbox keys'),
613
+ el('div', { class: 'chips2 dim mono', style: 'font-size:var(--fs-2)' }, c3ic('lock', 'sm'), 'transcribed on this machine' + (simState().mode === 'paused' ? ' · paused' : ''))];
614
+ const avN = (n) => el('span', { class: 'av', title: n }, String(n)[0]);
615
+
616
+ /* the After stage's notes card: the mockup's notesView (line 1494). Decisions, actions with owner and due, routing, and who was told what. */
617
+ function notesCard(host, r, m) {
618
+ const N = isDemo() && IX2() && IX2().NOTES; const redraw = () => refreshSel(host, r);
619
+ const head = el('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:10px' }, el('h3', null, c3ic('notes'), 'Notes'), recordBtn(m));
620
+ if (isDemo() && simOn()) return el('section', { class: 'card', 'data-cv': 'notes' }, head, ...liveBody());
621
+ if (!N) return el('section', { class: 'card', 'data-cv': 'notes' }, head, el('div', { class: 'fold', 'data-cv': 'no-notes' }, c3ic('moon', 'sm'), 'No notes, decisions or actions are held for this meeting. This page reads the calendar and records nothing.'), isDemo() ? null : noRecNode());
622
+ const nt = r.nt || (r.nt = { owners: {}, due: {}, logged: {}, diff: false, sent: false });
623
+ const sel = (label, opts, val, on) => el('select', { 'aria-label': label, onChange: (e) => on(e.target.value) }, ...opts.map((x) => el('option', { value: x, selected: x === val }, x || 'No owner')));
624
+ const small2 = 'font-size:var(--fs-2)';
625
+ return el('section', { class: 'card', 'data-cv': 'notes' }, head,
626
+ el('div', { class: 'chips2' }, el('b', null, N.title), el('span', { class: 'dim mono', style: small2 }, N.when), ...N.who.map(avN)),
627
+ nt.diff ? null : el('div', { class: 'fold w', 'data-cv': 'diff' }, c3ic('alert', 'sm'), N.diff + ' ', el('button', { type: 'button', class: 'link', style: 'background:none;border:0;color:inherit;text-decoration:underline', onClick: () => { nt.diff = true; toast({ text: 'Settled. Nothing outside changed.', timeout: 4200 }); redraw(); } }, 'Settle')),
628
+ el('table', { class: 'tbl', 'data-cv': 'decisions' }, el('thead', null, el('tr', null, el('th', null, 'Decision'), el('th', null, 'Rejected'), el('th'))),
629
+ el('tbody', null, ...N.decisions.map((d) => el('tr', null, el('td', null, d.t), el('td', { class: 'dim' }, d.rej), el('td', { style: 'text-align:right' },
630
+ nt.logged[d.id] ? el('span', { class: 'st ok' }, c3ic('check', 'sm'), 'logged')
631
+ : el('button', { type: 'button', class: 'ib sm', 'data-cv': 'log', title: 'Log in the Checkout v2 decision log', 'aria-label': 'Log in the Checkout v2 decision log: ' + d.t, onClick: () => { nt.logged[d.id] = 1; toast({ text: 'Logged in Checkout v2. Demo: nothing was saved.', timeout: 4200 }); redraw(); } }, c3ic('scale', 'sm'))))))),
632
+ el('table', { class: 'tbl', 'data-cv': 'actions' }, el('thead', null, el('tr', null, el('th', null, 'Action'), el('th', null, 'Owner'), el('th', null, 'Due'))),
633
+ el('tbody', null, ...N.actions.map((a) => el('tr', null, el('td', null, a.t),
634
+ el('td', null, sel('Owner', N.owners, nt.owners[a.id] !== undefined ? nt.owners[a.id] : a.o, (v) => { nt.owners[a.id] = v; })),
635
+ el('td', null, sel('Due', N.dues, nt.due[a.id] || a.d, (v) => { nt.due[a.id] = v; })))))),
636
+ el('div', { class: 'chips2' },
637
+ el('button', { type: 'button', class: 'ib pri', style: 'width:auto;padding:0 14px;gap:6px;display:inline-flex;align-items:center', 'data-cv': 'route', disabled: nt.sent,
638
+ title: 'Yours go to the Inbox. Others go to To-Do, Waiting For, and show on their card. Demo: nothing is saved.', onClick: () => { nt.sent = true; toast({ text: N.actions.length + ' routed to Work · 0 already existed. Demo: nothing was saved.', timeout: 4200 }); redraw(); } }, c3ic('out'), nt.sent ? 'Routed' : 'Route actions'),
639
+ el('span', { class: 'dim mono', style: small2, title: 'Yours go to the Inbox. Others go to To-Do, Waiting For, and show on their card.' }, c3ic('info', 'sm'), 'yours to Inbox · theirs to Waiting For')),
640
+ el('div', { class: 'chips2' }, el('span', { class: 'dim mono', style: small2 }, 'told'), ...N.told.map(avN), el('span', { class: 'dim mono', style: small2 }, 'not told'), ...N.notTold.map(avN),
641
+ el('span', { class: 'st warn', title: 'The 24 Oct date is now conditional' }, c3ic('alert', 'sm'), N.flag)));
642
+ }
643
+
644
+ /* Before: "Read first" and "People" draw as the mockup's chips (a file chip, an avatar with what that person owes), when the demo row has that shape */
645
+ function prepDetail(x, m) {
646
+ if (!m.demo) return x[2];
647
+ if (x[1] === 'Read first' && /\.\w{2,4}( · |$)/.test(x[2])) return el('span', { class: 'chips2', style: 'display:inline-flex;flex-wrap:wrap' }, ...x[2].split(' · ').map((f) => el('span', { class: 'chip2' }, c3ic('doc', 'sm'), f)));
648
+ if (x[1] === 'People' && (m.att || []).length) {
649
+ const owe = {}; String(x[2]).split(' · ').forEach((t) => { const k = /^(\w+) owes (\d+)$/.exec(t); if (k) owe[k[1][0]] = k[2]; });
650
+ return el('span', { class: 'chips2', style: 'display:inline-flex;flex-wrap:wrap' }, ...m.att.map((a) => el('span', { class: 'chips2', style: 'display:inline-flex' }, el('span', { class: 'av' }, a), owe[a] ? el('span', { class: 'dim mono', style: 'font-size:var(--fs-1)' }, 'owe ' + owe[a]) : null)));
651
+ }
652
+ return x[2];
653
+ }
654
+
655
+ function stageNode(host, r) {
656
+ const e = selEvent(r); if (!e) return null;
657
+ const m = meetModel(e); const ph = r.phase || autoPhase(e); const key = m.key;
658
+ const rd = r.ready || (r.ready = {});
659
+ const head = el('section', { class: 'card', 'data-cv': 'stage-head' },
660
+ el('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap' },
661
+ el('span', { class: 'chips2' }, el('b', null, m.title), el('span', { class: 'mono dim', style: small }, hhmm(timeOf(e.at)))),
662
+ el('div', { class: 'stg', role: 'group', 'aria-label': 'Meeting stage', 'data-cv': 'phase' },
663
+ [['before', 'clip', 'Before'], ['during', 'mic', 'During'], ['after', 'notes', 'After']].map(([k, ic, l]) =>
664
+ el('button', { type: 'button', class: 'sgb', 'data-cv-phase': k, 'data-a': 'phase', 'data-k': k, 'aria-pressed': String(ph === k), title: l, onClick: () => { r.phase = k; refreshSel(host, r); } }, c3ic(ic), el('span', null, l))))),
665
+ el('div', { class: 'chips2 dim', style: small }, c3ic(prState(m)[0], 'sm'), prState(m)[1], el('button', { type: 'button', class: 'chip2 lk', 'data-cv': 'options', onClick: () => openMeetCase(host, r, e) }, c3ic('scale', 'sm'), 'Decide, ask or move')));
666
+ let body;
667
+ if (ph === 'before') {
668
+ const rows = m.prepRows;
669
+ body = el('section', { class: 'card', 'data-cv': 'stage-before' }, el('h3', null, c3ic('spark'), 'Prepared for you ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'automatically, 24 h before')),
670
+ rows.length ? rows.map((x, i) => {
671
+ const ok = x[3] || !!rd[key + ':' + i];
672
+ return el('div', { class: 'chips2', style: 'align-items:flex-start;padding:6px 0;border-bottom:1px solid var(--border)' },
673
+ el('span', { class: 'ki', style: 'flex:none;' + (ok ? '' : 'background:var(--accw);color:var(--acc)') }, c3ic(x[0])),
674
+ el('div', { style: 'flex:1;min-width:0' }, el('b', { style: 'font-size:var(--fs-3)' }, x[1]), el('div', { class: 'dim', style: 'font-size:var(--fs-3)' }, prepDetail(x, m))),
675
+ ok ? el('span', { class: 'st ok', title: 'Prepared' }, c3ic('check', 'sm'))
676
+ : el('button', { type: 'button', class: 'ib pri sm', 'data-cv': 'prepready', 'data-a': 'prepready', 'data-k': key, 'aria-label': 'Mark ready: ' + x[1], disabled: !m.demo, title: m.demo ? 'Mark the pre-read ready' : 'Marking a step done needs the calendar memory to take a write. No page route does that yet.',
677
+ onClick: () => { rd[key + ':' + i] = true; toast({ text: 'Pre-read marked ready. Send it when you are ready.', timeout: 4200 }); refreshSel(host, r); } }, c3ic('check', 'sm')));
678
+ }) : el('div', { class: 'fold' }, c3ic('info', 'sm'), 'No prep is recorded for this meeting. Add steps to its calendar entry and they show here.'));
679
+ } else if (ph === 'during') {
680
+ const sim = isDemo() && simOn();
681
+ body = el('section', { class: 'card', 'data-cv': 'stage-during' },
682
+ el('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:10px' }, el('h3', null, c3ic('mic'), 'Live notes'), recordBtn(m)),
683
+ sim ? liveBody() : isDemo() ? el('div', { class: 'fold' }, c3ic('mic', 'sm'), 'press Record. Notes stay on this machine.') : noRecNode());
684
+ } else {
685
+ const D = m.demo ? [['Note to Priya: 24 Oct is conditional', 'out'], ['Note to design: scope decision', 'out']] : [];
686
+ const drafts = el('section', { class: 'card', 'data-cv': 'stage-after' }, el('h3', null, c3ic('spark'), 'Drafted after the meeting'),
687
+ D.length ? D.map((d, i) => { const k = key + ':fu' + i; return el('div', { class: 'chips2', style: 'justify-content:space-between' }, el('span', { class: 'chips2' }, c3ic(d[1], 'sm'), d[0]),
688
+ rd[k] ? el('span', { class: 'st ok' }, c3ic('check', 'sm'), 'ready') : el('button', { type: 'button', class: 'ib pri sm', 'aria-label': 'Mark ready: ' + d[0], title: 'Mark ready. Nothing is sent.', onClick: () => { rd[k] = true; toast({ text: 'Marked ready. Nothing was sent.', timeout: 4200 }); refreshSel(host, r); } }, c3ic('check', 'sm'))); })
689
+ : el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'Nothing is drafted for this meeting yet. Notes taken during it are summarised here.'));
690
+ body = el('div', { style: 'display:grid;gap:var(--sp-3)', 'data-cv': 'after' }, notesCard(host, r, m), drafts);
691
+ }
692
+ return el('div', { 'data-cv': 'stage', style: 'display:grid;gap:var(--sp-3)' }, head, body);
693
+ }
694
+
695
+ /* ── the page-level hooks: the pulse rows live in the shell (c3-shell.js, c3-plan.js) and are not edited there; they get their press from here ── */
696
+
697
+ const PULSEK = { red: 'bad', delayed: 'bad', overdue: 'bad', amber: 'warn', 'at risk': 'warn', green: 'ok', 'on time': 'ok', park: '', 'no plan': '' };
698
+ let pulseOn = false;
699
+ function pulseA11y(root) {
700
+ root.querySelectorAll('[data-c3pg-prow]').forEach((p) => { if (!p.hasAttribute('role')) { p.setAttribute('role', 'button'); p.tabIndex = 0; p.title = 'Open the initiative'; } });
701
+ root.querySelectorAll('.pbs > span').forEach((s) => { if (!s.hasAttribute('role')) { s.setAttribute('role', 'button'); s.tabIndex = 0; s.setAttribute('aria-pressed', 'false'); } });
702
+ }
703
+ function pulseFilter(seg) {
704
+ const bar = seg.parentElement; const word = (seg.getAttribute('title') || '').replace(/^\d+\s*/, '').toLowerCase(); const want = PULSEK[word];
705
+ const on = seg.getAttribute('aria-pressed') !== 'true';
706
+ bar.querySelectorAll(':scope > span').forEach((s) => { s.setAttribute('aria-pressed', String(on && s === seg)); s.style.opacity = on && s !== seg ? '.28' : ''; });
707
+ const box = bar.closest('[data-c3pg="pulse"]');
708
+ box.querySelectorAll('[data-c3pg-prow]').forEach((p) => { const d = p.querySelector('.dotc'); const k = d ? (d.classList.contains('bad') ? 'bad' : d.classList.contains('warn') ? 'warn' : d.classList.contains('ok') ? 'ok' : '') : ''; p.hidden = on && want !== undefined && k !== want; });
709
+ }
710
+ function installPulse(host) {
711
+ const parent = host.parentElement; if (parent) { pulseA11y(parent); new MutationObserver(() => pulseA11y(parent)).observe(parent, { childList: true, subtree: true }); }
712
+ if (pulseOn) return; pulseOn = true;
713
+ const act = (t) => {
714
+ const row = t.closest && t.closest('[data-c3pg-prow]'); const seg = t.closest && t.closest('[data-c3pg="pulse"] .pbs > span');
715
+ if (row) { const r = [...live].find((x) => x.ctx && x.ctx.ribbon && x.data) || [...live].find((x) => x.data) || null; openInit(r && r.host, r, row.getAttribute('data-c3pg-prow'), (row.querySelector('b') || {}).textContent); return true; }
716
+ if (seg) { pulseFilter(seg); return true; }
717
+ return false;
718
+ };
719
+ document.addEventListener('click', (e) => { if (act(e.target)) e.preventDefault(); });
720
+ document.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target.matches && e.target.matches('[data-c3pg-prow],[data-c3pg="pulse"] .pbs > span') && act(e.target)) e.preventDefault(); });
721
+ }
722
+
723
+ /* ── the Week page: "Progress and timing" and the time grid (console3 mockup progSVG line 2716, progCard 2739, weekView2 2604, weekListPh 2620) ── */
724
+
725
+ const ST2 = { ...ST, done: { l: 'Done', c: 'var(--success)', i: 'check' }, park: { l: 'Parked', c: 'var(--muted-foreground)', i: 'moon' } };
726
+ const SEV = { over: 0, late: 1, risk: 2, ok: 3, park: 4, done: 5 };
727
+ const ROUTINE_RE = /^(Standup|1:1|Sync$)/i;
728
+ const dlab = (s) => { const [, m, d] = ymd(s); return d + ' ' + MN[m - 1]; };
729
+
730
+ /** The commitments in a range: the initiatives' promised dates against forecast (from the server's gantt rows), and, in demo, the mockup's dated tasks. */
731
+ function commitRows(data) {
732
+ const c2 = IX2(); const a = utc(data.range.start); const dn = (s) => Math.round((utc(s) - a) / 864e5); const rows = [];
733
+ const today = data.today || todayStr();
734
+ for (const g of data.gantt || []) {
735
+ let k = (c2 && c2.STAT && c2.STAT[g.id]) || STKEY[g.status]; if (!k || k === 'none') continue;
736
+ const due = g.promised || (g.planned && g.planned.end) || (g.forecast && (g.forecast.end || g.forecast.start)); if (!due) continue;
737
+ const start = (g.planned && g.planned.start) || (g.forecast && g.forecast.start) || due; const fc = (g.forecast && (g.forecast.end || g.forecast.start)) || due;
738
+ const why = (g.pulse && g.pulse.why) || (g.title || g.id) + ': ' + g.status;
739
+ rows.push({ id: g.id, n: g.title || g.id, kind: 'init', start: dn(start), due: dn(due), fc: dn(fc), dueS: due, fcS: fc, pct: c2 && c2.PCT && c2.PCT[g.id] != null ? c2.PCT[g.id] : null, st: k, why, late: g.slip_days });
740
+ }
741
+ if (c2) for (const t of c2.TASKS || []) rows.push({ id: t.id, n: t.n, kind: 'task', due: dn(t.due), dueS: t.due, st: t.st, why: t.why, late: t.late });
742
+ rows.todayN = dn(today);
743
+ return rows;
744
+ }
745
+ const statTxt = (c, todayN) => (c.st === 'over' ? 'Overdue ' + (c.late || todayN - c.due) + ' d' : c.st === 'late' ? 'Delayed +' + (c.fc - c.due) + ' d' : ST2[c.st].l);
746
+
747
+ /* "Progress and timing": a row per commitment, its bar from start to promise, how far it is, where it should be today, and the forecast past the promise */
748
+ function progressCard(data, r, host) {
749
+ const week = r.view === 'week'; const a = 0; const N = week ? 5 : Math.round((utc(data.range.end) - utc(data.range.start)) / 864e5) + 1; const b = N - 1;
750
+ const all = commitRows(data); const td = all.todayN;
751
+ const R = all.filter((c) => { const lo = c.kind === 'init' ? c.start : c.due; const hi = Math.max(c.due, c.fc || c.due); if (c.st === 'over' && b >= td && a <= td) return true; return hi >= a && lo <= b; })
752
+ .sort((x, y) => SEV[x.st] - SEV[y.st] || x.due - y.due).slice(0, week ? 9 : 10);
753
+ const head = el('h3', null, c3ic('target'), 'Progress and timing ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'when things land, and whether they will'));
754
+ if (!R.length) return el('section', { class: 'card', 'data-cv': 'progress' }, head, el('div', { class: 'fold' }, c3ic('moon', 'sm'), data.events_source === 'none' || !(data.gantt || []).length ? 'No initiative or task has a date in this range.' : 'Nothing falls due in this range, and no initiative runs through it.'));
755
+ const W = 720; const LW = 150; const SW = 104; const CW = W - LW - SW; const dw = CW / N; const rh = 30; const top = 28; const H = top + R.length * rh + 8;
756
+ const Xl = (n) => LW + (n - a) * dw; const Xc = (n) => Xl(n) + dw / 2;
757
+ const s = svg('svg', { class: 'pg', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Progress of commitments: on time, at risk, delayed and overdue' });
758
+ const clipId = 'pgc' + r.view + data.range.start;
759
+ s.appendChild(svg('defs', {}, svg('clipPath', { id: clipId }, svg('rect', { x: LW, y: 18, width: CW, height: H - 18 }))));
760
+ for (let n = a; n <= b; n++) {
761
+ const t = new Date(utc(data.range.start) + n * 864e5); const w = t.getUTCDay(); const d = t.getUTCDate();
762
+ if (week) { s.appendChild(text(Xc(n), 14, DN[w] + ' ' + d, { 'text-anchor': 'middle', class: n === td ? 't' : '' })); if (n > a) s.appendChild(svg('line', { x1: Xl(n), x2: Xl(n), y1: 20, y2: H - 4, stroke: 'var(--border)' })); }
763
+ else if (w === 1) { s.appendChild(svg('line', { x1: Xl(n), x2: Xl(n), y1: 18, y2: H - 4, stroke: 'var(--border)' })); s.appendChild(text(Xl(n) + 3, 14, String(d))); }
764
+ }
765
+ if (td >= a && td <= b) {
766
+ const now = new Date(); const tx = Xl(td) + dw * Math.max(0, Math.min(1, (now.getHours() + now.getMinutes() / 60 - 8) / 10));
767
+ s.appendChild(svg('rect', { x: Xl(td), y: 20, width: dw, height: H - 24, fill: 'var(--accw)', rx: 6 })); s.appendChild(svg('line', { x1: tx, x2: tx, y1: 20, y2: H - 4, stroke: 'var(--foreground)', 'stroke-width': 2 }));
768
+ }
769
+ let anyPct = false;
770
+ R.forEach((c, i) => {
771
+ const y = top + i * rh; const cy = y + rh / 2; const S = ST2[c.st]; const nm = c.n.length > 17 ? c.n.slice(0, 16) + '…' : c.n; const dim = c.st === 'done' || c.st === 'park';
772
+ const go = c.kind === 'init' && !isPhoneR(r);
773
+ const g = svg('g', { opacity: dim ? '.6' : '1', class: 'pgrow', 'data-cv-prog': c.id, 'data-status': c.st, ...(go ? { role: 'button', tabindex: '0', 'data-a': 'goinit', 'data-k': c.id, 'aria-label': 'Open ' + c.n + ': ' + statTxt(c, td) } : {}), ...(go ? { style: 'cursor:pointer' } : {}) });
774
+ g.appendChild(text(0, cy + 4, nm, { class: 't' }));
775
+ if (c.kind === 'init') {
776
+ const xs = Xl(c.start); const xe = Xc(c.due); const ex = Math.max(0, Math.min(1, (td - c.start) / Math.max(1, c.due - c.start))); const xt = xs + (xe - xs) * ex;
777
+ const have = c.pct != null; if (have) anyPct = true; const xf = xs + (xe - xs) * (have ? c.pct : 0);
778
+ const clip = svg('g', { 'clip-path': `url(#${clipId})` });
779
+ clip.appendChild(svg('rect', { x: xs, y: cy - 5, width: Math.max(2, xe - xs), height: 10, rx: 5, fill: S.c, 'fill-opacity': have ? '.2' : '.35' }));
780
+ if (have) clip.appendChild(svg('rect', { x: xs, y: cy - 5, width: Math.max(2, xf - xs), height: 10, rx: 5, fill: S.c }));
781
+ if (c.st !== 'park') clip.appendChild(svg('line', { x1: xt, x2: xt, y1: cy - 9, y2: cy + 9, stroke: 'var(--foreground)', 'stroke-width': 2 }));
782
+ if (c.fc > c.due) { clip.appendChild(svg('rect', { x: xe, y: cy - 2.5, width: Xc(c.fc) - xe, height: 5, rx: 2.5, fill: 'var(--destructive)', 'fill-opacity': '.6' })); clip.appendChild(svg('circle', { cx: Xc(c.fc), cy, r: 6, fill: 'var(--destructive)', stroke: 'var(--card)', 'stroke-width': 2 })); }
783
+ clip.appendChild(svg('path', { d: `M${xe} ${cy - 9} l8 9 -8 9 -8 -9z`, fill: 'var(--card)', stroke: S.c, 'stroke-width': 2 }));
784
+ g.appendChild(clip);
785
+ if (c.due > b) g.appendChild(text(Xl(b + 1) - 4, cy - 12, 'due ' + dlab(c.dueS) + (c.fc > c.due ? ' → ' + dlab(c.fcS) : ''), { 'text-anchor': 'end', style: 'font-size:var(--fs-1)' }));
786
+ } else {
787
+ if (c.st === 'over') g.appendChild(svg('line', { x1: Xc(Math.max(a, c.due)), x2: Xc(Math.min(b, td)), y1: cy, y2: cy, stroke: 'var(--destructive)', 'stroke-width': 3, 'stroke-dasharray': '5 4', 'stroke-linecap': 'round' }));
788
+ if (c.due >= a && c.due <= b) {
789
+ if (c.st === 'done') { g.appendChild(svg('circle', { cx: Xc(c.due), cy, r: 8, fill: 'var(--success)', 'fill-opacity': '.18' })); g.appendChild(svg('path', { d: `M${Xc(c.due) - 4} ${cy} l3 3 5 -6`, fill: 'none', stroke: 'var(--success)', 'stroke-width': 2, 'stroke-linecap': 'round' })); }
790
+ else g.appendChild(svg('path', { d: `M${Xc(c.due)} ${cy - 8} l7 8 -7 8 -7 -8z`, fill: c.st === 'over' ? S.c : 'var(--card)', stroke: S.c, 'stroke-width': 2 }));
791
+ }
792
+ }
793
+ g.appendChild(text(W - SW + 10, cy + 4, statTxt(c, td), { style: 'fill:' + S.c + ';font-weight:500' }));
794
+ const tt = svg('title', {}); tt.textContent = c.why; g.appendChild(tt);
795
+ g.insertBefore(svg('rect', { x: 0, y, width: W, height: rh, fill: 'transparent' }), g.firstChild);
796
+ if (go) { const open = () => { if (!goInitiative(c.id)) openInit(host, r, c.id, c.n); }; g.addEventListener('click', open); g.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); open(); } }); }
797
+ s.appendChild(g);
798
+ });
799
+ const cut = all.filter((c) => c.kind === 'init').length;
800
+ return el('section', { class: 'card', 'data-cv': 'progress' }, head, s,
801
+ isPhoneR(r) ? phoneRows(data.gantt.filter((g) => STKEY[g.status] && STKEY[g.status] !== 'none').slice(0, 10), r, host) : null,
802
+ el('div', { class: 'pgl' }, el('span', null, '◇ due'), el('span', { style: 'color:var(--destructive)' }, '● forecast'), anyPct ? el('span', null, '▬ actual progress') : null, el('span', null, '| where it should be today'), el('span', { style: 'color:var(--destructive)' }, '┅ overdue'),
803
+ el('span', { style: 'color:var(--success)' }, '■ on time'), el('span', { style: 'color:var(--warning)' }, '■ at risk'), el('span', { style: 'color:var(--destructive)' }, '■ delayed')),
804
+ !anyPct && cut ? el('div', { class: 'dim mono', style: small, 'data-cv': 'no-pct' }, 'How far each initiative is done is not recorded, so the bars show the plan, the promise and the forecast only.') : null);
805
+ }
806
+
807
+ /* the days of the week page, Monday to Friday: meetings (start, end, title, routine, hot), markers, hours and the hours tied to a promise */
808
+ function gridDays(data) {
809
+ const c2 = IX2(); const out = [];
810
+ for (let i = 0; i < 5; i++) {
811
+ const date = shift('day', data.range.start, i);
812
+ let ms; let mk;
813
+ if (c2) { const d = c2.day(date); ms = d.ms; mk = d.mk; }
814
+ else {
815
+ const cell = (data.cells || []).find((c) => c.start === date); const ev = cell ? cell.events : [];
816
+ ms = ev.filter((e) => e.kind !== 'milestone' && timeOf(e.at) !== null).map((e) => ({ id: e.id, s: timeOf(e.at), e: endH(e), t: e.title || e.id, rt: !!(e.routine || e.mock_routine || ROUTINE_RE.test(e.title || '')), hot: !!e.mock_hot }));
817
+ mk = ev.filter((e) => e.kind === 'milestone').map((e) => [markOf(e.title), e.title]).filter((x) => x[0]);
818
+ }
819
+ ms = ms.slice().sort((x, y) => x.s - y.s);
820
+ const v = ms.reduce((t, q) => t + q.e - q.s, 0); const lk = ms.reduce((t, q) => (q.rt ? t : t + q.e - q.s), 0);
821
+ out.push({ date, dn: DN[dowOf(date)] + ' ' + Number(date.slice(8)), ms, mk, v, lk });
822
+ }
823
+ return out;
824
+ }
825
+ const rd1 = (x) => Math.round(x * 10) / 10;
826
+ const MK2 = { ...MKS, risk: ['▲', 'var(--destructive)'], forecast: ['●', 'var(--warning)'] };
827
+
828
+ function weekGrid(data, r, host) {
829
+ const days = gridDays(data); const today = data.today || todayStr();
830
+ const all = days.flatMap((d) => d.ms); const t0 = Math.min(8, ...all.map((q) => Math.floor(q.s))); const t1 = Math.max(18, ...all.map((q) => Math.ceil(q.e)));
831
+ const W = 760; const x0 = 44; const cw = 142; const top = 66; const ph = 38; const rows = t1 - t0; const H = top + rows * ph + 14; const X = (i) => x0 + i * cw; const Yt = (t) => top + (t - t0) * ph;
832
+ const s = svg('svg', { class: 'chart md hide-ph', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Meetings this week. Blue is tied to a promise or decision, grey is routine, dashed purple is a focus window.' });
833
+ for (let t = t0; t <= t1; t += 2) { s.appendChild(svg('line', { x1: x0, x2: x0 + 5 * cw, y1: Yt(t), y2: Yt(t), stroke: 'var(--border)' })); s.appendChild(text(x0 - 6, Yt(t) + 4, hhmm(t), { 'text-anchor': 'end' })); }
834
+ const now = new Date(); const nh = Math.max(t0, Math.min(t1, now.getHours() + now.getMinutes() / 60));
835
+ days.forEach((d, i) => {
836
+ const isT = d.date === today; const bw = cw - 16;
837
+ if (isT) s.appendChild(svg('rect', { x: X(i), y: top - 44, width: cw, height: rows * ph + 44, fill: 'var(--accw)', rx: 8 }));
838
+ s.appendChild(text(X(i) + 8, top - 32, d.dn, { class: 't', style: small })); s.appendChild(text(X(i) + cw - 8, top - 32, rd1(d.v) + ' h', { 'text-anchor': 'end' }));
839
+ s.appendChild(svg('rect', { x: X(i) + 8, y: top - 26, width: bw, height: 5, rx: 2.5, fill: 'var(--muted)' }));
840
+ s.appendChild(svg('rect', { x: X(i) + 8, y: top - 26, width: Math.min(bw, d.v / 6 * bw), height: 5, rx: 2.5, fill: 'var(--ring)', opacity: '.6' }));
841
+ s.appendChild(svg('rect', { x: X(i) + 8, y: top - 26, width: Math.min(bw, d.lk / 6 * bw), height: 5, rx: 2.5, fill: 'var(--acc)' }));
842
+ d.mk.forEach((q, k) => { const g = MK2[q[0]] || MK2.report; const n = svg('g', {}, text(X(i) + 8 + k * 16, top - 6, g[0], { style: 'font-size:var(--fs-4);fill:' + g[1] })); const t = svg('title', {}); t.textContent = q[1]; n.appendChild(t); s.appendChild(n); });
843
+ let prev = 9; // a focus window: a gap of an hour and a half or more between meetings, from 09:00 to 17:30
844
+ d.ms.concat([{ s: 17.5, e: 17.5 }]).forEach((q) => {
845
+ if (q.s - prev >= 1.5) { const g = svg('g', {}, svg('rect', { x: X(i) + 4, y: Yt(prev) + 2, width: cw - 8, height: (q.s - prev) * ph - 4, rx: 6, fill: 'var(--purple)', 'fill-opacity': '.07', stroke: 'var(--purple)', 'stroke-dasharray': '4 4' }), text(X(i) + cw / 2, Yt(prev) + (q.s - prev) * ph / 2 + 4, rd1(q.s - prev) + ' h focus', { 'text-anchor': 'middle', style: 'fill:var(--purple)' })); s.appendChild(g); }
846
+ prev = Math.max(prev, q.e);
847
+ });
848
+ d.ms.forEach((q, k) => {
849
+ const nm = q.t.length > 17 ? q.t.slice(0, 16) + '…' : q.t; const done = r.done && q.id && r.done[baseId({ id: q.id })];
850
+ const open = () => openDay(d.date, q.id || null);
851
+ const g = svg('g', { class: 'wb', role: 'button', tabindex: '0', 'data-a': 'calblk', 'data-k': Number(d.date.slice(8)) + '|' + (q.id || ''), 'data-cv-block': q.id || d.date, 'aria-label': q.t + ', ' + d.dn + ' ' + hhmm(q.s) + '. Open the day.', style: 'cursor:pointer' },
852
+ svg('rect', { class: 'grow', style: `animation-delay:${i * 60 + k * 30}ms`, x: X(i) + 4, y: Yt(q.s), width: cw - 8, height: (q.e - q.s) * ph - 2, rx: 6, fill: q.hot ? 'var(--destructive)' : q.rt ? 'var(--ring)' : 'var(--acc)', 'fill-opacity': q.rt ? '.5' : '.88', ...(done && done.id === 'skip' ? { 'stroke-dasharray': '4 3', stroke: 'var(--ring)' } : {}) }),
853
+ text(X(i) + 11, Yt(q.s) + 15, nm, { style: 'fill:' + (q.rt ? 'var(--foreground)' : '#fff') + ';font-size:var(--fs-2)' }));
854
+ const t = svg('title', {}); t.textContent = q.t + ' ' + hhmm(q.s); g.appendChild(t);
855
+ g.addEventListener('click', open); g.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); open(); } });
856
+ s.appendChild(g);
857
+ });
858
+ if (isT) s.appendChild(svg('line', { x1: X(i), x2: X(i) + cw, y1: Yt(nh), y2: Yt(nh), stroke: 'var(--foreground)', 'stroke-width': 2 }));
859
+ });
860
+ const list = el('ul', { class: 'list', style: 'margin:0' }, ...days.map((d) => el('li', { class: 'nrow', 'data-a': 'calblk', style: 'grid-template-columns:auto minmax(0,1fr);cursor:pointer;padding-inline:8px', role: 'button', tabindex: '0', onClick: () => openDay(d.date, null), onKeydown: (ev) => { if (ev.key === 'Enter') openDay(d.date, null); } },
861
+ el('span', { class: 'ki mono', style: 'width:auto;padding:0 8px;' + small }, d.dn),
862
+ el('div', { class: 'nb' }, el('div', { class: 'lbar' }, el('i', { style: `width:${d.lk / 6 * 100}%;background:var(--acc)` }), el('i', { style: `width:${(d.v - d.lk) / 6 * 100}%;background:var(--ring)` }), el('i', { style: 'flex:1;background:var(--muted)' })),
863
+ el('div', { class: 'chips2 dim mono', style: small }, rd1(d.v) + ' h' + d.mk.map((q) => ' · ' + (MK2[q[0]] || MK2.report)[0] + ' ' + q[1]).join(''))))));
864
+ if (!all.length && !days.some((d) => d.mk.length)) return el('section', { class: 'card', 'data-cv': 'week-grid' }, el('div', { class: 'fold', 'data-cv': 'no-events' }, c3ic('moon', 'sm'), data.events_source === 'none' ? 'No calendar is cached yet, so there are no meetings to draw.' : 'Nothing is scheduled this week.'));
865
+ return el('section', { class: 'card', 'data-cv': 'week-grid' }, s, el('div', { class: 'show-ph' }, list),
866
+ el('div', { class: 'legend' }, el('span', { style: 'color:var(--acc)' }, '■ tied to a promise or decision'), el('span', null, '■ routine'), el('span', { style: 'color:var(--purple)' }, '▭ focus window'), el('span', null, '◆ release'), el('span', null, '◇ promise'), el('span', null, '▪ report')));
867
+ }
868
+
869
+ /* the line above the week: the heaviest day, and how many meeting hours are tied to a promise or a decision (the mockup's lensInfo, line 2367) */
870
+ function weekHead(data) {
871
+ const days = gridDays(data); const tot = days.reduce((t, d) => t + d.v, 0); const lk = days.reduce((t, d) => t + d.lk, 0);
872
+ if (!tot) return lensHead(data);
873
+ const hv = days.reduce((m, d) => (d.v > m.v ? d : m), days[0]);
874
+ return el('div', { class: 'lh' }, el('span', { class: 'ki' }, c3ic('cal')), el('div', { style: 'min-width:0' }, el('b', null, hv.dn.split(' ')[0] + ' is the heaviest day: ' + rd1(hv.v) + ' h of meetings'),
875
+ el('div', { class: 'dim' }, Math.round(lk) + ' of ' + Math.round(tot) + ' h are tied to a promise or a decision. The rest is routine.')), c3ring(Math.round(lk), Math.max(1, Math.round(tot)), 44));
876
+ }
877
+
878
+ /* ── the page ───────────────────────────────────────────────────────────────────────────────────────────────────── */
879
+
880
+ function draw(host, r) {
881
+ if (r.ctx && r.ctx.ribbon) { r.root.replaceChildren(ribbonCard(host, r)); if (!r.loading) r.anim = null; return; }
882
+ const phone = r.root.classList.contains('ph');
883
+ const head = bar(host, r, phone);
884
+ let body;
885
+ if (r.error) body = el('p', { class: 'dim mono', style: small, role: 'status', 'data-cv': 'error' }, 'The calendar could not be read: ' + r.error);
886
+ else if (!r.data) body = el('p', { class: 'dim mono', style: small, role: 'status', 'data-cv': 'loading' }, 'Reading the repository');
887
+ else {
888
+ const d = r.data;
889
+ const strat = strategy(d);
890
+ const c = r.ctx || {};
891
+ // The mockup's day page (vCal, line 2648): the deck and the due list on the left, the timeline on the right. Other zooms stack.
892
+ const two = d.unit === 'day' && c.plan;
893
+ const week = c.plan && r.view === 'week' && d.unit === 'day';
894
+ const kids = week ? [weekHead(d), el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, c.plan, progressCard(d, r, host)),
895
+ el('div', { class: 'bcol r' }, el('section', { class: 'card', 'data-cv': 'glance' }, el('h3', null, c3ic('cal'), 'The week at a glance'), weekStrip(r, host)))), weekGrid(d, r, host)), strat]
896
+ : two
897
+ ? [lensHead(d), el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, c.plan, r.view === 'day' ? (c.due || null) : null, c.plan && r.view === 'day' ? stageNode(host, r) : null, r.view === 'month' ? progressCard(d, r, host) : null), el('div', { class: 'bcol r' }, events(d, r, host)))), r.view === 'day' || r.view === 'month' ? null : gantt(d, r, host), strat]
898
+ : [lensHead(d), c.plan || null, events(d, r, host), gantt(d, r, host), strat];
899
+ // the mockup's Quarter and Year are the strategy lens, two different panes (c3-cal-strategy.js); they replace the day-part deck, the list and the gantt
900
+ const qy = isC3(r) && (r.view === 'quarter' || r.view === 'year') ? (r.view === 'quarter' ? mountQuarter : mountYear)(el('div', { 'data-cv': 'strategy-pane' }), { data: d, today: d.today, goInit: (id, t) => { if (!goInitiative(id)) openInit(host, r, id, t); } }) : null;
901
+ body = el('div', { class: 'calbody' + (r.anim && !r.loading ? ' ' + r.anim : '') }, ...(qy ? [qy] : kids));
902
+ if (!r.loading) r.anim = null;
903
+ }
904
+ disposeStrategy(r.root);
905
+ r.root.replaceChildren(head, body);
906
+ }
907
+
908
+ /* Change the view or the date and read again. anim names the mockup's transition (.zi, .zo, .sn, .sp, console3.css). */
909
+ function go(host, r, change) {
910
+ if (change.view) r.view = change.view;
911
+ if (change.zoom) r.zoom = change.zoom;
912
+ if (change.date) r.date = change.date;
913
+ r.anim = change.anim || null;
914
+ load(host, r);
915
+ }
916
+
917
+ async function load(host, r) {
918
+ const seq = (r.seq = (r.seq || 0) + 1);
919
+ r.loading = true; r.error = null; draw(host, r);
920
+ try {
921
+ const d = await pmGet(`/api/calendar-views?view=${encodeURIComponent(r.view)}&date=${encodeURIComponent(r.date)}` + (r.ctx && r.ctx.scope ? '&scope=' + encodeURIComponent(r.ctx.scope) : ''));
922
+ if (seq !== r.seq || !roots.has(host)) return;
923
+ if (!d || d.ok === false) throw new Error((d && d.error) || 'unreadable');
924
+ r.data = d; r.data2 = null;
925
+ if (r.ctx && r.ctx.ribbon && r.view === 'week') { const d2 = await pmGet(`/api/calendar-views?view=week&date=${encodeURIComponent(shift('week', r.date, 1))}` + (r.ctx.scope ? '&scope=' + encodeURIComponent(r.ctx.scope) : '')).catch(() => null); if (seq !== r.seq || !roots.has(host)) return; r.data2 = d2 && d2.ok !== false ? d2 : null; }
926
+ } catch (e) {
927
+ if (seq !== r.seq || !roots.has(host)) return;
928
+ r.error = String((e && e.message) || e); r.data = null;
929
+ }
930
+ r.loading = false; draw(host, r);
931
+ }
932
+
933
+ /* The mockup's calendar gestures (lines 2640 to 2700): a sideways swipe of more than 80px, a pinch and Ctrl or Cmd with the wheel step the zoom or the
934
+ * period; + and - zoom; t goes to today; the arrows step. The mockup binds the keys on the document, so they work without focus inside the view.
935
+ * The Briefing's ribbon has no period to step: it takes + and - and the wheel only. */
936
+ function wire(host, r) {
937
+ const root = r.root; let s = null; let sup = false; let supT = null; const pts = {}; let pinch = null; let wt = 0;
938
+ const rib = !!(r.ctx && r.ctx.ribbon);
939
+ const zoomBy = (k) => { // k < 0 zooms in (toward the day), k > 0 out
940
+ const L = rib ? RIB : VIEWS; const i = L.findIndex((v) => v[0] === (rib ? r.zoom : r.view)); const j = i + k;
941
+ if (i < 0 || j < 0 || j >= L.length) return false;
942
+ go(host, r, rib ? { view: L[j][2], zoom: L[j][0], anim: k < 0 ? 'zi' : 'zo' } : { view: L[j][0], anim: k < 0 ? 'zi' : 'zo' }); return true;
943
+ };
944
+ const hush = () => { sup = true; clearTimeout(supT); supT = setTimeout(() => { sup = false; }, 90); };
945
+ root.addEventListener('pointerdown', (e) => {
946
+ if (rib || e.target.closest('.slt,select,input,textarea,.fc.back')) { s = null; return; }
947
+ pts[e.pointerId] = [e.clientX, e.clientY]; const ids = Object.keys(pts);
948
+ if (ids.length === 2) { pinch = Math.hypot(pts[ids[0]][0] - pts[ids[1]][0], pts[ids[0]][1] - pts[ids[1]][1]); s = null; return; }
949
+ s = { x: e.clientX, y: e.clientY };
950
+ });
951
+ root.addEventListener('pointermove', (e) => {
952
+ if (!pinch || !pts[e.pointerId]) return; pts[e.pointerId] = [e.clientX, e.clientY]; const ids = Object.keys(pts); if (ids.length < 2) return;
953
+ const d = Math.hypot(pts[ids[0]][0] - pts[ids[1]][0], pts[ids[0]][1] - pts[ids[1]][1]); const q = d / pinch;
954
+ if (q > 1.3 || q < 0.75) { pinch = d; zoomBy(q > 1.3 ? -1 : 1); }
955
+ });
956
+ const up = (e) => {
957
+ delete pts[e.pointerId]; if (Object.keys(pts).length) return; pinch = null;
958
+ if (!s) return; const dx = e.clientX - s.x; const dy = e.clientY - s.y; s = null;
959
+ if (Math.abs(dx) > 80 && Math.abs(dy) < 45) { hush(); go(host, r, { date: shift(r.view, r.date, dx < 0 ? 1 : -1), anim: dx < 0 ? 'sn' : 'sp' }); }
960
+ };
961
+ root.addEventListener('pointerup', up); root.addEventListener('pointercancel', (e) => { delete pts[e.pointerId]; s = null; pinch = null; });
962
+ root.addEventListener('click', (e) => { if (sup) { sup = false; e.stopPropagation(); e.preventDefault(); } }, true);
963
+ root.addEventListener('wheel', (e) => {
964
+ if (!(e.ctrlKey || e.metaKey)) return; e.preventDefault(); const n = Date.now(); if (n - wt < 350) return; wt = n; zoomBy(e.deltaY > 0 ? 1 : -1);
965
+ }, { passive: false });
966
+ const onKey = (e) => {
967
+ if (e.metaKey || e.ctrlKey || e.altKey || e.defaultPrevented) return;
968
+ if (!root.isConnected || !root.offsetParent) return; // another tab is showing
969
+ const t = e.target; if (t && t.closest && t.closest('input,select,textarea,[contenteditable="true"]')) return;
970
+ if (document.querySelector('[data-shell="sheet"]')) return; // a sheet is open
971
+ if ((e.key === '+' || e.key === '=') && zoomBy(-1)) e.preventDefault();
972
+ else if ((e.key === '-' || e.key === '_') && zoomBy(1)) e.preventDefault();
973
+ else if (rib) return;
974
+ else if (e.key === 't' && r.date !== todayStr()) go(host, r, { date: todayStr(), anim: r.date > todayStr() ? 'sp' : 'sn' });
975
+ else if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft') && !(t && t.closest && t.closest('.fc'))) { e.preventDefault(); go(host, r, { date: shift(r.view, r.date, e.key === 'ArrowRight' ? 1 : -1), anim: e.key === 'ArrowRight' ? 'sn' : 'sp' }); }
976
+ };
977
+ document.addEventListener('keydown', onKey);
978
+ r.off = () => { document.removeEventListener('keydown', onKey); clearTimeout(supT); };
979
+ }
980
+
981
+ /* the simulated recorder (shell-recording.js, demo only): a start, a stop or a pause redraws the stage under the day's cards, and the group's title opens the During stage */
982
+ let simOnWin = false;
983
+ function installSim() {
984
+ if (simOnWin || typeof window === 'undefined') return; simOnWin = true;
985
+ const calInst = () => [...live].filter((x) => x.ctx && x.ctx.plan && x.root.isConnected);
986
+ window.addEventListener('c3-sim-rec', () => calInst().forEach((x) => { if (selEvent(x)) refreshSel(x.host, x); }));
987
+ window.addEventListener('c3-sim-open', () => { gotoTab('cal'); calInst().forEach((x) => { if (x.view !== 'day') x.view = 'day'; x.phase = 'during'; if (x.data && x.data.unit === 'day' && x.data.range.start === x.data.range.end) refreshSel(x.host, x); else go(x.host, x, { view: 'day', date: todayOf(x) }); }); });
988
+ }
989
+
990
+ export function mountCalendarViews(host, ctx) {
991
+ if (!host) return;
992
+ ensureC3Css();
993
+ let r = roots.get(host);
994
+ if (!r) {
995
+ r = { ctx: ctx || {}, root: el('section', { class: 'c3 calwrap', 'data-board': 'calendar-views', 'aria-label': 'Calendar' }), view: (ctx && ctx.view) || 'week', zoom: (ctx && ctx.view === 'day') ? 'today' : (ctx && ctx.view) || 'week', date: todayStr(), data: null, error: null, host, done: {}, ready: {}, sel: null, phase: null };
996
+ if (PENDING && ctx && ctx.plan) { r.sel = PENDING.sel; if (PENDING.date) r.date = PENDING.date; PENDING = null; }
997
+ live.add(r);
998
+ r.stopPhone = c3Phone(r.root);
999
+ roots.set(host, r);
1000
+ wire(host, r);
1001
+ if (ctx && ctx.ribbon) installPulse(host);
1002
+ installSim();
1003
+ host.replaceChildren(r.root);
1004
+ load(host, r);
1005
+ return;
1006
+ }
1007
+ if (!r.root.isConnected) host.replaceChildren(r.root);
1008
+ }
1009
+
1010
+ export function disposeCalendarViews(host) {
1011
+ const r = roots.get(host);
1012
+ if (r && r.stopPhone) r.stopPhone();
1013
+ if (r) { live.delete(r); if (r.off) r.off(); }
1014
+ roots.delete(host);
1015
+ }