create-pmos 0.134.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 (209) 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 +16 -1
  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-capture.js +9 -1
  83. package/workspace/lib/notes-final.js +38 -13
  84. package/workspace/lib/notes-replay.js +125 -0
  85. package/workspace/lib/notes-screen.js +180 -0
  86. package/workspace/lib/notes-whisper.js +10 -3
  87. package/workspace/lib/outbox-store.js +10 -6
  88. package/workspace/lib/pmos-verbs.js +3 -1
  89. package/workspace/lib/proactive-cli.js +7 -0
  90. package/workspace/lib/proactive.js +15 -2
  91. package/workspace/lib/repo-script.js +36 -0
  92. package/workspace/lib/report-dashboards.js +5 -2
  93. package/workspace/lib/report-frame.js +122 -0
  94. package/workspace/lib/report-schedules.js +113 -0
  95. package/workspace/lib/route-modules.js +9 -2
  96. package/workspace/lib/routes-boards.js +734 -0
  97. package/workspace/lib/routes-brief-page.js +13 -4
  98. package/workspace/lib/routes-briefing.js +22 -11
  99. package/workspace/lib/routes-c3-init.js +4 -3
  100. package/workspace/lib/routes-c3-life.js +2 -1
  101. package/workspace/lib/routes-c3-reports.js +96 -13
  102. package/workspace/lib/routes-c3-strategy.js +4 -3
  103. package/workspace/lib/routes-c3-timeline.js +3 -2
  104. package/workspace/lib/routes-calendar-import.js +3 -2
  105. package/workspace/lib/routes-calendar-views.js +9 -4
  106. package/workspace/lib/routes-doc-health.js +2 -1
  107. package/workspace/lib/routes-doc-sheet.js +2 -1
  108. package/workspace/lib/routes-forecast.js +2 -1
  109. package/workspace/lib/routes-hooks.js +3 -2
  110. package/workspace/lib/routes-initiatives.js +2 -1
  111. package/workspace/lib/routes-intake.js +2 -1
  112. package/workspace/lib/routes-notes.js +55 -9
  113. package/workspace/lib/routes-outbox.js +23 -3
  114. package/workspace/lib/routes-pm-items.js +19 -6
  115. package/workspace/lib/routes-records.js +3 -2
  116. package/workspace/lib/routes-releases.js +2 -1
  117. package/workspace/lib/routes-report-facts.js +4 -3
  118. package/workspace/lib/routes-report-families.js +2 -1
  119. package/workspace/lib/routes-roadmap-ops.js +5 -4
  120. package/workspace/lib/routes-runs.js +6 -5
  121. package/workspace/lib/routes-settings.js +51 -0
  122. package/workspace/lib/routes-signals.js +3 -2
  123. package/workspace/lib/routes-stage-tools.js +220 -0
  124. package/workspace/lib/run-answers.js +2 -1
  125. package/workspace/lib/settings-store.js +226 -0
  126. package/workspace/lib/stage-tools.js +208 -0
  127. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  128. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  129. package/workspace/lib/work-evidence.js +73 -0
  130. package/workspace/package-lock.json +2 -2
  131. package/workspace/package.json +1 -1
  132. package/workspace/public/pm/briefing-deck.js +63 -17
  133. package/workspace/public/pm/briefing-model.js +36 -7
  134. package/workspace/public/pm/c3-ambient.js +100 -0
  135. package/workspace/public/pm/c3-brief-needs.js +26 -6
  136. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  137. package/workspace/public/pm/c3-brief-today.js +56 -21
  138. package/workspace/public/pm/c3-brief.js +2 -1
  139. package/workspace/public/pm/c3-calendar.js +161 -42
  140. package/workspace/public/pm/c3-calpop.js +5 -5
  141. package/workspace/public/pm/c3-demo-boards.js +649 -0
  142. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  143. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  144. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  145. package/workspace/public/pm/c3-demo-data.js +17 -27
  146. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  147. package/workspace/public/pm/c3-demo.js +2 -1
  148. package/workspace/public/pm/c3-dock.js +1 -1
  149. package/workspace/public/pm/c3-ib-docs.js +80 -0
  150. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  151. package/workspace/public/pm/c3-ib-items.js +201 -0
  152. package/workspace/public/pm/c3-ib-releases.js +51 -0
  153. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  154. package/workspace/public/pm/c3-ib-triage.js +178 -0
  155. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  156. package/workspace/public/pm/c3-ini-records.js +197 -0
  157. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  158. package/workspace/public/pm/c3-ini-ui.js +229 -0
  159. package/workspace/public/pm/c3-ini-work.js +139 -0
  160. package/workspace/public/pm/c3-initiatives.css +218 -0
  161. package/workspace/public/pm/c3-initiatives.js +741 -0
  162. package/workspace/public/pm/c3-plan.js +2 -2
  163. package/workspace/public/pm/c3-reports.css +47 -47
  164. package/workspace/public/pm/c3-reports.js +223 -185
  165. package/workspace/public/pm/c3-settings.css +74 -0
  166. package/workspace/public/pm/c3-settings.js +703 -0
  167. package/workspace/public/pm/c3-shell.js +102 -36
  168. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  169. package/workspace/public/pm/c3-terminal.js +2 -2
  170. package/workspace/public/pm/calendar-views.js +1 -1
  171. package/workspace/public/pm/components.css +319 -213
  172. package/workspace/public/pm/console3.css +99 -23
  173. package/workspace/public/pm/dest-notes.js +26 -4
  174. package/workspace/public/pm/notes-screen-capture.js +165 -0
  175. package/workspace/public/pm/notes-screen-view.js +98 -0
  176. package/workspace/public/pm/panel-trees.js +5 -2
  177. package/workspace/public/pm/pm-work-view.js +233 -146
  178. package/workspace/public/pm/shell-bell.js +3 -2
  179. package/workspace/public/pm/shell-recording.js +29 -8
  180. package/workspace/public/pm/tokens.css +2 -2
  181. package/workspace/public/pm/view-notes.js +46 -9
  182. package/workspace/public/pm/welcome.js +197 -89
  183. package/workspace/public/welcome.html +6 -4
  184. package/workspace/server.js +105 -6
  185. package/workspace/skills/day-check.skill +1 -1
  186. package/workspace/skills/jira-sync.skill +6 -4
  187. package/workspace/skills/morning-brief.skill +1 -1
  188. package/workspace/test-files.json +12 -3
  189. package/workspace/test-lists/c3-ib-items.json +9 -0
  190. package/workspace/test-lists/c3-ib-more.json +9 -0
  191. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  192. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  193. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  194. package/workspace/test-lists/console3-briefing.json +9 -0
  195. package/workspace/test-lists/console3-foundation.json +9 -0
  196. package/workspace/test-lists/console3-reports.json +3 -3
  197. package/workspace/test-lists/meeting-notes.json +10 -4
  198. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  199. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  200. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  201. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  202. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  203. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  204. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  205. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  206. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  207. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  208. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  209. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -5,19 +5,22 @@
5
5
  * chip and the swipe, pinch and key gestures; the helpers it passes are its own, so nothing is copied here.
6
6
  *
7
7
  * Top to bottom: the page line and the zoom bar (Day, Week, Month, Quarter, Year), the layer chips (Meetings, Milestones
8
- * and due dates, Releases, Report dates, My due items, then Free time), the grid of the zoom with the layer markers on it,
9
- * and the detail column (under the grid on a narrow screen, at the right from 1100px).
8
+ * and due dates, Releases, Report dates, My due items, then Free time and Tidy-up), the grid of the zoom with the layer
9
+ * markers on it, and the detail column (under the grid on a narrow screen, at the right from 1100px).
10
10
  *
11
11
  * What it reads from the answer, all optional and additive: `cells` (the meetings), `layers` (the dated items, each with
12
12
  * kind, layer, tone and a reference to its home), `free` (free blocks with a suggested use or none), `context` (what came
13
- * out of a past meeting and what moved after it; what is prepared and what to prepare for a coming one) and `layers_note`.
13
+ * out of a past meeting and what moved after it; what is prepared and what to prepare for a coming one), `layers_note`, and
14
+ * `tidy`, `tidy_total`, `tidy_note` (initiative calendar-tidy-up: suggestions by fixed rules, each a proposal in words with
15
+ * references; the Tidy-up chip lists them and marks the meetings they name on the Day and Week timeline).
14
16
  * A key that is missing reads as "the server sent none", in words. Nothing is invented.
15
17
  *
16
18
  * What it does: it selects (a meeting, a marker) and it navigates (a zoom, a day, a reference to the home of an item).
17
19
  * It writes nothing. A marker carries no action: its detail has one reference link that opens the item where it lives. */
18
20
  import { el } from './dom.js';
19
21
  import { c3ic } from './pm-work-view.js';
20
- import { button } from './primitives.js';
22
+ import { button, ensureComponentsCss } from './primitives.js';
23
+ import { toast } from './c3-toast.js';
21
24
 
22
25
  const LAYERS = [
23
26
  ['meetings', 'Meetings'],
@@ -34,7 +37,19 @@ const TONE = { bad: 'Late or at risk of missing', warn: 'At risk', ok: 'On track
34
37
  const REF = { initiative: 'Open the initiative', 'plan-item': 'Open the initiative', item: 'Open in Work', release: 'Open the release', report: 'Open in Reports' };
35
38
  const CTXWORD = { decision: 'Decision', action: 'Action', moved: 'Moved', prepared: 'Prepared', todo: 'To prepare' };
36
39
  const NO_SUGGESTION = 'Free, nothing specific to suggest';
37
- const PPH = 38; // pixels per hour on the Day and Week timelines (the mockup's vday)
40
+ /* the tidy-up suggestions (lib/calendar-tidy.js): the severity groups in order, and each rule in words */
41
+ const TIDY_GROUPS = [['bad', 'Needs a fix'], ['warn', 'At risk'], ['info', 'Worth a look']];
42
+ const TIDY_RULE = { overlap: 'Overlap', 'no-buffer': 'No break', 'prep-risk': 'Prep at risk', 'dead-initiative': 'Stopped initiative',
43
+ 'review-early': 'Review too early', 'likely-delay': 'Late initiative', 'heavy-day': 'Heavy day' };
44
+ const TIDY_RANK = { bad: 0, warn: 1, info: 2 };
45
+ const PPH_PAGE = 46; // pixels per hour on the Day and Week timelines (the mockup's page grid)
46
+ const PPH_OVERLAY = 36; // the same grid in the overlay drawer
47
+ const DAY_LOAD = 6; // the scale of a day's load bar, in hours
48
+ const HOME = { event: 'Calendar', milestone: 'Initiatives', due: 'Initiatives', release: 'Initiatives', report: 'Reports', own: 'Work' }; // where a record lives (the home chip)
49
+ /* the overlay remembers the layers the PM hid, from one opening to the next (the mockup's mem.hide) */
50
+ const mem = { hide: [] };
51
+ /* the page's own Tidy-up choices: what was proposed and what was dismissed. They write nothing (D65) */
52
+ const proposed = new Set(); const dismissed = new Set();
38
53
 
39
54
  const pad = (n) => String(n).padStart(2, '0');
40
55
  const nowIso = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; };
@@ -46,7 +61,7 @@ const setVar = (node, name, value) => { node.style.setProperty('--cal3-' + name,
46
61
  /* ── the page's own state on the instance: which layers are on, whether free time shows, what is selected ─────────── */
47
62
 
48
63
  function state(r) {
49
- const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0])), free: false, sel: null });
64
+ const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0]).filter((id) => !(r.ctx && r.ctx.overlay && mem.hide.includes(id)))), free: true, tidy: false, sel: null });
50
65
  if (r.sel) { st.sel = { type: 'event', id: r.sel }; r.sel = null; } // a meeting chosen on the Briefing (openDay)
51
66
  return st;
52
67
  }
@@ -63,7 +78,11 @@ function model(d, h) {
63
78
  const freeBy = new Map(); if (free) for (const x of free.days) freeBy.set(x.date, x.blocks || []);
64
79
  const ctx = d.context && typeof d.context === 'object' ? d.context : null;
65
80
  const findEvent = (id) => meetings.find((m) => m.e.id === id) || meetings.find((m) => h.baseId(m.e) === id) || null;
66
- return { view, cells, meetingsOf, meetings, layers, free, freeBy, ctx, findEvent };
81
+ const tidy = Array.isArray(d.tidy) ? d.tidy.filter((s) => s && s.id && !dismissed.has(s.id)) : null;
82
+ const tidyBy = new Map(); // event id -> the suggestions that name it, the most severe first
83
+ if (tidy) for (const s of tidy) for (const x of s.refs || []) if (x && x.type === 'event' && x.id) { if (!tidyBy.has(x.id)) tidyBy.set(x.id, []); tidyBy.get(x.id).push(s); }
84
+ for (const list of tidyBy.values()) list.sort((a, b) => (TIDY_RANK[a.severity] ?? 3) - (TIDY_RANK[b.severity] ?? 3));
85
+ return { view, cells, meetingsOf, meetings, layers, free, freeBy, ctx, findEvent, tidy, tidyBy };
67
86
  }
68
87
 
69
88
  const sortMarks = (a, b) => (a.tone === 'bad' ? 0 : 1) - (b.tone === 'bad' ? 0 : 1) || (RANK[a.kind] ?? 9) - (RANK[b.kind] ?? 9) || String(a.date).localeCompare(String(b.date));
@@ -77,12 +96,11 @@ const freeIn = (M, a, b) => (M.free ? M.free.days.filter((x) => x.date >= a && x
77
96
 
78
97
  /** Draw the Calendar tab of instance r into a new node. h is calendar-views.js's own helpers (see PAGE there). */
79
98
  export function drawCalendarPage(host, r, h) {
99
+ ensureComponentsCss(); // the Tidy-up buttons are pmc-btn
80
100
  const st = state(r);
81
101
  const phone = r.root.classList.contains('ph');
82
- const head = el('div', { class: 'cal3-head', 'data-cal3': 'head' }, el('h2', null, 'Calendar'),
83
- el('span', { class: 'dim' }, 'What happens by when: meetings, dates and free time on one grid.'));
84
102
  const bar = h.bar(host, r, phone);
85
- const page = (...kids) => el('div', { class: 'cal3', 'data-cal3': 'page' }, head, bar, ...kids);
103
+ const page = (...kids) => el('div', { class: 'cal3' + (r.ctx.overlay ? ' ov' : ''), 'data-cal3': 'page' }, r.ctx.deck || null, bar, ...kids);
86
104
  if (r.error) return page(fold('alert', 'The calendar could not be read: ' + r.error, { 'data-cv': 'error', role: 'status' }));
87
105
  if (!r.data) return page(fold('clock', 'Reading the repository', { 'data-cv': 'loading', role: 'status' }));
88
106
 
@@ -114,7 +132,7 @@ export function drawCalendarPage(host, r, h) {
114
132
  const body = el('div', { class: 'calbody' + (r.anim && !r.loading ? ' ' + r.anim : '') },
115
133
  el('div', { class: 'cal3-wrap' },
116
134
  el('div', { class: 'cal3-main' }, empty, grid),
117
- el('div', { class: 'cal3-side' }, detail(ui), st.free ? freeList(ui, range) : null, lanes(ui, range))));
135
+ el('div', { class: 'cal3-side' }, detail(ui), st.tidy ? tidyList(ui) : null, st.free ? freeList(ui, range) : null, lanes(ui, range))));
118
136
  return page(chips(ui, range), body);
119
137
  }
120
138
 
@@ -132,10 +150,16 @@ function chips(ui, range) {
132
150
  });
133
151
  const freeN = M.free ? (typeof M.free.count === 'number' ? M.free.count : freeIn(M, range.start, range.end).length) : null;
134
152
  return el('div', { class: 'cal3-chips', role: 'group', 'aria-label': 'Layers', 'data-cal3': 'chips' },
135
- ...LAYERS.map(([id, label]) => chip(id, label, count(id), st.on.has(id), () => { if (st.on.has(id)) st.on.delete(id); else st.on.add(id); }, el('i', { class: 'cal3-sw cal3-sw-' + id }))),
153
+ ...LAYERS.map(([id, label]) => chip(id, label, count(id), st.on.has(id), () => {
154
+ if (st.on.has(id)) st.on.delete(id); else st.on.add(id);
155
+ if (ui.r.ctx.overlay) mem.hide = LAYERS.map((l) => l[0]).filter((x) => !st.on.has(x)); // the overlay remembers what was hidden
156
+ }, el('i', { class: 'cal3-sw cal3-sw-' + id }))),
136
157
  el('span', { class: 'cal3-sep', 'aria-hidden': 'true' }),
137
- chip('free', 'Free time', freeN, st.free, () => { st.free = !st.free; }, c3ic('clock', 'sm')));
158
+ chip('free', 'Free time', freeN, st.free, () => { st.free = !st.free; }, c3ic('clock', 'sm')),
159
+ chip('tidy', 'Tidy-up', tidyCount(M, ui.d), st.tidy, () => { st.tidy = !st.tidy; }, c3ic('spark', 'sm')));
138
160
  }
161
+ /* the number of suggestions in the range: the server's total (the list may be capped), or null when it sent none */
162
+ const tidyCount = (M, d) => (M.tidy ? Math.max(0, (typeof d.tidy_total === 'number' ? d.tidy_total : M.tidy.length) - (d.tidy.length - M.tidy.length)) : null); // less the ones the PM dismissed
139
163
 
140
164
  /* ── a marker: glyph and short title, tone from the tokens; pressing it selects it (the detail has the reference) ─── */
141
165
 
@@ -153,15 +177,37 @@ function marker(ui, m, compact) {
153
177
  const cssId = (s) => String(s).replace(/["\\]/g, '\\$&');
154
178
 
155
179
  /* the markers of a span, the most important first, at most `max`, and a "+N" for the rest */
156
- function markRow(ui, a, b, max, compact) {
180
+ function markRow(ui, a, b, max, compact, view) {
157
181
  const all = marksIn(ui.M, ui.st, a, b);
158
- return [...all.slice(0, max).map((m) => marker(ui, m, compact)), all.length > max ? el('span', { class: 'cal3-more', title: plural(all.length - max, 'more dated item') }, '+' + (all.length - max)) : null];
182
+ return [...all.slice(0, max).map((m) => marker(ui, m, compact)), all.length > max ? moreBtn(ui, all.length - max, a, b, view) : null];
183
+ }
184
+ /* the "+N" of a span is a button: it zooms into the week or the month that holds the rest */
185
+ function moreBtn(ui, n, a, b, view) {
186
+ const label = view === 'week' ? 'Open the week of ' + ui.h.dayLabel(a) : view === 'day' ? 'Open ' + ui.h.dayLabel(a) : 'Open ' + ui.h.MN[Number(a.slice(5, 7)) - 1] + ' ' + a.slice(0, 4);
187
+ return button({ label: '+' + n, variant: 'ghost', size: 'sm', attrs: { class: 'cal3-more', 'data-cal3-more': a, title: plural(n, 'more dated item') + '. ' + label, 'aria-label': plural(n, 'more dated item') + '. ' + label },
188
+ onClick: () => ui.h.go(ui.host, ui.r, { view, date: a, anim: 'zi' }) });
159
189
  }
160
190
 
161
191
  /* ── Day and Week: the timeline with the all-day marker strip above it ───────────────────────────────────────────── */
162
192
 
193
+ /* what a meeting is, for the grid: tied to a promise or a decision (an initiative, prep, or a record PMOS holds), or routine; blocked
194
+ * and prep left come from the event or its context when the server sends them (the mockup's blocked(), kind() and note) */
195
+ function evState(ui, m, nowStr) {
196
+ const { M, h } = ui; const e = m.e;
197
+ const rec = M.ctx ? (M.ctx[e.id] || M.ctx[h.baseId(e)] || null) : null;
198
+ const items = rec && Array.isArray(rec.items) ? rec.items : [];
199
+ const todo = items.some((x) => x.kind === 'todo') || e.prep_todo === true;
200
+ const held = !!rec && rec.held !== false && items.length > 0;
201
+ const standup = /stand-?up/i.test(e.title || '');
202
+ const tied = !standup && !(e.routine || e.mock_routine) && !!(e.initiative || (Array.isArray(e.prep) ? e.prep.length : e.prep) || items.length);
203
+ const past = String(e.end || e.at) <= nowStr;
204
+ return { tied, blocked: e.blocked === true || e.mock_hot === true, todo, held, past, note: past ? (held ? ' · held' : '') : (todo ? ' · prep to do' : '') };
205
+ }
206
+ const hoursOf = (m, h) => Math.max(0, h.endH(m.e) - h.timeOf(m.e.at));
207
+
163
208
  function timeGrid(ui, days) {
164
209
  const { M, st, h, d } = ui;
210
+ const PPH = ui.r.ctx.overlay ? PPH_OVERLAY : PPH_PAGE;
165
211
  const now = nowIso(); const today = d.today;
166
212
  const timedOn = (date) => (st.on.has('meetings') ? M.meetings.filter((m) => m.date === date && h.timeOf(m.e.at) !== null) : []);
167
213
  const allDayOn = (date) => (st.on.has('meetings') ? M.meetings.filter((m) => m.date === date && h.timeOf(m.e.at) === null) : []);
@@ -174,14 +220,27 @@ function timeGrid(ui, days) {
174
220
  const tg = el('div', { class: 'cal3-tg' + (days.length === 1 ? ' one' : ''), 'data-cal3': days.length === 1 ? 'day' : 'week' });
175
221
  setVar(tg, 'n', days.length); setVar(tg, 'hgt', (t1 - t0) * PPH + 'px'); setVar(tg, 'pph', PPH + 'px');
176
222
 
223
+ /* the hours of a day and its load: tied to a promise or a decision, routine, and the rest, on a 6 hour scale */
224
+ const load = (date) => {
225
+ const es = timedOn(date); let tot = 0; let tied = 0;
226
+ for (const m of es) { const x = hoursOf(m, h); tot += x; if (evState(ui, m, now).tied) tied += x; }
227
+ return { tot, tied };
228
+ };
229
+ const seg = (hrs, cls) => setVar(el('i', { class: cls }), 'w', Math.min(100, hrs / DAY_LOAD * 100) + '%');
177
230
  const headRow = el('div', { class: 'cal3-row cal3-dhead' }, el('div', { class: 'cal3-gut' }),
178
- ...days.map((date) => el('div', { class: 'cal3-dh' + (date === today ? ' today' : '') },
179
- days.length > 1 ? button({ label: h.dayLabel(date), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', 'data-cal3-day': date, title: 'Open ' + h.dayLabel(date) }, onClick: () => h.go(ui.host, ui.r, { view: 'day', date, anim: 'zi' }) })
180
- : el('b', null, h.dayLabel(date)))));
231
+ ...days.map((date) => {
232
+ const L = load(date);
233
+ return el('div', { class: 'cal3-dh' + (date === today ? ' today' : ''), 'data-cal3-dh': date },
234
+ el('div', { class: 'dhl' },
235
+ days.length > 1 ? button({ label: h.dayLabel(date), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', 'data-cal3-day': date, title: 'Open ' + h.dayLabel(date) }, onClick: () => h.go(ui.host, ui.r, { view: 'day', date, anim: 'zi' }) })
236
+ : el('b', null, h.dayLabel(date)),
237
+ el('span', { class: 'hrs', 'data-cal3-hrs': date }, L.tot ? +L.tot.toFixed(1) + ' h' : '')),
238
+ el('div', { class: 'lbar', 'aria-hidden': 'true' }, seg(L.tied, 'tied'), seg(L.tot - L.tied, 'rt'), el('i', { class: 'rest' })));
239
+ }));
181
240
 
182
241
  const strip = el('div', { class: 'cal3-row cal3-strip', 'data-cal3': 'strip' }, el('div', { class: 'cal3-gut' }, 'dates'),
183
242
  ...days.map((date) => {
184
- const kids = [...allDayOn(date).map((m) => meetingButton(ui, m, 'cal3-allday')), ...markRow(ui, date, date, days.length === 1 ? 12 : 4, days.length > 1)];
243
+ const kids = [...allDayOn(date).map((m) => meetingButton(ui, m, 'cal3-allday')), ...markRow(ui, date, date, days.length === 1 ? 12 : 4, days.length > 1, 'day')];
185
244
  return el('div', { class: 'cal3-sc', 'data-cal3-strip': date }, ...(kids.filter(Boolean).length ? kids : [el('span', { class: 'cal3-none' }, days.length === 1 ? 'No dated item on this day' : '')]));
186
245
  }));
187
246
 
@@ -195,7 +254,8 @@ function timeGrid(ui, days) {
195
254
  const evs = timedOn(date).sort((a, b) => h.timeOf(a.e.at) - h.timeOf(b.e.at));
196
255
  lanes4(evs, h).forEach(([m, lane, n]) => {
197
256
  const s = h.timeOf(m.e.at); const f = h.endH(m.e);
198
- const b = meetingButton(ui, m, 'vb cal3-ev' + (String(m.e.end || m.e.at) <= now ? ' past' : '') + (m.e.routine || m.e.mock_routine ? ' rt' : ''), [el('b', null, m.e.title || m.e.id), f - s >= 1 ? el('small', null, h.hhmm(s) + ' to ' + h.hhmm(f)) : null]);
257
+ const S = evState(ui, m, now); const rt = !S.tied && !S.held && !S.todo; // routine: a standup, or no initiative, no prep and no record
258
+ const b = meetingButton(ui, m, 'vb cal3-ev' + (S.past ? ' past' : '') + (rt || m.e.routine || m.e.mock_routine ? ' rt' : '') + (S.blocked ? ' hot' : ''), [el('b', null, m.e.title || m.e.id), f - s >= 0.5 ? el('small', null, h.hhmm(s) + ' to ' + h.hhmm(f) + S.note) : null]);
199
259
  setVar(b, 'y', (Y(s) + 1) + 'px'); setVar(b, 'h', Math.max(16, Y(f) - Y(s) - 3) + 'px'); setVar(b, 'l', lane); setVar(b, 'lc', n);
200
260
  col.appendChild(b);
201
261
  });
@@ -203,7 +263,9 @@ function timeGrid(ui, days) {
203
263
  return col;
204
264
  });
205
265
  tg.append(headRow, strip, el('div', { class: 'cal3-row cal3-body' }, hours, ...cols));
206
- return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-scroll' }, tg));
266
+ const key = (cls, text, wrap) => el('span', { class: wrap || null }, el('i', { class: 'lgk ' + cls, 'aria-hidden': 'true' }), text);
267
+ const legend = el('div', { class: 'legend', 'data-cal3': 'legend' }, key('tied', 'tied to a promise or decision'), key('rt', 'routine'), key('fw', 'focus window', 'lgfw'), key('rel', 'release'), key('pro', 'promise'), key('rep', 'report'));
268
+ return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-scroll' }, tg), legend);
207
269
  }
208
270
 
209
271
  /* overlapping meetings share the column side by side: [meeting, lane, lanes in its cluster] */
@@ -218,19 +280,24 @@ function lanes4(evs, h) {
218
280
  function meetingButton(ui, m, cls, label) {
219
281
  const sel = ui.st.sel && ui.st.sel.type === 'event' && (ui.st.sel.id === m.e.id || ui.st.sel.id === ui.h.baseId(m.e));
220
282
  const t = m.e.title || m.e.id; const s = ui.h.timeOf(m.e.at);
221
- const name = t + ', ' + ui.h.dayLabel(m.date) + (s !== null ? ' at ' + ui.h.hhmm(s) : ', all day');
283
+ // with the Tidy-up chip on, a meeting that a suggestion names carries the most severe one's tone and title
284
+ const tidy = ui.st.tidy ? (ui.M.tidyBy.get(m.e.id) || ui.M.tidyBy.get(ui.h.baseId(m.e)) || null) : null;
285
+ const sev = tidy ? tidy[0].severity : null;
286
+ const name = t + ', ' + ui.h.dayLabel(m.date) + (s !== null ? ' at ' + ui.h.hhmm(s) : ', all day') + (tidy ? '. Tidy-up: ' + tidy.map((x) => x.title).join('; ') : '');
222
287
  return button({
223
288
  label: label || t, variant: 'ghost', size: 'sm',
224
- attrs: { class: cls + (sel ? ' sel' : ''), 'data-id': m.e.id, 'data-cal3-ev': m.e.id, 'aria-pressed': String(!!sel), 'aria-label': name, title: name },
289
+ attrs: { class: cls + (sel ? ' sel' : '') + (tidy ? ' cal3-tidy-ev cal3-tidy-' + (TIDY_RANK[sev] !== undefined ? sev : 'info') : ''), 'data-id': m.e.id, 'data-cal3-ev': m.e.id,
290
+ ...(tidy ? { 'data-cal3-tidy': sev } : {}), 'aria-pressed': String(!!sel), 'aria-label': name, title: name },
225
291
  onClick: () => ui.select({ type: 'event', id: m.e.id }, '[data-cal3-ev="' + cssId(m.e.id) + '"]'),
226
292
  });
227
293
  }
228
294
 
229
- /* a free block: the existing hatched window (.vf) with its suggested use, as text only */
295
+ /* a free block: a dashed focus window with its length; the suggested use is in its title and in the Free time list */
230
296
  function freeBlock(ui, date, b, Y) {
231
297
  const s = hmToH(b.from); const f = hmToH(b.to);
232
- const n = el('div', { class: 'vf cal3-free', 'data-cal3-free': date + ' ' + b.from },
233
- el('b', null, b.from + ' to ' + b.to + ' free'), el('span', null, b.suggestion && b.suggestion.text ? b.suggestion.text : NO_SUGGESTION));
298
+ const text = b.suggestion && b.suggestion.text ? b.suggestion.text : NO_SUGGESTION;
299
+ const n = el('div', { class: 'vf cal3-free', 'data-cal3-free': date + ' ' + b.from, title: b.from + ' to ' + b.to + ': ' + text },
300
+ el('b', null, +((f - s)).toFixed(1) + ' h focus'));
234
301
  setVar(n, 'y', (Y(s) + 2) + 'px'); setVar(n, 'h', Math.max(18, Y(f) - Y(s) - 4) + 'px');
235
302
  return n;
236
303
  }
@@ -245,12 +312,10 @@ function monthGrid(ui) {
245
312
  for (let i = 0; i < off; i++) g.appendChild(el('div', { class: 'cal3-mc out', 'aria-hidden': 'true' }));
246
313
  for (const c of days) {
247
314
  const wd = h.dowOf(c.start); const n = st.on.has('meetings') ? M.meetingsOf(c).length : 0;
248
- const fr = st.free ? (M.freeBy.get(c.start) || []).length : 0;
249
315
  g.appendChild(el('div', { class: 'cal3-mc' + (wd === 0 || wd === 6 ? ' we' : '') + (c.start === d.today ? ' today' : ''), 'data-cal3-date': c.start },
250
316
  button({ label: String(Number(c.start.slice(8))), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dn', 'data-cal3-day': c.start, 'aria-label': 'Open ' + h.dayLabel(c.start) + (n ? ', ' + plural(n, 'meeting') : ''), title: 'Open ' + h.dayLabel(c.start) }, onClick: () => h.go(ui.host, ui.r, { view: 'day', date: c.start, anim: 'zi' }) }),
251
317
  n ? el('span', { class: 'cal3-cnt' }, plural(n, 'meeting')) : null,
252
- ...markRow(ui, c.start, c.start, 3, true),
253
- fr ? el('span', { class: 'cal3-fcnt', 'data-cal3-fcnt': c.start }, fr + ' free') : null));
318
+ ...markRow(ui, c.start, c.start, 3, true, 'day')));
254
319
  }
255
320
  return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, g);
256
321
  }
@@ -260,18 +325,16 @@ function monthGrid(ui) {
260
325
  function spanCounts(ui, c) {
261
326
  const { M, st } = ui;
262
327
  const n = st.on.has('meetings') ? M.meetingsOf(c).length : 0;
263
- const fr = st.free ? freeIn(M, c.start, c.end).length : 0;
264
328
  return el('div', { class: 'cal3-counts' },
265
329
  st.on.has('meetings') ? el('span', { class: 'tag' }, plural(n, 'meeting')) : null,
266
- el('span', { class: 'tag' }, plural(marksIn(M, st, c.start, c.end).length, 'dated item')),
267
- st.free ? el('span', { class: 'tag cal3-ftag' }, fr + ' free') : null);
330
+ el('span', { class: 'tag' }, plural(marksIn(M, st, c.start, c.end).length, 'dated item')));
268
331
  }
269
332
 
270
333
  function quarterRows(ui) {
271
334
  const { M, h, d } = ui;
272
335
  const rows = M.cells.map((c) => el('div', { class: 'cal3-qrow' + (c.start <= d.today && d.today <= c.end ? ' now' : ''), 'data-cal3-week': c.start },
273
336
  button({ label: 'Week of ' + h.dayLabel(c.start), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', title: 'Open the week of ' + h.dayLabel(c.start) }, onClick: () => h.go(ui.host, ui.r, { view: 'week', date: c.start, anim: 'zi' }) }),
274
- spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 4, false))));
337
+ spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 4, false, 'week'))));
275
338
  return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-quarter', 'data-cal3': 'quarter' }, ...rows));
276
339
  }
277
340
 
@@ -279,7 +342,7 @@ function yearCards(ui) {
279
342
  const { M, h, d } = ui;
280
343
  const cards = M.cells.map((c) => el('div', { class: 'cal3-ym' + (c.start <= d.today && d.today <= c.end ? ' now' : ''), 'data-cal3-month': c.start.slice(0, 7) },
281
344
  button({ label: h.MN[Number(c.start.slice(5, 7)) - 1] + ' ' + c.start.slice(0, 4), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', title: 'Open the month' }, onClick: () => h.go(ui.host, ui.r, { view: 'month', date: c.start, anim: 'zi' }) }),
282
- spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 3, false))));
345
+ spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 3, false, 'month'))));
283
346
  return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-year', 'data-cal3': 'year' }, ...cards));
284
347
  }
285
348
 
@@ -301,6 +364,10 @@ function openRef(ui, ref, extra) {
301
364
  if (ref.type === 'report') h.gotoTab('rep');
302
365
  }
303
366
 
367
+ /* the home chip: where the record lives (the mockup's UI.homeChip) */
368
+ const homeChip = (name) => el('span', { class: 'chip2', title: 'Where this lives', 'data-cal3': 'home' }, c3ic('repo', 'sm'), name || 'its home');
369
+ const peopleOf = (e) => [].concat(e.people || e.with || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String);
370
+
304
371
  function closeBtn(ui) {
305
372
  return button({ label: 'Close', variant: 'ghost', size: 'sm', attrs: { class: 'cal3-close', 'data-cal3': 'close' }, onClick: () => { ui.st.sel = null; ui.redraw('[data-cal3-chip="meetings"]'); } });
306
373
  }
@@ -315,10 +382,10 @@ function detail(ui) {
315
382
  return box([
316
383
  el('div', { class: 'cal3-dt' }, el('h3', null, c3ic(icon), word), closeBtn(ui)),
317
384
  el('b', { class: 'cal3-title' }, m.title),
318
- el('div', { class: 'chips2' }, el('span', { class: 'tag' }, word), el('span', { class: 'tag' }, h.dayLabel(m.date) + (m.end && m.end !== m.date ? ' to ' + h.dayLabel(m.end) : '')),
385
+ el('div', { class: 'chips2' }, homeChip(HOME[m.kind]), el('span', { class: 'tag' }, word), el('span', { class: 'tag' }, h.dayLabel(m.date) + (m.end && m.end !== m.date ? ' to ' + h.dayLabel(m.end) : '')),
319
386
  m.tone && TONE[m.tone] ? el('span', { class: 'tag cal3-tone-' + m.tone }, TONE[m.tone]) : null, m.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === m.initiative) || {}).title || m.initiative)) : null),
320
387
  el('div', { class: 'cal3-open', 'data-cal3': 'open' }, refLink(ui, m.ref, { initiative: m.initiative, title: m.title })),
321
- el('p', { class: 'dim cal3-note' }, 'A marker shows the date only. You act on the item where it lives.'),
388
+ el('p', { class: 'dim cal3-note' }, m.kind === 'own' ? 'A marker shows the date only. You act on the item where it lives.' : 'This date lives here. Change it where the plan is kept, not on the grid.'),
322
389
  ], { 'data-cal3-sel': m.id });
323
390
  }
324
391
  }
@@ -341,21 +408,24 @@ function meetingDetail(ui, hit) {
341
408
  const rows = items.filter((x) => kinds.includes(x.kind));
342
409
  return el('div', { class: 'cal3-ctx', 'data-cal3-ctx': key }, el('h4', null, title),
343
410
  rows.length ? el('ul', { class: 'cal3-cl', role: 'list' }, ...rows.map((x) => el('li', { class: 'cal3-ci', 'data-cal3-item': x.kind },
344
- el('span', { class: 'tag' }, CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
411
+ el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, x.done && x.kind === 'todo' ? 'Done' : CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
345
412
  x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
346
413
  refLink(ui, x.ref, { title: x.text })))) : el('p', { class: 'dim cal3-note' }, none));
347
414
  };
348
415
  let body;
349
416
  if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
350
- else if (!rec || rec.held === false) body = [fold('moon', 'PMOS holds nothing for this meeting: no note, no decision and no linked item.', { 'data-cal3': 'nothing-held' })];
417
+ else if (!rec || rec.held === false) body = [fold('moon', past ? 'Nothing held for this meeting: no note, decisions or linked items yet.' : 'Nothing to prepare for this meeting.', { 'data-cal3': 'nothing-held' })];
351
418
  else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
419
+ else if (!items.some((x) => x.kind === 'prepared' || x.kind === 'todo')) body = [el('p', { class: 'dim cal3-note', 'data-cal3': 'nothing-to-prepare' }, 'Nothing to prepare for this meeting.')];
352
420
  else body = [list(['prepared'], 'Prepared', 'prepared', 'Nothing is prepared yet.'), list(['todo'], 'To prepare', 'todo', 'Nothing is left to prepare.')];
353
421
  return [
354
422
  el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('cal'), 'Meeting'), el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), closeBtn(ui)),
355
423
  el('b', { class: 'cal3-title' }, e.title || e.id),
356
424
  el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
425
+ el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag' }, n)),
426
+ e.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === e.initiative) || {}).title || e.initiative)) : null),
357
427
  ...body,
358
- rec && rec.held !== false ? el('p', { class: 'dim cal3-note' }, 'Only what PMOS holds a record for. Each line opens its source.') : null,
428
+ el('p', { class: 'dim cal3-note' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : 'PMOS proposes changes. It never edits your calendar.'),
359
429
  ];
360
430
  }
361
431
 
@@ -365,15 +435,64 @@ function freeList(ui, range) {
365
435
  const head = el('h3', null, c3ic('clock'), 'Free time');
366
436
  if (!M.free) return el('section', { class: 'card cal3-freel', 'data-cal3': 'free-list' }, head, fold('info', 'The calendar sent no free time for this range.', { 'data-cal3': 'no-free' }));
367
437
  const all = freeIn(M, range.start, range.end);
368
- const cap = 12;
438
+ const small = ui.M.view === 'day' || ui.M.view === 'week';
439
+ const cap = 8;
369
440
  return el('section', { class: 'card cal3-freel', 'data-cal3': 'free-list' }, head,
370
441
  el('p', { class: 'dim cal3-note' }, 'Blocks of ' + (M.free.min_minutes || 90) + ' minutes or more between ' + (M.free.day_start || '09:00') + ' and ' + (M.free.day_end || '18:00') + '. A suggestion is a proposal: PMOS writes nothing.'),
371
- all.length ? el('ul', { class: 'cal3-cl', role: 'list' }, ...all.slice(0, cap).map((b) => el('li', { class: 'cal3-ci', 'data-cal3-fb': b.date + ' ' + b.from },
442
+ !small ? fold('info', 'Free time shows in the Day and Week views.', { 'data-cal3': 'free-small' })
443
+ : all.length ? el('ul', { class: 'cal3-cl', role: 'list' }, ...all.slice(0, cap).map((b) => el('li', { class: 'cal3-ci', 'data-cal3-fb': b.date + ' ' + b.from },
372
444
  el('span', { class: 'dim mono cal3-at' }, h.dayLabel(b.date) + ' ' + b.from + ' to ' + b.to),
373
445
  el('span', { class: 'cal3-ct' }, b.suggestion && b.suggestion.text ? b.suggestion.text : NO_SUGGESTION),
374
446
  b.suggestion && b.suggestion.ref ? refLink(ui, b.suggestion.ref, { title: b.suggestion.text }) : null)))
375
447
  : el('p', { class: 'dim cal3-note' }, 'No free block of ' + (M.free.min_minutes || 90) + ' minutes or more in this range.'),
376
- all.length > cap ? el('p', { class: 'dim mono cal3-note' }, '+' + (all.length - cap) + ' more blocks in this range') : null);
448
+ small && all.length > cap ? el('p', { class: 'dim mono cal3-note' }, '+' + (all.length - cap) + ' more blocks in this range') : null);
449
+ }
450
+
451
+ /* the tidy-up suggestions of the range, grouped by severity: what is wrong, why, the proposed change and the references
452
+ * to the items involved. A suggestion is a proposal in words; the panel has no control that changes anything. */
453
+ function tidyList(ui) {
454
+ const { M, d, h } = ui;
455
+ const head = el('h3', null, c3ic('spark'), 'Tidy-up');
456
+ const box = (...kids) => el('section', { class: 'card cal3-tidyl', 'data-cal3': 'tidy-list', 'aria-label': 'Tidy-up suggestions' }, head, ...kids);
457
+ const notes = d.tidy_note && typeof d.tidy_note === 'object' ? Object.keys(d.tidy_note).filter((k) => d.tidy_note[k]) : [];
458
+ const noteList = notes.length ? el('div', { class: 'cal3-ctx', 'data-cal3': 'tidy-notes' }, el('h4', null, 'Not checked'),
459
+ el('ul', { class: 'cal3-cl', role: 'list' }, ...notes.map((k) => el('li', { class: 'cal3-ci', 'data-cal3-tidy-note': k },
460
+ el('span', { class: 'tag' }, TIDY_RULE[k] || k), el('span', { class: 'cal3-ct dim' }, String(d.tidy_note[k])))))) : null;
461
+ if (!M.tidy) return box(fold('info', 'The server sent no tidy-up list.', { 'data-cal3': 'no-tidy' }), noteList);
462
+ const lead = el('p', { class: 'dim cal3-note' }, 'Each suggestion is a proposal. PMOS changes nothing in your calendar: you make the change where the meeting or the item lives.');
463
+ if (!M.tidy.length) return box(lead, el('p', { class: 'dim cal3-note', 'data-cal3': 'tidy-empty' }, 'Nothing to tidy in this range.'), noteList);
464
+ const total = tidyCount(M, d);
465
+ const refOf = (s, x) => {
466
+ if (x.type === 'event') {
467
+ const hit = M.findEvent(x.id);
468
+ if (!hit) return el('span', { class: 'dim cal3-noptr' }, 'meeting not drawn');
469
+ return el('a', { href: '#', class: 'cal3-ref', 'data-cal3-ref': 'event:' + x.id, title: 'Show the meeting: ' + (hit.e.title || x.id),
470
+ onClick: (ev) => { ev.preventDefault(); ui.select({ type: 'event', id: hit.e.id }, '[data-cal3-ev="' + cssId(hit.e.id) + '"]'); } }, c3ic('cal', 'sm'), hit.e.title || 'Show the meeting');
471
+ }
472
+ const ini = (s.refs || []).find((y) => y && y.type === 'initiative');
473
+ return refLink(ui, x, { title: s.title, initiative: ini ? ini.id : undefined });
474
+ };
475
+ const groups = TIDY_GROUPS.map(([sev, label]) => {
476
+ const rows = M.tidy.filter((s) => s.severity === sev);
477
+ if (!rows.length) return null;
478
+ return el('div', { class: 'cal3-ctx', 'data-cal3-tidy-group': sev }, el('h4', { class: 'cal3-tone-' + sev }, label + ' (' + rows.length + ')'),
479
+ el('ul', { class: 'cal3-cl', role: 'list' }, ...rows.map((s) => el('li', { class: 'cal3-ci cal3-tidy-i cal3-tidy-' + sev, 'data-cal3-tidy-item': s.id, 'data-cal3-rule': s.rule },
480
+ el('span', { class: 'tag cal3-tone-' + sev }, TIDY_RULE[s.rule] || s.rule),
481
+ el('span', { class: 'dim mono cal3-at' }, h.dayLabel(s.date)),
482
+ el('b', { class: 'cal3-tidy-t' }, s.title),
483
+ s.why ? el('p', { class: 'cal3-note cal3-tidy-why' }, s.why) : null,
484
+ s.suggestion ? el('p', { class: 'cal3-note cal3-tidy-fix', 'data-cal3': 'tidy-fix' }, el('b', null, 'Proposed: '), s.suggestion) : null,
485
+ el('span', { class: 'cal3-tidy-refs' }, ...(s.refs || []).filter((x) => x && x.type && x.id).map((x) => refOf(s, x))),
486
+ /* a proposal is a note on this page and nothing more: no control here changes the calendar or sends a word (D65) */
487
+ el('span', { class: 'iacts' },
488
+ proposed.has(s.id) ? el('span', { class: 'tag cal3-up', 'data-cal3-proposed': s.id }, 'Proposed')
489
+ : button({ label: 'Propose it', variant: 'outline', size: 'sm', attrs: { 'data-cal3-act': 'propose', 'data-cal3-key': s.id },
490
+ onClick: () => { proposed.add(s.id); toast('Proposal: ' + String(s.suggestion || s.title).replace(/\.$/, '') + '. Nothing changed in your calendar.'); ui.redraw('[data-cal3-proposed="' + cssId(s.id) + '"]'); } }),
491
+ button({ label: 'Dismiss', variant: 'ghost', size: 'sm', attrs: { 'data-cal3-act': 'dismiss', 'data-cal3-key': s.id },
492
+ onClick: () => { dismissed.add(s.id); toast('Dismissed'); ui.redraw('[data-cal3-chip="tidy"]'); } }))))));
493
+ });
494
+ return box(lead, ...groups.filter(Boolean),
495
+ total > M.tidy.length ? el('p', { class: 'dim mono cal3-note' }, 'Showing ' + M.tidy.length + ' of ' + total + ' suggestions, the most severe first.') : null, noteList);
377
496
  }
378
497
 
379
498
  /* each lane in words: its count in the range, or why it is empty */
@@ -1,7 +1,7 @@
1
1
  /* pm/c3-calpop.js — the calendar as a popup over any page of the console (briefing-brief PRD R7, R9).
2
2
  *
3
3
  * mountCalPopupTrigger(toolsHost) adds the calendar button to the top bar's tools, and binds the key C (once)
4
- * openCalendarPopup(opener?) opens the full calendar (five zooms, no ribbon) in a dialog over the page
4
+ * openCalendarPopup(opener?) opens the Calendar page's own grid (five zooms, chips, detail, Free time, lanes) in a drawer over the page
5
5
  * closeCalendarPopup(back?) closes it; `back` (default true) returns the focus to where it was
6
6
  * lastZoom() the zoom last used, "day" until the PM changes it
7
7
  *
@@ -22,7 +22,7 @@ const store = {
22
22
  get() { try { return localStorage.getItem(KEY); } catch { return null; } },
23
23
  set(v) { try { localStorage.setItem(KEY, v); } catch { /* no storage */ } },
24
24
  };
25
- let zoom = ZOOMS.includes(store.get()) ? store.get() : 'day';
25
+ let zoom = ZOOMS.includes(store.get()) ? store.get() : 'week'; // the overlay opens on the Week (the mockup's mem.zoom)
26
26
  let cur = null; // { wrap, host, opener, off, obs, stopPhone }
27
27
  let keyBound = false;
28
28
 
@@ -57,8 +57,8 @@ export function openCalendarPopup(opener) {
57
57
  const close = () => closeCalendarPopup(true);
58
58
  const host = el('div', { class: 'calpop-host', 'data-c3pg': 'calpop-host' });
59
59
  const closeBtn = c3ib('Close', 'x', { title: 'Close (Esc)', 'data-c3pg': 'calpop-close', onclick: close }, 'sm');
60
- const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic('cal'), el('b', null, 'Calendar')), el('span', { class: 'chips2' }, closeBtn));
61
- const dlg = el('div', { class: 'sheet bs calpop', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Calendar', tabindex: '-1', 'data-c3pg': 'calpop' },
60
+ const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic('cal'), el('b', null, 'Calendar')), el('span', { class: 'chips2' }, el('span', { class: 'dim calpop-esc' }, 'Esc to close'), closeBtn));
61
+ const dlg = el('div', { class: 'sheet bs dw calpop', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Calendar', tabindex: '-1', 'data-c3pg': 'calpop' },
62
62
  el('div', { class: 'grab' }), head, el('div', { class: 'sb' }, host));
63
63
  const wrap = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'calpop' }, el('div', { class: 'scrim', onclick: close }), dlg);
64
64
  const stopPhone = c3Phone(wrap);
@@ -73,7 +73,7 @@ export function openCalendarPopup(opener) {
73
73
  document.addEventListener('keydown', onKey, true);
74
74
  document.body.appendChild(wrap);
75
75
  cur = { wrap, host, opener: from, onKey, stopPhone, obs: null };
76
- mountCalendarViews(host, { view: zoom, scope: undefined });
76
+ mountCalendarViews(host, { view: zoom, page: true, overlay: true }); // the same page grid as the Calendar tab, 36px an hour, no deck
77
77
  /* remember the zoom as it changes, so a reload of the page also keeps it */
78
78
  cur.obs = new MutationObserver(() => { const z = readZoom(host); if (z) { zoom = z; store.set(z); } });
79
79
  cur.obs.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ['aria-pressed'] });