create-pmos 0.133.0 → 0.135.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 (212) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/bin/notes-replay.js +235 -0
  7. package/workspace/c3-briefing-test.js +130 -0
  8. package/workspace/c3-foundation-test.js +159 -0
  9. package/workspace/c3-ib-items-test.js +105 -0
  10. package/workspace/c3-ib-more-test.js +120 -0
  11. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  12. package/workspace/c3-ini-harness.js +66 -0
  13. package/workspace/c3-initiatives-test.js +140 -0
  14. package/workspace/c3-one-home-test.js +14 -1
  15. package/workspace/c3-reports-test.js +57 -18
  16. package/workspace/c3-settings-test.js +139 -0
  17. package/workspace/c3-work-test.js +42 -10
  18. package/workspace/fixtures/one-home-baseline.json +1 -9
  19. package/workspace/guide/briefing-cards.md +15 -3
  20. package/workspace/guide/briefing.md +12 -8
  21. package/workspace/guide/calendar-popup.md +2 -2
  22. package/workspace/guide/console-calendar.md +39 -10
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-boards.md +65 -0
  25. package/workspace/guide/jira.md +3 -1
  26. package/workspace/guide/layout.md +2 -0
  27. package/workspace/guide/my-work.md +5 -3
  28. package/workspace/guide/notes.md +35 -2
  29. package/workspace/guide/reports-dashboards.md +29 -5
  30. package/workspace/guide/settings.md +68 -0
  31. package/workspace/guide/welcome.md +10 -10
  32. package/workspace/lib/calendar-cli.js +3 -2
  33. package/workspace/lib/calendar-layers.js +21 -10
  34. package/workspace/lib/calendar-tidy.js +230 -0
  35. package/workspace/lib/engine/boards.js +542 -0
  36. package/workspace/lib/engine/breakdown-ops.js +180 -0
  37. package/workspace/lib/engine/calendar-import.js +84 -0
  38. package/workspace/lib/engine/calendar-model.js +185 -0
  39. package/workspace/lib/engine/card-state.js +92 -0
  40. package/workspace/lib/engine/derived/derive.js +92 -0
  41. package/workspace/lib/engine/derived/plan.js +3 -0
  42. package/workspace/lib/engine/derived/trust.js +3 -0
  43. package/workspace/lib/engine/doc-health.js +142 -0
  44. package/workspace/lib/engine/doc-sheet.js +75 -0
  45. package/workspace/lib/engine/intake.js +78 -0
  46. package/workspace/lib/engine/outbox.js +86 -0
  47. package/workspace/lib/engine/plan-forecast.js +342 -0
  48. package/workspace/lib/engine/promises.js +29 -0
  49. package/workspace/lib/engine/records/index.js +114 -0
  50. package/workspace/lib/engine/releases.js +96 -0
  51. package/workspace/lib/engine/replies.js +99 -0
  52. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  53. package/workspace/lib/engine/report-families.js +175 -0
  54. package/workspace/lib/engine/roadmap-ops.js +150 -0
  55. package/workspace/lib/engine/sessions-list.js +27 -0
  56. package/workspace/lib/engine/signal-rules.js +95 -0
  57. package/workspace/lib/engine/signals/cards.js +57 -0
  58. package/workspace/lib/engine/signals/detectors.js +370 -0
  59. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  60. package/workspace/lib/engine/signals/facts.js +197 -0
  61. package/workspace/lib/engine/signals/index.js +33 -0
  62. package/workspace/lib/engine/signals/kinds.js +110 -0
  63. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  64. package/workspace/lib/engine/signals/options.js +24 -0
  65. package/workspace/lib/engine/signals/score.js +72 -0
  66. package/workspace/lib/engine/signals/select.js +47 -0
  67. package/workspace/lib/engine/step-planner.js +65 -0
  68. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  69. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  70. package/workspace/lib/engine/trust/import-guard.js +36 -0
  71. package/workspace/lib/engine/trust/ledger.js +53 -0
  72. package/workspace/lib/engine/trust/outbox.js +45 -0
  73. package/workspace/lib/engine/trust/plan.js +5 -0
  74. package/workspace/lib/engine/trust/trust.js +21 -0
  75. package/workspace/lib/engine/truth-table.js +57 -0
  76. package/workspace/lib/engine/workspace-lib.js +24 -0
  77. package/workspace/lib/factory-join.js +1 -1
  78. package/workspace/lib/facts-async.js +4 -4
  79. package/workspace/lib/headless.js +95 -0
  80. package/workspace/lib/import-overlay.js +2 -1
  81. package/workspace/lib/jira-status.js +30 -3
  82. package/workspace/lib/notes-audio.js +28 -9
  83. package/workspace/lib/notes-capture.js +136 -0
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-live-guard.js +117 -0
  86. package/workspace/lib/notes-replay.js +125 -0
  87. package/workspace/lib/notes-screen.js +180 -0
  88. package/workspace/lib/notes-whisper.js +139 -16
  89. package/workspace/lib/notes.js +4 -2
  90. package/workspace/lib/outbox-store.js +10 -6
  91. package/workspace/lib/pmos-verbs.js +3 -1
  92. package/workspace/lib/proactive-cli.js +7 -0
  93. package/workspace/lib/proactive.js +15 -2
  94. package/workspace/lib/repo-script.js +36 -0
  95. package/workspace/lib/report-dashboards.js +5 -2
  96. package/workspace/lib/report-frame.js +122 -0
  97. package/workspace/lib/report-schedules.js +113 -0
  98. package/workspace/lib/route-modules.js +9 -2
  99. package/workspace/lib/routes-boards.js +734 -0
  100. package/workspace/lib/routes-brief-page.js +13 -4
  101. package/workspace/lib/routes-briefing.js +22 -11
  102. package/workspace/lib/routes-c3-init.js +4 -3
  103. package/workspace/lib/routes-c3-life.js +2 -1
  104. package/workspace/lib/routes-c3-reports.js +96 -13
  105. package/workspace/lib/routes-c3-strategy.js +4 -3
  106. package/workspace/lib/routes-c3-timeline.js +3 -2
  107. package/workspace/lib/routes-calendar-import.js +3 -2
  108. package/workspace/lib/routes-calendar-views.js +9 -4
  109. package/workspace/lib/routes-doc-health.js +2 -1
  110. package/workspace/lib/routes-doc-sheet.js +2 -1
  111. package/workspace/lib/routes-forecast.js +2 -1
  112. package/workspace/lib/routes-hooks.js +3 -2
  113. package/workspace/lib/routes-initiatives.js +2 -1
  114. package/workspace/lib/routes-intake.js +2 -1
  115. package/workspace/lib/routes-notes.js +113 -15
  116. package/workspace/lib/routes-outbox.js +23 -3
  117. package/workspace/lib/routes-pm-items.js +19 -6
  118. package/workspace/lib/routes-records.js +3 -2
  119. package/workspace/lib/routes-releases.js +2 -1
  120. package/workspace/lib/routes-report-facts.js +4 -3
  121. package/workspace/lib/routes-report-families.js +2 -1
  122. package/workspace/lib/routes-roadmap-ops.js +5 -4
  123. package/workspace/lib/routes-runs.js +6 -5
  124. package/workspace/lib/routes-settings.js +51 -0
  125. package/workspace/lib/routes-signals.js +3 -2
  126. package/workspace/lib/routes-stage-tools.js +220 -0
  127. package/workspace/lib/run-answers.js +2 -1
  128. package/workspace/lib/settings-store.js +226 -0
  129. package/workspace/lib/stage-tools.js +208 -0
  130. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  131. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  132. package/workspace/lib/work-evidence.js +73 -0
  133. package/workspace/package-lock.json +2 -2
  134. package/workspace/package.json +1 -1
  135. package/workspace/public/pm/briefing-deck.js +63 -17
  136. package/workspace/public/pm/briefing-model.js +36 -7
  137. package/workspace/public/pm/c3-ambient.js +100 -0
  138. package/workspace/public/pm/c3-brief-needs.js +26 -6
  139. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  140. package/workspace/public/pm/c3-brief-today.js +56 -21
  141. package/workspace/public/pm/c3-brief.js +2 -1
  142. package/workspace/public/pm/c3-calendar.js +161 -42
  143. package/workspace/public/pm/c3-calpop.js +5 -5
  144. package/workspace/public/pm/c3-demo-boards.js +649 -0
  145. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  146. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  147. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  148. package/workspace/public/pm/c3-demo-data.js +17 -27
  149. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  150. package/workspace/public/pm/c3-demo.js +2 -1
  151. package/workspace/public/pm/c3-dock.js +1 -1
  152. package/workspace/public/pm/c3-ib-docs.js +80 -0
  153. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  154. package/workspace/public/pm/c3-ib-items.js +201 -0
  155. package/workspace/public/pm/c3-ib-releases.js +51 -0
  156. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  157. package/workspace/public/pm/c3-ib-triage.js +178 -0
  158. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  159. package/workspace/public/pm/c3-ini-records.js +197 -0
  160. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  161. package/workspace/public/pm/c3-ini-ui.js +229 -0
  162. package/workspace/public/pm/c3-ini-work.js +139 -0
  163. package/workspace/public/pm/c3-initiatives.css +218 -0
  164. package/workspace/public/pm/c3-initiatives.js +741 -0
  165. package/workspace/public/pm/c3-plan.js +2 -2
  166. package/workspace/public/pm/c3-reports.css +47 -47
  167. package/workspace/public/pm/c3-reports.js +223 -185
  168. package/workspace/public/pm/c3-settings.css +74 -0
  169. package/workspace/public/pm/c3-settings.js +703 -0
  170. package/workspace/public/pm/c3-shell.js +102 -36
  171. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  172. package/workspace/public/pm/c3-terminal.js +2 -2
  173. package/workspace/public/pm/calendar-views.js +26 -13
  174. package/workspace/public/pm/components.css +319 -213
  175. package/workspace/public/pm/console3.css +118 -23
  176. package/workspace/public/pm/dest-notes.js +26 -4
  177. package/workspace/public/pm/notes-screen-capture.js +165 -0
  178. package/workspace/public/pm/notes-screen-view.js +98 -0
  179. package/workspace/public/pm/panel-trees.js +5 -2
  180. package/workspace/public/pm/pm-work-view.js +233 -146
  181. package/workspace/public/pm/shell-bell.js +3 -2
  182. package/workspace/public/pm/shell-recording.js +148 -17
  183. package/workspace/public/pm/tokens.css +2 -2
  184. package/workspace/public/pm/view-notes.js +53 -11
  185. package/workspace/public/pm/welcome.js +197 -89
  186. package/workspace/public/welcome.html +6 -4
  187. package/workspace/server.js +105 -6
  188. package/workspace/skills/day-check.skill +1 -1
  189. package/workspace/skills/jira-sync.skill +6 -4
  190. package/workspace/skills/morning-brief.skill +1 -1
  191. package/workspace/test-files.json +14 -3
  192. package/workspace/test-lists/c3-ib-items.json +9 -0
  193. package/workspace/test-lists/c3-ib-more.json +9 -0
  194. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  195. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  196. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  197. package/workspace/test-lists/console3-briefing.json +9 -0
  198. package/workspace/test-lists/console3-foundation.json +9 -0
  199. package/workspace/test-lists/console3-reports.json +3 -3
  200. package/workspace/test-lists/meeting-notes.json +12 -4
  201. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  202. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  203. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  204. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  205. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  206. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  207. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  208. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  209. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  210. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  211. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  212. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -0,0 +1,278 @@
1
+ /* pm/c3-ini-timeline.js — the HTML timeline of the Initiatives page (mockup v5, 34-initiatives.js "Timeline: compact rows, mono 11px
2
+ * labels, thin bars, zoom, expand, drag, pan"). Used by the Portfolio (board > initiative > epic), a board's Timeline tab (initiative >
3
+ * epic > item) and an initiative's Timeline tab (epic > item).
4
+ *
5
+ * timeline({ key, rows, from, to, today, markers, defOpen, head, onMove, onLocked }) -> div.ini-tl
6
+ * rows [{ uid, label, key?, src? ('pmos'|'jira'), start, end ('YYYY-MM-DD'), progress (0..1), tone ('', ok, warn, bad, pm, muted),
7
+ * cls ('grp'|'ini'|'ep'|'it'), drag? (a PMOS task id: the bar can be dragged), lock? (a Jira key: dates come from Jira),
8
+ * open? () => void, tip? { t, p, st, o, src, jira, hint }, kids? [rows] }]
9
+ * markers [{ id, at, label, kind: 'release'|'milestone', open? }] drawn on their own row
10
+ * defOpen the uids open on the first draw; later draws keep what the PM opened and closed (state is kept per `key`)
11
+ * onMove(taskId, days, uid) a PMOS bar was dragged (or moved with the arrow keys) by whole days
12
+ * onLocked(key) a Jira bar was pressed and dragged, or moved with an arrow key
13
+ * tlInit(node) after the node is in the page: scroll so today sits near the left, end the grow-in
14
+ * tlStill(key) the next draw of that key keeps its scroll and does not animate (after a reschedule)
15
+ *
16
+ * Layout: rows are 24px (a board group 28px), the label column is 240px (132px under 760px), labels are 11px mono. The day width is
17
+ * --in-pd px (Week 22, Month 6, Quarter 2); a bar sits at --in-d0..--in-d1 days. Zoom eases the day width over 380 ms around the centre
18
+ * of the view. A row with children has a chevron that opens it (grid rows 0fr to 1fr) and its bars grow in. A press on the empty chart
19
+ * pans it (mouse). Pointer handlers are on the document (R28-15); every control is a button. Reduced motion turns the motion off.
20
+ */
21
+ import { el } from './dom.js';
22
+ import { btn, icon, geo, day, parseDay } from './c3-ini-ui.js';
23
+
24
+ const DAY = 864e5;
25
+ export const ZOOMS = [['week', 'Week', 22], ['month', 'Month', 6], ['quarter', 'Quarter', 2]];
26
+ const ZPD = Object.fromEntries(ZOOMS.map(([k, , p]) => [k, p]));
27
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
28
+ const TLS = new Map(); // key -> { zoom, open:Set, scroll, still }
29
+ const INST = new WeakMap(); // .ini-tl node -> instance
30
+ const reduced = () => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; } };
31
+ const dOf = (from, d) => Math.round((d - from) / DAY);
32
+ const monthStart = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
33
+ export const tlRange = (today, back = 75, fwd = 230) => { const t = parseDay(today) || new Date(); return { from: monthStart(new Date(+t - back * DAY)), to: monthStart(new Date(+t + fwd * DAY)) }; };
34
+ const stOf = (key, def) => { if (!TLS.has(key)) TLS.set(key, { zoom: 'month', open: new Set(def || []), scroll: null, still: false }); return TLS.get(key); };
35
+ export const tlStill = (key) => { const s = TLS.get(key); if (s) s.still = true; };
36
+
37
+ function ticks(from, days, zoom) {
38
+ const maj = []; const min = []; const end = new Date(+from + days * DAY);
39
+ for (let d = monthStart(from); d < end; d = new Date(d.getFullYear(), d.getMonth() + 1, 1)) {
40
+ const x = dOf(from, d); if (x < 0) continue;
41
+ if (zoom === 'quarter') { if (d.getMonth() % 3 === 0) maj.push([x, 'Q' + (d.getMonth() / 3 + 1) + ' ' + d.getFullYear()]); min.push([x, MON[d.getMonth()]]); }
42
+ else maj.push([x, MON[d.getMonth()] + ' ' + d.getFullYear()]);
43
+ }
44
+ if (zoom !== 'quarter') {
45
+ const m = new Date(from); m.setDate(m.getDate() + ((8 - m.getDay()) % 7));
46
+ for (let d = m; d < end; d = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 7)) min.push([dOf(from, d), zoom === 'week' ? 'Mon ' + d.getDate() : String(d.getDate())]);
47
+ }
48
+ return { maj, min };
49
+ }
50
+ const axis = (from, days, zoom) => { const { maj, min } = ticks(from, days, zoom); return [...maj.map(([x, l]) => geo(el('span', { class: 'ini-tlt mj' }, l), { '--in-d0': x })), ...min.map(([x, l]) => geo(el('span', { class: 'ini-tlt' }, l), { '--in-d0': x }))]; };
51
+ const grid = (from, days, zoom) => { const { maj, min } = ticks(from, days, zoom); const seen = new Set(maj.map((m) => m[0])); return [...maj.map(([x]) => geo(el('i', { class: 'mj' }), { '--in-d0': x })), ...min.filter(([x]) => !seen.has(x)).map(([x]) => geo(el('i'), { '--in-d0': x }))]; };
52
+ const anyRow = (rows, f) => rows.some((r) => r && (f(r) || anyRow(r.kids || [], f)));
53
+
54
+ export function timeline({ key, rows, from, to, today, markers = [], defOpen, head = 'Initiative', onMove, onLocked }) {
55
+ const st = stOf(key, defOpen);
56
+ const days = dOf(from, to);
57
+ const T = parseDay(today) || new Date();
58
+ const anim = !st.still && !reduced();
59
+ const byUid = new Map();
60
+ let n = 0;
61
+ function rowNode(r, depth) {
62
+ byUid.set(r.uid, r);
63
+ const kids = (r.kids || []).filter(Boolean); const has = kids.length > 0; const open = has && st.open.has(r.uid);
64
+ const s = parseDay(r.start); const e = parseDay(r.end);
65
+ let bar = null;
66
+ if (s && e && !(e < from || s > to)) {
67
+ const d0 = Math.max(0, (s - from) / DAY); const d1 = Math.min(days, Math.max((e - from) / DAY, d0 + 0.8)); const p = Math.round((r.progress || 0) * 100);
68
+ const aria = (r.key ? r.key + ' ' : '') + r.label + ', ' + day(r.start) + ' to ' + day(r.end) + ', ' + p + '% done' + (r.drag ? '. Drag or use the arrow keys to move it' : r.lock ? '. Dates come from Jira' : '');
69
+ bar = geo(el('span', { class: 'ini-tlb ' + (r.cls || 'it') + ' ini-t-' + (r.tone || 'acc') + (r.drag ? ' pm' : '') + (r.lock ? ' jl' : ''), tabindex: '0', role: 'button', 'data-tlb': r.uid,
70
+ ...(r.drag ? { 'data-tldrag': r.drag } : {}), ...(r.lock ? { 'data-tllock': r.lock } : {}), 'data-s': r.start, 'data-e': r.end, 'aria-label': aria },
71
+ geo(el('i'), { '--in-w': p + '%' }), r.lock ? el('span', { class: 'lk' }, icon('lock', 'sm')) : null), { '--in-d0': d0.toFixed(2), '--in-d1': d1.toFixed(2), '--in-i': Math.min(n++, 40) });
72
+ }
73
+ const label = [r.key ? el('span', { class: 'ini-tlkey' }, r.key) : null,
74
+ r.src === 'pmos' ? el('span', { class: 'ini-tlsrc' }, 'PMOS') : r.src === 'jira' ? el('span', { class: 'ini-tlsrc j', title: 'From Jira' }, icon('jira', 'sm')) : null,
75
+ el('span', { class: 'ini-tlname' }, r.label)];
76
+ const lab = has
77
+ ? [btn({ class: 'ini-tlc', 'data-tltog': r.uid, 'aria-expanded': String(open), 'aria-label': (open ? 'Collapse ' : 'Expand ') + r.label, onclick: () => toggle(r.uid) }, icon('next', 'sm')),
78
+ btn({ class: 'ini-tlx', title: r.label, 'data-tltog': r.uid, onclick: () => toggle(r.uid) }, ...label),
79
+ r.open ? btn({ class: 'ini-tlo', 'aria-label': 'Open ' + r.label, title: 'Open', 'data-tlopen': r.uid, onclick: () => r.open() }, icon('next', 'sm')) : null]
80
+ : [el('span', { class: 'ini-tlc0' }), r.open ? btn({ class: 'ini-tlx', title: r.label, 'data-tlopen': r.uid, onclick: () => r.open() }, ...label) : el('span', { class: 'ini-tlx', title: r.label }, ...label)];
81
+ const out = [el('div', { class: 'ini-tlr ' + (r.cls || 'it'), 'data-tlrow': r.uid }, geo(el('div', { class: 'ini-tll' }, ...lab), { '--in-dep': depth }), el('div', { class: 'ini-tltr' }, bar))];
82
+ if (has) out.push(el('div', { class: 'ini-tlk' + (open ? ' open' : ''), 'data-tlkids': r.uid }, el('div', null, ...kids.flatMap((k) => rowNode(k, depth + 1)))));
83
+ return out;
84
+ }
85
+ const body = rows.flatMap((r) => rowNode(r, 0));
86
+ const mk = markers.filter((m) => { const d = parseDay(m.at); return d && d >= from && d <= to; });
87
+ const mrow = mk.length ? el('div', { class: 'ini-tlr mrk' }, geo(el('div', { class: 'ini-tll' }, el('span', { class: 'ini-tlc0' }), el('span', { class: 'ini-tlx' }, el('span', { class: 'ini-tlname dim' }, 'Releases, milestones'))), { '--in-dep': 0 }),
88
+ el('div', { class: 'ini-tltr' }, ...mk.map((m) => geo(btn({ class: 'ini-tlm ' + m.kind, 'data-tlm': m.id, 'aria-label': m.label + ', ' + day(m.at), title: m.label + ' · ' + day(m.at) + ' (lives on the Calendar)', onclick: () => (m.open ? m.open() : null) }), { '--in-d0': ((parseDay(m.at) - from) / DAY).toFixed(2) })))) : null;
89
+ const dT = (T - from) / DAY;
90
+ const zoomSeg = el('div', { class: 'seg ini-seg', role: 'group', 'aria-label': 'Zoom' }, ...ZOOMS.map(([k, l]) => btn({ 'aria-pressed': String(st.zoom === k), 'data-tlzoom': k, onclick: () => zoom(k) }, l)));
91
+ const ax = el('div', { class: 'ini-tltr ini-tlax' }, ...axis(from, days, st.zoom));
92
+ const gr = el('div', { class: 'ini-tlgrid', 'aria-hidden': 'true' }, ...grid(from, days, st.zoom));
93
+ const sc = el('div', { class: 'ini-tlsc', 'data-tlsc': '' }, el('div', { class: 'ini-tlin' },
94
+ gr,
95
+ dT >= 0 && dT <= days ? geo(el('div', { class: 'ini-tlnow', 'aria-hidden': 'true' }, el('b', null, 'Today')), { '--in-d0': dT.toFixed(2) }) : null,
96
+ el('div', { class: 'ini-tlr ini-tlhd' }, el('div', { class: 'ini-tll' }, el('span', { class: 'ini-tlname dim' }, head)), ax),
97
+ ...(body.length ? body : [el('div', { class: 'ini-tlr' }, el('div', { class: 'ini-tll' }, el('span', { class: 'ini-tlname dim' }, 'Nothing matches the filters.')), el('div', { class: 'ini-tltr' }))]),
98
+ mrow));
99
+ const tip = el('div', { class: 'ini-tip', role: 'tooltip', 'aria-hidden': 'true' });
100
+ const key2 = (cls) => el('i', { class: 'ini-tlkeyi ' + cls, 'aria-hidden': 'true' });
101
+ const legend = el('div', { class: 'pgl ini-pgl' }, el('span', { class: 'ini-fg' }, key2('now'), 'today'), el('span', null, key2('bar'), 'progress'),
102
+ anyRow(rows, (r) => r.lock) ? el('span', null, icon('lock', 'sm'), 'Jira: dates come from Jira') : null,
103
+ anyRow(rows, (r) => r.drag) ? el('span', null, key2('pm'), 'PMOS task: drag to reschedule') : null,
104
+ mk.length ? [el('span', null, key2('rel'), 'release'), el('span', null, key2('ms'), 'milestone')] : null,
105
+ el('span', null, 'drag the empty chart to pan'));
106
+ const node = geo(el('div', { class: 'ini-tl' + (anim ? ' anim' : ''), 'data-tl': key, 'data-ini': 'timeline' },
107
+ el('div', { class: 'ini-tlbar' }, zoomSeg, el('span', { class: 'chips2' },
108
+ btn({ class: 'pill', 'data-tltoday': '', onclick: () => goToday(true) }, icon('cal', 'sm'), 'Today'),
109
+ btn({ class: 'pill', 'data-tlall': '1', onclick: () => all(true) }, 'Expand all'),
110
+ btn({ class: 'pill', 'data-tlall': '0', onclick: () => all(false) }, 'Collapse all'))),
111
+ sc, tip, legend), { '--in-pd': ZPD[st.zoom], '--in-days': days });
112
+
113
+ const lw = () => { const l = node.querySelector('.ini-tll'); return l ? l.offsetWidth : 240; };
114
+ const pdNow = () => parseFloat(node.style.getPropertyValue('--in-pd')) || ZPD[st.zoom];
115
+ function goToday(smooth) {
116
+ const left = Math.max(0, dT * pdNow() - (sc.clientWidth - lw()) * 0.28);
117
+ if (smooth && !reduced() && sc.scrollTo) sc.scrollTo({ left, behavior: 'smooth' }); else sc.scrollLeft = left;
118
+ }
119
+ function zoom(z) {
120
+ if (st.zoom === z) return;
121
+ const p0 = ZPD[st.zoom]; const p1 = ZPD[z]; const view = sc.clientWidth - lw();
122
+ const center = (sc.scrollLeft + view / 2) / p0;
123
+ st.zoom = z;
124
+ zoomSeg.querySelectorAll('[data-tlzoom]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.tlzoom === z)));
125
+ node.classList.add('zooming');
126
+ const finish = () => {
127
+ geo(node, { '--in-pd': p1 }); sc.scrollLeft = center * p1 - view / 2; st.scroll = sc.scrollLeft;
128
+ ax.replaceChildren(...axis(from, days, z)); gr.replaceChildren(...grid(from, days, z));
129
+ node.classList.remove('zooming');
130
+ };
131
+ if (reduced()) { finish(); return; }
132
+ const t0 = performance.now(); const dur = 380;
133
+ const step = (now) => {
134
+ const k = Math.min(1, (now - t0) / dur); const e = 1 - Math.pow(1 - k, 3); const p = p0 + (p1 - p0) * e;
135
+ geo(node, { '--in-pd': p.toFixed(3) }); sc.scrollLeft = center * p - view / 2;
136
+ if (k < 1) requestAnimationFrame(step); else finish();
137
+ };
138
+ requestAnimationFrame(step);
139
+ }
140
+ function toggle(uid, force) {
141
+ const kids = node.querySelector('[data-tlkids="' + CSS.escape(uid) + '"]'); if (!kids) return;
142
+ const open = force != null ? force : !st.open.has(uid);
143
+ if (open) st.open.add(uid); else st.open.delete(uid);
144
+ kids.classList.toggle('open', open);
145
+ node.querySelectorAll('.ini-tlc[data-tltog="' + CSS.escape(uid) + '"]').forEach((b) => { b.setAttribute('aria-expanded', String(open)); b.setAttribute('aria-label', (open ? 'Collapse ' : 'Expand ') + ((byUid.get(uid) || {}).label || '')); });
146
+ if (open && !reduced()) {
147
+ kids.querySelectorAll(':scope > div > .ini-tlr .ini-tlb').forEach((b, k) => geo(b, { '--in-j': k }));
148
+ kids.classList.remove('grow'); void kids.offsetWidth; kids.classList.add('grow');
149
+ clearTimeout(kids._g); kids._g = setTimeout(() => kids.classList.remove('grow'), 900);
150
+ }
151
+ }
152
+ function all(open) { node.querySelectorAll('[data-tlkids]').forEach((k) => toggle(k.dataset.tlkids, open)); }
153
+ sc.addEventListener('scroll', () => { st.scroll = sc.scrollLeft; }, { passive: true });
154
+ INST.set(node, { key, st, byUid, tip, sc, from, onMove, onLocked, goToday, pd: pdNow });
155
+ return node;
156
+ }
157
+
158
+ /** After the node is in the page: the first scroll (today near the left, or where the PM left it), and the end of the grow-in. */
159
+ export function tlInit(root) {
160
+ if (!root) return;
161
+ const nodes = root.classList && root.classList.contains('ini-tl') ? [root] : [...root.querySelectorAll('.ini-tl')];
162
+ for (const node of nodes) {
163
+ const I = INST.get(node); if (!I) continue;
164
+ if (I.st.still && I.st.scroll != null) I.sc.scrollLeft = I.st.scroll; else I.goToday(false);
165
+ I.st.still = false;
166
+ if (node.classList.contains('anim')) setTimeout(() => node.classList.remove('anim'), 1600);
167
+ }
168
+ }
169
+
170
+ /* ── the tooltip ── */
171
+ function tipShow(bar, ev, nd, extra) {
172
+ const node = bar.closest('.ini-tl'); const I = node && INST.get(node); if (!I) return;
173
+ const r = I.byUid.get(bar.dataset.tlb) || {}; const t = r.tip || {};
174
+ const s = new Date(+parseDay(bar.dataset.s) + (nd || 0) * DAY); const e = new Date(+parseDay(bar.dataset.e) + (nd || 0) * DAY);
175
+ const iso = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
176
+ const len = Math.max(1, Math.round((e - s) / DAY));
177
+ I.tip.replaceChildren(el('b', null, t.t || r.label || ''),
178
+ el('div', null, icon('cal', 'sm'), day(iso(s)) + ' to ' + day(iso(e)) + ' · ' + len + ' days', nd ? el('span', { class: 'ini-tlmv' }, (nd > 0 ? '+' : '') + nd + 'd') : null),
179
+ el('div', null, icon('up', 'sm'), (t.p || '0%') + ' done' + (t.st ? ' · ' + t.st : '')),
180
+ t.o ? el('div', null, icon('user', 'sm'), t.o) : null,
181
+ el('div', null, t.jira ? icon('lock', 'sm') : icon('init', 'sm'), t.src || ''),
182
+ el('div', { class: 'dim' }, extra || t.hint || ''));
183
+ const box = node.getBoundingClientRect(); const br = bar.getBoundingClientRect();
184
+ I.tip.classList.add('on'); I.tip.setAttribute('aria-hidden', 'false');
185
+ const w = I.tip.offsetWidth; const hh = I.tip.offsetHeight; const x = ev && ev.clientX ? ev.clientX : br.left + Math.min(br.width, 40);
186
+ let top = br.bottom - box.top + 8; if (br.bottom + hh + 12 > window.innerHeight && br.top - hh - 8 > box.top) top = br.top - box.top - hh - 8;
187
+ geo(I.tip, { '--in-x': Math.max(4, Math.min(box.width - w - 4, x - box.left + 10)) + 'px', '--in-y': top + 'px' });
188
+ node.querySelectorAll('.ini-tlr.hl').forEach((x2) => x2.classList.remove('hl')); const row = bar.closest('.ini-tlr'); if (row) row.classList.add('hl');
189
+ }
190
+ function tipHide(node) {
191
+ const I = node && INST.get(node); if (!I) return;
192
+ I.tip.classList.remove('on'); I.tip.setAttribute('aria-hidden', 'true');
193
+ node.querySelectorAll('.ini-tlr.hl').forEach((x) => x.classList.remove('hl'));
194
+ }
195
+ function openRow(bar) { const I = INST.get(bar.closest('.ini-tl')); const r = I && I.byUid.get(bar.dataset.tlb); if (r && r.open) r.open(); }
196
+
197
+ /* ── document-level pointer and key handlers: drag a PMOS bar, nudge a Jira bar (locked), pan the chart ── */
198
+ let TLD = null;
199
+ function install() {
200
+ if (typeof document === 'undefined' || document.__iniTl) return;
201
+ document.__iniTl = true;
202
+ document.addEventListener('pointerdown', (e) => {
203
+ if (e.button !== 0) return;
204
+ const node = e.target.closest && e.target.closest('.ini-tl'); const I = node && INST.get(node); if (!I) return;
205
+ const bar = e.target.closest('.ini-tlb');
206
+ if (bar) {
207
+ TLD = { mode: 'bar', bar, node, I, x0: e.clientX, moved: false, nd: 0, pd: I.pd(), d0: parseFloat(bar.style.getPropertyValue('--in-d0')), d1: parseFloat(bar.style.getPropertyValue('--in-d1')) };
208
+ if (bar.dataset.tldrag) { try { bar.setPointerCapture(e.pointerId); } catch { /* not capturable */ } e.preventDefault(); }
209
+ return;
210
+ }
211
+ if (!e.target.closest('.ini-tltr') || e.target.closest('button') || e.pointerType !== 'mouse') return;
212
+ TLD = { mode: 'pan', node, I, x0: e.clientX, s0: I.sc.scrollLeft, moved: false };
213
+ });
214
+ document.addEventListener('pointermove', (e) => {
215
+ if (!TLD || TLD.mode === 'cancel') return;
216
+ const dx = e.clientX - TLD.x0;
217
+ if (!TLD.moved && Math.abs(dx) < 4) return;
218
+ TLD.moved = true;
219
+ if (TLD.mode === 'pan' || (!TLD.bar.dataset.tldrag && !TLD.bar.dataset.tllock)) {
220
+ if (TLD.mode === 'bar') { TLD.mode = 'pan'; TLD.s0 = TLD.I.sc.scrollLeft; }
221
+ TLD.I.sc.scrollLeft = TLD.s0 - dx; TLD.node.classList.add('panning'); tipHide(TLD.node); return;
222
+ }
223
+ const b = TLD.bar;
224
+ if (b.dataset.tldrag) {
225
+ const nd = Math.round(dx / TLD.pd); if (nd === TLD.nd && b.classList.contains('drag')) return;
226
+ TLD.nd = nd; b.classList.add('drag'); TLD.node.classList.add('dragging');
227
+ geo(b, { '--in-d0': (TLD.d0 + nd).toFixed(2), '--in-d1': (TLD.d1 + nd).toFixed(2) });
228
+ tipShow(b, e, nd, nd ? 'Release to reschedule. Esc cancels.' : 'Drag left or right');
229
+ } else if (!TLD.warned) {
230
+ TLD.warned = true; b.classList.remove('nope'); void b.offsetWidth; b.classList.add('nope');
231
+ tipShow(b, e, 0, 'Dates come from Jira. Change them in Jira.');
232
+ }
233
+ });
234
+ document.addEventListener('pointerup', () => {
235
+ if (!TLD) return;
236
+ const D = TLD; TLD = null;
237
+ D.node.classList.remove('panning', 'dragging');
238
+ if (D.mode === 'pan' || D.mode === 'cancel') return;
239
+ const b = D.bar; b.classList.remove('drag');
240
+ if (!D.moved) { tipHide(D.node); openRow(b); return; }
241
+ if (b.dataset.tldrag) { if (D.nd && D.I.onMove) { tlStill(D.I.key); D.I.onMove(b.dataset.tldrag, D.nd, b.dataset.tlb); } else tipHide(D.node); return; }
242
+ if (b.dataset.tllock) { setTimeout(() => b.classList.remove('nope'), 500); if (D.I.onLocked) D.I.onLocked(b.dataset.tllock); }
243
+ });
244
+ document.addEventListener('pointercancel', () => {
245
+ if (TLD && TLD.bar && TLD.mode === 'bar') { geo(TLD.bar, { '--in-d0': TLD.d0, '--in-d1': TLD.d1 }); TLD.bar.classList.remove('drag'); }
246
+ if (TLD) TLD.node.classList.remove('panning', 'dragging');
247
+ TLD = null;
248
+ });
249
+ document.addEventListener('keydown', (e) => {
250
+ const t = e.target;
251
+ if (t && t.classList && t.classList.contains('ini-tlb')) {
252
+ const I = INST.get(t.closest('.ini-tl')); if (!I) return;
253
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openRow(t); return; }
254
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
255
+ e.preventDefault();
256
+ if (t.dataset.tldrag && I.onMove) { tlStill(I.key); I.onMove(t.dataset.tldrag, e.key === 'ArrowRight' ? 1 : -1, t.dataset.tlb); return; }
257
+ if (t.dataset.tllock && I.onLocked) I.onLocked(t.dataset.tllock);
258
+ }
259
+ return;
260
+ }
261
+ if (e.key === 'Escape' && TLD && TLD.mode === 'bar' && TLD.bar.dataset.tldrag) {
262
+ geo(TLD.bar, { '--in-d0': TLD.d0.toFixed(2), '--in-d1': TLD.d1.toFixed(2) }); TLD.bar.classList.remove('drag'); tipHide(TLD.node);
263
+ TLD = { mode: 'cancel', node: TLD.node, moved: true };
264
+ }
265
+ });
266
+ const hover = (e) => { if (TLD) return; const b = e.target.closest && e.target.closest('.ini-tlb'); if (b) tipShow(b, e); };
267
+ document.addEventListener('mouseover', hover);
268
+ document.addEventListener('mousemove', hover);
269
+ document.addEventListener('mouseout', (e) => {
270
+ if (TLD) return;
271
+ const b = e.target.closest && e.target.closest('.ini-tlb'); if (!b) return;
272
+ if (e.relatedTarget && b.contains(e.relatedTarget)) return;
273
+ tipHide(b.closest('.ini-tl'));
274
+ });
275
+ document.addEventListener('focusin', (e) => { const b = e.target.closest && e.target.closest('.ini-tlb'); if (b) tipShow(b, null); });
276
+ document.addEventListener('focusout', (e) => { const b = e.target.closest && e.target.closest('.ini-tlb'); if (b) tipHide(b.closest('.ini-tl')); });
277
+ }
278
+ install();
@@ -0,0 +1,229 @@
1
+ /* pm/c3-ini-ui.js — the console3 builders the Initiatives page draws with (mockup v5, src/pages/34-initiatives.js: nr, crumbs, lh,
2
+ * segTabs, cols, UI.sect, UI.kv, UI.empty, statusChip, healthSt, ownerChip, stageChip, stepsMini, progChip, hbr). Every tab module of
3
+ * the page (c3-initiatives.js and the c3-ib-*.js / c3-ii-tabs.js files) imports them from here, so the markup is the mockup's
4
+ * everywhere: `.calwrap > .calbar`, `.lh`, `.seg`, `.brief.sbb > .bcols > .bcol`, `.card.sect > .sh2`, `ul.list > li.nrow`, `.chips2`,
5
+ * `.chip2`, `.st`, `.tag`, `.pill`, `.chgl`, `.hbs > .hbr`, `.fold`.
6
+ *
7
+ * Rules the builders keep (the conformance gate, R28): every control is a <button> from primitives.js button() (btn() below adds the
8
+ * console3 class it is drawn as, and `ini-b`, the reset that takes the primitive's own height and nowrap off); a colour, a width or a
9
+ * position is a class or a `--in-…` custom property set with geo(), never an inline style; icons come from the console3 sprite
10
+ * (c3ic) or, for the three it lacks (gear, grip, the Jira mark), from svgIcon(). Nothing here reads or writes the server.
11
+ *
12
+ * btn(attrs, ...kids) a button; attrs.class is the console3 class (pill, chip2 lk, ib sm, tt, ...), attrs.onclick its press
13
+ * pbtn(variant, label, icon, attrs) a primitive button drawn as the primitive (primary, outline, ghost), small
14
+ * sect({ ic, title, count, right, attrs }, ...body) section.card.sect > div.sh2 > h2 (icon, title, bdg.g count) + right, then body
15
+ * card({ ic, title, sub, attrs }, ...body) section.card > h3 (icon, title, dim sub), then body
16
+ * nrow({ ic, hot, tone, title, onOpen, openLabel, sub, chips, after, xp, right, attrs, key }) li.nrow (ki, nb > tt + sub + chips +
17
+ * after + xp, acts); an `xp` (a node or an array) makes the row expand in place (c3-expand.js)
18
+ * list(rows) · empty(text) · emptyLi(text) · crumbs(parts, right) · lh(ic, title, sub, right) · segTabs(items, cur, onPick, label)
19
+ * cols(left, right) · hbs(rows) · hbr(label, value, frac, tone) · kv(pairs) · chgl(items) · chgli(ic, text, tag, tone)
20
+ * chip(...kids) · tag(...kids) · st(tone, ic, text) · note(...kids) · lbl(text) · sw(cat) · stageChip(name, cat) · statusChip(s)
21
+ * healthSt(h) · ownerChip(o) · stepsMini(workflow, stage, label) · progChip(p, tone) · miniBar(p, tone) · dateChip(a, b, late)
22
+ * openMenu(anchor, { label, items: [{ label, kids, onPick, disabled, current, attrs } | { group }] }) a popover menu, closed by
23
+ * Escape, a press outside or a pick; arrow keys move between its items
24
+ * day(iso) '12 Oct' · dayLong(iso) 'Mon 12 Oct 2026' · ago(iso, today) · daysBetween(a, b) · plural(n, one, many) · trunc(s, n)
25
+ */
26
+ import { el, frag } from './dom.js';
27
+ import { button } from './primitives.js';
28
+ import { c3ic } from './pm-work-view.js';
29
+ import { makeXp, attachExpand } from './c3-expand.js';
30
+
31
+ export const CATS = ['Discover', 'Plan', 'Do', 'Learn'];
32
+ export const PORTFOLIO = 'Portfolio';
33
+
34
+ /* ── icons the console3 sprite does not have: a 24-unit lucide path ── */
35
+ const SVG = 'http://www.w3.org/2000/svg';
36
+ export function svgIcon(paths, cls) {
37
+ const s = document.createElementNS(SVG, 'svg');
38
+ s.setAttribute('class', 'ic' + (cls ? ' ' + cls : '')); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true');
39
+ for (const d of paths) { const p = document.createElementNS(SVG, 'path'); p.setAttribute('d', d); s.appendChild(p); }
40
+ return s;
41
+ }
42
+ export const GEAR = ['M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', 'M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'];
43
+ export const GRIP = ['M9 5h.01M9 12h.01M9 19h.01M15 5h.01M15 12h.01M15 19h.01'];
44
+ export const JIRA = ['M12 3l9 9-9 9-9-9z', 'M12 8l4 4-4 4-4-4z'];
45
+ /** An icon by name: the sprite's, or one of the three drawn here. */
46
+ export function icon(name, cls) {
47
+ if (name === 'settings') return svgIcon(GEAR, cls);
48
+ if (name === 'grip') return svgIcon(GRIP, cls);
49
+ if (name === 'jira') return svgIcon(JIRA, cls);
50
+ return c3ic(name, cls);
51
+ }
52
+
53
+ /* ── controls ── */
54
+ /** A native button drawn as a console3 control. attrs: class (the console3 class), onclick, and any attribute. */
55
+ export function btn(attrs, ...kids) {
56
+ const { onclick, class: cls, disabled, ...rest } = attrs || {};
57
+ const b = button({ variant: 'ghost', onClick: onclick, disabled: !!disabled, attrs: { ...rest, class: 'ini-b' + (cls ? ' ' + cls : '') } });
58
+ b.replaceChildren(frag(...kids.filter((k) => k != null && k !== false)));
59
+ return b;
60
+ }
61
+ /** The mockup's UI.btn: a primitive button, small, drawn as the primitive. */
62
+ export function pbtn(variant, label, ic, attrs) {
63
+ const { onclick, disabled, ...rest } = attrs || {};
64
+ const b = button({ label, variant, size: 'sm', onClick: onclick, disabled: !!disabled, attrs: rest });
65
+ if (ic) b.prepend(icon(ic, 'sm'));
66
+ return b;
67
+ }
68
+ export const pill = (attrs, ...kids) => btn({ ...attrs, class: 'pill' + (attrs && attrs.class ? ' ' + attrs.class : '') }, ...kids);
69
+ export const lk = (attrs, ...kids) => btn({ ...attrs, class: 'chip2 lk' + (attrs && attrs.class ? ' ' + attrs.class : '') }, ...kids);
70
+ export const ib = (label, ic, attrs) => btn({ title: label, 'aria-label': label, ...(attrs || {}), class: 'ib sm' + (attrs && attrs.class ? ' ' + attrs.class : '') }, typeof ic === 'string' ? icon(ic, 'sm') : ic);
71
+ /** Set run-time geometry (a `--in-…` or `--tl-…` custom property) on a node; the rules in c3-initiatives.css read them. */
72
+ export function geo(node, vars) {
73
+ for (const k of Object.keys(vars)) node.style.setProperty(k, String(vars[k]));
74
+ return node;
75
+ }
76
+
77
+ /* ── text bits ── */
78
+ export const plural = (n, one, many) => n + ' ' + (n === 1 ? one : many || one + 's');
79
+ export const trunc = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
80
+ export const chip = (...kids) => el('span', { class: 'chip2' }, ...kids);
81
+ export const chipA = (attrs, ...kids) => el('span', { class: 'chip2', ...(attrs || {}) }, ...kids);
82
+ export const tag = (...kids) => el('span', { class: 'tag' }, ...kids);
83
+ export const st = (tone, ic, text, attrs) => el('span', { class: 'st ' + tone, ...(attrs || {}) }, ic ? icon(ic, 'sm') : null, text);
84
+ export const note = (...kids) => el('div', { class: 'dim ini-note' }, ...kids);
85
+ export const noteA = (attrs, ...kids) => el('div', { class: 'dim ini-note', ...(attrs || {}) }, ...kids);
86
+ export const lbl = (text) => el('div', { class: 'dim mono ini-lbl' }, text);
87
+ export const mono = (text) => el('span', { class: 'dim mono ini-meta' }, text);
88
+ export const sw = (cat) => el('i', { class: 'ini-sw ini-c', 'data-cat': cat || 'Plan', 'aria-hidden': 'true' });
89
+
90
+ /* ── dates ── */
91
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
92
+ const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
93
+ /** '2026-10-14' (or an ISO time) -> a local Date at midnight, or null. */
94
+ export function parseDay(iso) {
95
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ''));
96
+ return m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : null;
97
+ }
98
+ export const isoOf = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
99
+ export const day = (iso) => { const d = parseDay(iso); return d ? d.getDate() + ' ' + MON[d.getMonth()] : ''; };
100
+ export const dayLong = (iso) => { const d = parseDay(iso); return d ? WD[d.getDay()] + ' ' + d.getDate() + ' ' + MON[d.getMonth()] + ' ' + d.getFullYear() : ''; };
101
+ export const daysBetween = (a, b) => { const x = parseDay(a); const y = parseDay(b); return x && y ? Math.round((y - x) / 864e5) : null; };
102
+ /** 'today', 'yesterday', '3 days ago', or the day for a future date. */
103
+ export function ago(iso, today) {
104
+ const n = daysBetween(today, iso);
105
+ if (n == null) return '';
106
+ return n === 0 ? 'today' : n === -1 ? 'yesterday' : n < 0 ? -n + ' days ago' : day(iso);
107
+ }
108
+ export { MON };
109
+
110
+ /* ── chips ── */
111
+ export const statusChip = (s) => (s === 'at risk' ? st('warn', 'alert', 'At risk') : s === 'late' ? st('bad', 'clock', 'Late') : s === 'done' ? st('ok', 'check', 'Done') : chip('On track'));
112
+ export const HEALTH = { ok: 'On track', warn: 'Needs attention', bad: 'Off track' };
113
+ export const healthSt = (h) => (h === 'ok' ? st('ok', 'check', 'On track') : h === 'warn' ? st('warn', 'alert', 'Needs attention') : h === 'bad' ? st('bad', 'alert', 'Off track') : chip('No health yet'));
114
+ export const ownerText = (o) => (o ? o : el('span', { class: 'dim' }, 'No owner'));
115
+ export const ownerChip = (o) => chip(icon('user', 'sm'), ownerText(o));
116
+ export const stageChip = (name, cat) => chip(sw(cat), name || 'No stage');
117
+ export const toneOf = (status) => (status === 'late' ? 'bad' : status === 'at risk' ? 'warn' : status === 'done' ? 'ok' : '');
118
+ /** A 44px progress bar inline with a chip. */
119
+ export function miniBar(p, tone) {
120
+ const u = geo(el('u'), { '--in-w': Math.max(2, Math.round((Number(p) || 0) * 100)) + '%' });
121
+ return el('span', { class: 'ini-mini ini-t-' + (tone || 'ok'), 'aria-hidden': 'true' }, u);
122
+ }
123
+ export const progChip = (p, tone) => chip(miniBar(p, tone), Math.round((Number(p) || 0) * 100) + '%');
124
+ /** One dot per stage of the initiative's own workflow: past, current, to come, and "Stage k/n". */
125
+ export function stepsMini(workflow, stage, label) {
126
+ const wf = workflow || [];
127
+ const k = wf.findIndex((s) => s.name === stage);
128
+ return el('span', { class: 'chip2 ini-steps', title: (label ? label + ' workflow: ' : '') + wf.map((s) => s.name).join(' > ') },
129
+ ...wf.map((s, n) => el('i', { class: 'ini-dot ini-c' + (n < k ? ' d' : n === k ? ' c' : ''), 'data-cat': s.cat, 'aria-hidden': 'true' })),
130
+ el('span', null, (stage || 'No stage') + ' ', el('span', { class: 'dim' }, (k + 1) + '/' + wf.length)));
131
+ }
132
+ export function dateChip(a, b, late) {
133
+ return el('span', { class: 'chip2' + (late ? ' ini-late' : '') }, icon('cal', 'sm'), a && b ? day(a) + ' to ' + day(b) : day(a || b) || 'No dates yet');
134
+ }
135
+
136
+ /* ── frames ── */
137
+ export const list = (rows, attrs) => el('ul', { class: 'list', ...(attrs || {}) }, ...rows);
138
+ export const empty = (text, attrs) => el('div', { class: 'fold', ...(attrs || {}) }, icon('check', 'sm'), text);
139
+ export const emptyLi = (text, attrs) => el('li', { class: 'fold', ...(attrs || {}) }, icon('check', 'sm'), text);
140
+ export function sect(o, ...body) {
141
+ const { ic, title, count, right, attrs } = o || {};
142
+ return el('section', { ...(attrs || {}), class: 'card sect' + (attrs && attrs.class ? ' ' + attrs.class : '') },
143
+ el('div', { class: 'sh2' }, el('h2', null, ic ? icon(ic, 'sm') : null, title, count != null ? [' ', el('span', { class: 'bdg g' }, String(count))] : null), right || null),
144
+ ...body.filter((x) => x != null));
145
+ }
146
+ export function card(o, ...body) {
147
+ const { ic, title, sub, attrs } = o || {};
148
+ return el('section', { ...(attrs || {}), class: 'card' + (attrs && attrs.class ? ' ' + attrs.class : '') },
149
+ el('h3', null, ic ? icon(ic, 'sm') : null, title, sub ? [' ', el('span', { class: 'dim ini-sub' }, sub)] : null), ...body.filter((x) => x != null));
150
+ }
151
+ /**
152
+ * The console3 list row. o.title is text (or a node); o.onOpen makes it a button (`.tt`); o.xp a panel that opens in place.
153
+ * o.chips and o.right are arrays of nodes. o.attrs go on the li.
154
+ */
155
+ export function nrow(o) {
156
+ const t = o.onOpen
157
+ ? btn({ class: 'tt', onclick: o.onOpen, ...(o.openLabel ? { 'aria-label': o.openLabel } : {}), ...(o.titleAttrs || {}) }, o.title)
158
+ : el('span', { class: 'tt', ...(o.titleAttrs || {}) }, o.title);
159
+ const xp = o.xp ? makeXp(o.xp) : null;
160
+ const li = el('li', { class: 'nrow' + (o.cls ? ' ' + o.cls : ''), ...(o.attrs || {}) },
161
+ el('span', { class: 'ki' + (o.hot ? ' hot' : '') + (o.tone ? ' ini-k-' + o.tone : '') }, icon(o.ic || 'doc')),
162
+ el('div', { class: 'nb' }, t, o.sub ? note(o.sub) : null, o.chips && o.chips.length ? el('div', { class: 'chips' }, ...o.chips.filter(Boolean)) : null, o.after || null, xp),
163
+ o.right && o.right.filter(Boolean).length ? el('div', { class: 'acts' }, ...o.right.filter(Boolean)) : el('span'));
164
+ if (xp) attachExpand(li, xp, { key: o.key });
165
+ return li;
166
+ }
167
+ export function crumbs(parts, right) {
168
+ const kids = [];
169
+ parts.forEach((p, k) => {
170
+ if (k) kids.push(icon('next', 'sm'));
171
+ kids.push(p.go ? lk({ onclick: p.go, ...(p.attrs || {}) }, p.ic ? icon(p.ic, 'sm') : null, p.t) : el('span', { class: 'chip2', 'aria-current': 'page', ...(p.attrs || {}) }, p.ic ? icon(p.ic, 'sm') : null, p.t));
172
+ });
173
+ return el('div', { class: 'calbar' }, el('nav', { class: 'chips2', 'aria-label': 'Breadcrumb', 'data-ini': 'crumbs' }, ...kids), right || null);
174
+ }
175
+ export function lh(ic, title, sub, right, attrs) {
176
+ return el('div', { class: 'lh', ...(attrs || {}) }, el('span', { class: 'ki' }, icon(ic)),
177
+ el('div', { class: 'ini-lhm' }, el('b', null, title), el('div', { class: 'dim' }, sub)), right ? el('div', { class: 'chips2' }, ...[].concat(right)) : null);
178
+ }
179
+ /** items: [[id, label, icon, extra node?, attrs?]]. The pressed one has aria-pressed=true. */
180
+ export function segTabs(items, cur, onPick, label, attrs) {
181
+ return el('div', { class: 'calbar' }, el('div', { class: 'seg ini-seg', role: 'group', 'aria-label': label, ...(attrs || {}) },
182
+ ...items.map(([k, l, ic, extra, a]) => btn({ 'aria-pressed': String(k === cur), 'data-ini-tab': k, onclick: () => onPick(k), ...(a || {}) }, ic ? icon(ic, 'sm') : null, l, extra || null))));
183
+ }
184
+ export const cols = (left, right) => el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' },
185
+ el('div', { class: 'bcol l' }, ...[].concat(left).filter(Boolean)), el('div', { class: 'bcol r' }, ...[].concat(right).filter(Boolean))));
186
+ export const hbs = (rows) => el('div', { class: 'hbs' }, ...rows);
187
+ /** tone: ok, warn, bad, acc, muted (the colour of the bar). */
188
+ export function hbr(label, value, frac, tone, attrs) {
189
+ const u = geo(el('u'), { '--in-w': Math.max(2, Math.min(100, Math.round((Number(frac) || 0) * 100))) + '%' });
190
+ return el('div', { class: 'hbr ini-t-' + (tone || 'acc'), ...(attrs || {}) }, el('div', { class: 'l' }, el('span', null, label), el('i', null, String(value))), el('div', { class: 'tk' }, u));
191
+ }
192
+ export function kv(pairs, attrs) {
193
+ return el('dl', { class: 'ini-kv', ...(attrs || {}) }, ...pairs.filter(Boolean).flatMap(([k, v]) => [el('dt', null, k), el('dd', null, v)]));
194
+ }
195
+ export const chgl = (items, attrs) => el('ul', { class: 'chgl', ...(attrs || {}) }, ...items);
196
+ export const chgli = (ic, text, tg, tone, attrs) => el('li', { class: tone || '', ...(attrs || {}) }, icon(ic, 'sm'), el('span', null, text), tg ? el('span', { class: 'tag' }, tg) : el('span'));
197
+
198
+ /* ── the popover menu (Move to, More tools) ── */
199
+ let menuOpen = null;
200
+ export function closeMenu() {
201
+ if (!menuOpen) return;
202
+ const m = menuOpen; menuOpen = null;
203
+ document.removeEventListener('pointerdown', m.off, true); document.removeEventListener('keydown', m.key, true);
204
+ m.node.remove(); m.anchor.setAttribute('aria-expanded', 'false');
205
+ }
206
+ export function openMenu(anchor, { label, items }) {
207
+ if (menuOpen) { const was = menuOpen.anchor; closeMenu(); if (was === anchor) return; }
208
+ const node = el('div', { class: 'ini-menu', role: 'menu', 'aria-label': label, 'data-ini': 'menu' },
209
+ ...items.map((it) => (it.group != null ? el('div', { class: 'ini-mlbl' }, it.group)
210
+ : btn({ role: 'menuitem', class: 'ini-mi', disabled: !!it.disabled, ...(it.current ? { 'aria-current': 'true' } : {}), ...(it.attrs || {}), onclick: () => { closeMenu(); it.onPick(); } }, ...[].concat(it.kids || it.label)))));
211
+ const host = anchor.closest('.c3pop') || anchor.closest('.c3') || document.body;
212
+ host.appendChild(node);
213
+ const r = anchor.getBoundingClientRect();
214
+ const w = Math.min(300, window.innerWidth - 16);
215
+ geo(node, { '--in-x': Math.max(8, Math.min(window.innerWidth - w - 8, r.right - w)) + 'px', '--in-y': Math.min(window.innerHeight - 60, r.bottom + 6) + 'px' });
216
+ const itemsOf = () => [...node.querySelectorAll('[role="menuitem"]:not(:disabled)')];
217
+ const off = (e) => { if (!node.contains(e.target) && !anchor.contains(e.target)) closeMenu(); };
218
+ const key = (e) => {
219
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeMenu(); anchor.focus(); return; }
220
+ const l = itemsOf(); const at = l.indexOf(document.activeElement);
221
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); const n = l.length; if (n) l[((at < 0 ? -1 : at) + (e.key === 'ArrowDown' ? 1 : -1) + n) % n].focus(); }
222
+ if (e.key === 'Tab') closeMenu();
223
+ };
224
+ menuOpen = { node, anchor, off, key };
225
+ anchor.setAttribute('aria-expanded', 'true');
226
+ document.addEventListener('pointerdown', off, true); document.addEventListener('keydown', key, true);
227
+ const f = itemsOf()[0]; if (f) f.focus();
228
+ return node;
229
+ }