create-pmos 0.223.0 → 0.224.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.223.0",
3
+ "version": "0.224.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@8cbfda9
1
+ kernel-source: pmos@f8c3934
2
2
  assembled: 2026-10-14
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-14T05:39:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:57:56Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-14T05:39:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:57:56Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-14T05:39:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:57:56Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: the Calendar tab of the console page"
4
4
  description: "The Calendar tab of the console page (/): Calendar and Notes, the five zooms, the highlights, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
5
- timestamp: 2026-10-13
5
+ timestamp: 2026-10-14
6
6
  ---
7
7
 
8
8
  # Guide: the Calendar tab of the console page
@@ -79,11 +79,13 @@ Press a name to open the item in the detail column. When nothing significant fal
79
79
 
80
80
  ## The grid
81
81
 
82
- - **Day** shows the timeline of the day. The markers of the day are in a strip above it.
82
+ - **Day** shows the timeline of the day. The markers of the day are in a strip above it, side by side on one line.
83
83
  - **Week** shows a timeline for each day. Each day name has the hours of meetings of that day and a small bar. The bar has three parts: time tied to a promise or a decision, routine time, and the rest of a 6 hour day. The markers of each day are in a strip under the day name. Press a day name to open that day.
84
84
  - **Month** shows a cell for each day. A cell shows the highlight of the day in one sentence, for example "SSO general release ships." Under it, a quiet line counts the rest, for example "1 due · 2 routine". Press that line or the day number to open the day. A line under the grid says what is counted and not named. With **Routine** on, a cell shows the number of meetings and up to three markers instead, and "+2" opens the day with the other markers.
85
85
  - **Quarter** shows a column for each month. **Year** shows a column for each quarter, with a button for each of its three months. Each column shows its highlight and a quiet line, for example "24 meetings · 3 due · 9 routine". Quarter and Year are stricter than Month: they name only milestones, releases, reports that go out and late items. Press a column's title to open that month or quarter. With **Routine** on, a column shows its markers instead.
86
86
 
87
+ On Day and Week, the reports of a day are in one **Reports** chip with their number, for example "Reports 2". On Day it also says what they are, for example "2 scheduled · 1 not issued". Press the chip to show the reports under it, each with its state: **scheduled**, **not issued** or **issued**. Press it again to fold them. A report that was not issued after its day is late for a week: it stays out of the chip with an amber edge. The reports of a horizon scan are documents, not report dates, so they are not on the calendar. Two report schedules with the same name show once on a day.
88
+
87
89
  Titles wrap onto more lines. They are never cut.
88
90
 
89
91
  A marker has a symbol and a short title. A red edge means late or at risk of missing. An amber edge means at risk.
@@ -96,7 +98,9 @@ Press a marker to show its detail in the side column. The detail shows the title
96
98
 
97
99
  ## Select a meeting
98
100
 
99
- Press a meeting to show its context in the side column. Under the title, a row shows where the meeting lives, the people and the initiative. Each line has a link to its source.
101
+ Press a meeting to open it in a popup at the right of the screen, as a session opens. The popup has four sizes: **Normal**, **Half**, **2/3** and **Whole**. It opens at the size you used last. Press `Esc`, **Close** or the page behind it to close it. When the meeting has a note, at **Half** and wider the note is beside the meeting: its title, the first lines of its summary and **Open the note**. **Focus**, **Open the note** and a link to another page close the popup first.
102
+
103
+ In the popup, a row shows where the meeting lives, the people and the initiative. Each line has a link to its source. When all the lines come from one note, the note is named once under them. The calendar popup over other pages shows the meeting in its side column instead.
100
104
 
101
105
  - For a past meeting, **What came out of it** lists the decisions and actions of its note. **What moved after it** lists the items that changed within 48 hours and refer to the meeting or its initiative.
102
106
  - For a coming meeting, the detail shows these parts. A part with no record is left out.
@@ -32,6 +32,7 @@ const PER_DAY = 60; // minutes of a whole working day befor
32
32
  const DAY_START = 9 * 60;
33
33
  const DAY_END = 18 * 60;
34
34
  const WARN_DAYS = 2;
35
+ const LATE_DAYS = 7; // a report not issued after its day is late (warn) for this many days
35
36
  const SUGGEST_DAYS = 7;
36
37
  const WINDOW_MS = 48 * 3600000;
37
38
  const PER_KIND = { decision: 6, action: 6, moved: 8, prepared: 8, todo: 8 };
@@ -132,15 +133,23 @@ function layersFrom(dated, range, today) {
132
133
  if (!r || !r.cadence || !isDay(r.slot)) continue;
133
134
  const date = r.issued && isDay(r.issued.at) ? day10(r.issued.at) : day10(r.slot);
134
135
  n++;
136
+ // a report left unissued after its day is late for a week, then it is history (a daily draft nobody issued would otherwise warn forever)
137
+ const late = !r.issued && date < today && diffDays(today, date) <= LATE_DAYS;
135
138
  put('reports', { id: `report:${r.cadence}:${day10(r.slot)}`, date, kind: 'report', layer: 'reports', title: `${r.cadence} report${r.issued ? '' : ' (not issued)'}`,
136
- tone: r.issued ? 'ok' : null, cadence: r.cadence, routine: routineCadence(r.cadence), ref: { type: 'report', id: `${r.cadence}/${day10(r.slot)}` } });
139
+ tone: r.issued ? 'ok' : late ? 'warn' : null, state: r.issued ? 'issued' : 'not issued', cadence: r.cadence, routine: routineCadence(r.cadence), ref: { type: 'report', id: `${r.cadence}/${day10(r.slot)}` } });
137
140
  }
141
+ // two schedules with the same name that run on the same day are one run on the calendar
142
+ const runs = new Set();
138
143
  for (const s of d.reportSchedules || []) {
139
144
  if (!s || !s.id) continue;
145
+ const name = str(s.name) || s.cadence || s.id;
140
146
  for (const dt of s.dates || []) {
141
147
  if (!isDay(dt)) continue;
148
+ const key = name.toLowerCase() + '\u0000' + day10(dt);
149
+ if (runs.has(key)) continue;
150
+ runs.add(key);
142
151
  n++;
143
- put('reports', { id: `report-sched:${s.id}:${day10(dt)}`, date: day10(dt), kind: 'report', layer: 'reports', title: `${str(s.name) || s.cadence || s.id} (scheduled)`, tone: null,
152
+ put('reports', { id: `report-sched:${s.id}:${day10(dt)}`, date: day10(dt), kind: 'report', layer: 'reports', title: `${name} (scheduled)`, tone: null, state: 'scheduled',
144
153
  ...(s.cadence ? { cadence: s.cadence } : {}), routine: routineCadence(s.cadence) || routineCadence(s.name), ref: { type: 'report', id: s.cadence || s.id } });
145
154
  }
146
155
  }
@@ -117,7 +117,8 @@ function readers(root, runtimeDir) {
117
117
  } catch (e) { unavailable.releases = 'the releases file could not be read: ' + reason(e); }
118
118
  try {
119
119
  const RN = require('./report-needs.js');
120
- out.reports = RN.reportsOf(root).reports.map((r) => ({ cadence: r.cadence, slot: r.slot, issued: r.issued ? { at: r.issued.at } : null }));
120
+ // a horizon scan writes reports/scan-<type>/<day>.md (lib/scan.js planPath): a finished document, not a report slot to issue
121
+ out.reports = RN.reportsOf(root).reports.filter((r) => !/^scan-/.test(r.cadence)).map((r) => ({ cadence: r.cadence, slot: r.slot, issued: r.issued ? { at: r.issued.at } : null }));
121
122
  out.reportSchedules = reportSchedules(RN, range);
122
123
  } catch (e) { unavailable.reports = 'the report folders could not be read: ' + reason(e); }
123
124
  try { out.own = ownRows().map((o) => ({ id: o.id, title: o.title, due: o.due, done: !!o.done })); } catch (e) { unavailable.own = 'your items could not be read: ' + reason(e); }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.223.0",
3
+ "version": "0.224.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.223.0",
9
+ "version": "0.224.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.223.0",
3
+ "version": "0.224.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -6,7 +6,8 @@
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
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).
9
+ * markers on it, and the detail column (under the grid on a narrow screen, at the right from 1100px). A meeting opens in a popup
10
+ * sheet instead, as a session does (initiative c3-cal-strip-popup); a dated marker keeps its card in the column.
10
11
  *
11
12
  * What it reads from the answer, all optional and additive: `cells` (the meetings), `layers` (the dated items, each with
12
13
  * kind, layer, tone and a reference to its home), `free` (free blocks with a suggested use or none), `context` (what came
@@ -24,6 +25,7 @@ import { toast } from './c3-toast.js';
24
25
  import { pmPost } from './http.js';
25
26
  import { notesView, openNote, noteOfEvent, primeNotes } from './c3-notes.js'; // Calendar › Notes (initiative c3-notes-place)
26
27
  import { onNotes } from './notes-links.js';
28
+ import { openSheet } from './c3-sheet.js';
27
29
 
28
30
  const LAYERS = [
29
31
  ['meetings', 'Meetings'],
@@ -109,6 +111,7 @@ export function drawCalendarPage(host, r, h) {
109
111
  const redraw0 = () => h.redraw(host, r);
110
112
  const modes = r.ctx.overlay ? null : modeSeg(st, redraw0);
111
113
  if (st.mode === 'notes' && modes) { // Calendar › Notes: the list and the open note; the zoom and the period have nothing to step here
114
+ if (st.sel && st.sel.type === 'event') { st.sel = null; closeMeetingSheet(r); }
112
115
  return page(el('div', { class: 'calbar' }, el('div', { class: 'lens' }, modes)), notesView({ host, r, h, st, redraw: redraw0 }));
113
116
  }
114
117
  if (!r.notesOff) { // a meeting's "Open the note" needs the shared notes read; a new note redraws the open detail
@@ -122,7 +125,10 @@ export function drawCalendarPage(host, r, h) {
122
125
  if (!r.data) return pageBar(fold('clock', 'Reading the repository', { 'data-cv': 'loading', role: 'status' }));
123
126
 
124
127
  const d = r.data; const M = model(d, h);
125
- const redraw = (focusSel) => { h.redraw(host, r); if (focusSel) { const n = r.root.querySelector(focusSel); if (n) n.focus(); } };
128
+ const redraw = (focusSel) => {
129
+ h.redraw(host, r);
130
+ if (focusSel) { const n = r.root.querySelector(focusSel) || (meet && meet.r === r ? meet.root.querySelector(focusSel) : null); if (n) n.focus(); }
131
+ };
126
132
  const select = (sel, focusSel) => {
127
133
  st.sel = sel; redraw(focusSel);
128
134
  const det = r.root.querySelector('[data-cal3="detail"]');
@@ -150,9 +156,64 @@ export function drawCalendarPage(host, r, h) {
150
156
  el('div', { class: 'cal3-wrap' },
151
157
  el('div', { class: 'cal3-main' }, empty, empty ? null : highlights(ui, range), grid),
152
158
  el('div', { class: 'cal3-side' }, detail(ui), st.tidy ? tidyList(ui) : null, st.free ? freeList(ui, range) : null, lanes(ui, range))));
159
+ syncMeetingSheet(ui);
153
160
  return pageBar(chips(ui, range), body);
154
161
  }
155
162
 
163
+ /* ── a meeting in a popup (initiative c3-cal-strip-popup): the session sheet, with its Normal, Half, 2/3 and Whole sizes ── */
164
+ const MEET_SIZE_KEY = 'pmos.workspace.c3.meeting-size';
165
+ const MEET_SIZES = [['normal', 'Normal', 'Normal'], ['half', 'Half', 'Half of the window'], ['two-thirds', '2/3', 'Two-thirds of the window'], ['whole', 'Whole', 'Whole window']];
166
+ const meetStore = {
167
+ get() { try { return localStorage.getItem(MEET_SIZE_KEY); } catch { return null; } },
168
+ set(v) { try { localStorage.setItem(MEET_SIZE_KEY, v); } catch { /* no storage */ } },
169
+ };
170
+ let meet = null; // the open popup: { r, id, close, root, sheet, body, title }
171
+
172
+ export function closeMeetingSheet(r) {
173
+ if (!meet || (r && meet.r !== r)) return;
174
+ const m = meet; meet = null; m.quiet = true; m.close();
175
+ }
176
+
177
+ /* open, refresh or close the popup so it matches the selection; the page calls it on every draw */
178
+ function syncMeetingSheet(ui) {
179
+ const { st, M, r, h } = ui;
180
+ const hit = !r.ctx.overlay && st.sel && st.sel.type === 'event' ? M.findEvent(st.sel.id) : null;
181
+ if (!hit) { closeMeetingSheet(r); return; }
182
+ const box = el('section', { class: 'cal3 cal3-detail cal3-meet', 'data-cal3': 'detail', 'aria-label': 'Meeting', 'data-cal3-sel': hit.e.id }, ...meetingDetail(ui, hit, true));
183
+ const title = hit.e.title || hit.e.id;
184
+ if (meet && meet.r === r && meet.root.isConnected) {
185
+ meet.body.replaceChildren(box);
186
+ if (meet.id !== hit.e.id) { meet.id = hit.e.id; meet.title.textContent = title; meet.sheet.setAttribute('aria-label', title); meet.body.parentElement.scrollTop = 0; }
187
+ return;
188
+ }
189
+ let size = MEET_SIZES.some((x) => x[0] === meetStore.get()) ? meetStore.get() : 'half';
190
+ const btns = MEET_SIZES.map(([k, label, tip]) => button({ label, variant: 'ghost', attrs: { 'aria-pressed': String(k === size), 'data-cal3-size': k, title: tip, 'aria-label': tip },
191
+ onClick: () => setSize(k) }));
192
+ const seg = el('div', { class: 'seg cal3-msize', role: 'group', 'aria-label': 'Size', 'data-cal3': 'meeting-size' }, ...btns);
193
+ const self = {};
194
+ const o = openSheet({ title, icon: 'cal', wide: true, body: box, actions: [seg], cls: 'cal3-ms tsz-' + size,
195
+ onClose: () => { if (meet === self) meet = null; if (self.quiet) return; if (st.sel && st.sel.type === 'event') { st.sel = null; h.redraw(ui.host, r); } } });
196
+ Object.assign(self, { r, id: hit.e.id, close: o.close, root: o.root, sheet: o.sheet, body: o.body, title: o.sheet.querySelector('.sh2 b') });
197
+ o.root.setAttribute('data-cal3-meeting', '');
198
+ meet = self;
199
+ function setSize(k) {
200
+ size = k; meetStore.set(k);
201
+ MEET_SIZES.forEach(([x]) => o.sheet.classList.toggle('tsz-' + x, x === k));
202
+ btns.forEach((b) => b.setAttribute('aria-pressed', String(b.getAttribute('data-cal3-size') === k)));
203
+ }
204
+ }
205
+
206
+ /* the note of a meeting beside what came out of it: its title, its first summary lines and "Open the note" */
207
+ function noteCard(ui, e) {
208
+ const n = noteOfEvent(e); if (!n) return null;
209
+ const lines = (n.summary_lines || []).filter(Boolean);
210
+ return el('div', { class: 'cal3-ctx cal3-mnote', 'data-cal3-ctx': 'note' }, el('h4', null, 'Note'),
211
+ el('div', { class: 'cal3-mnote-b' }, el('b', null, n.title || 'Untitled note'),
212
+ ...(lines.length ? lines.map((t) => el('p', null, t)) : [el('p', { class: 'dim' }, n.private ? 'A private note. Its summary stays in the note.' : 'No summary yet.')])),
213
+ el('a', { href: '#', class: 'cal3-ref', 'data-cal3': 'open-note', 'data-cal3-open-note': n.id, title: 'Open the note: ' + (n.title || 'Untitled note'),
214
+ onClick: (ev) => { ev.preventDefault(); openNote(ui.r, n.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the note'));
215
+ }
216
+
156
217
  /* "Open the note": the note recorded for this meeting, on Calendar › Notes; nothing when no note names the meeting */
157
218
  function noteLink(ui, e) {
158
219
  if (ui.r.ctx.overlay) return null;
@@ -196,18 +257,49 @@ const tidyCount = (M, d) => (M.tidy ? Math.max(0, (typeof d.tidy_total === 'numb
196
257
 
197
258
  /* ── a marker: glyph and short title, tone from the tokens; pressing it selects it (the detail has the reference) ─── */
198
259
 
199
- function marker(ui, m, compact) {
260
+ function marker(ui, m, compact, tagged) {
200
261
  const [word, icon] = kindOf(m);
201
262
  const sel = ui.st.sel && ui.st.sel.type === 'marker' && ui.st.sel.id === m.id;
202
263
  const name = word + ': ' + m.title + ', ' + ui.h.dayLabel(m.date) + (m.tone && TONE[m.tone] ? '. ' + TONE[m.tone] : '');
264
+ // inside the Reports fold the state is a tag after the name: "Daily report draft scheduled"
265
+ const label = tagged && m.state ? [c3ic(icon, 'sm'), el('span', { class: 'cal3-mk-t' }, untag(m.title)), el('span', { class: 'cal3-mk-s' }, m.state)]
266
+ : [c3ic(icon, 'sm'), el('span', { class: 'cal3-mk-t' }, m.title)];
203
267
  return button({
204
- label: [c3ic(icon, 'sm'), el('span', { class: 'cal3-mk-t' }, m.title)], variant: 'ghost', size: 'sm',
268
+ label, variant: 'ghost', size: 'sm',
205
269
  attrs: { class: 'cal3-mk cal3-k-' + (KIND[m.kind] ? m.kind : 'other') + (m.tone ? ' cal3-tone-' + m.tone : '') + (compact ? ' compact' : '') + (sel ? ' sel' : ''),
206
270
  'data-cal3-mk': m.id, 'data-cal3-layer': m.layer, 'data-cal3-kind': m.kind, 'aria-pressed': String(!!sel), 'aria-label': name, title: name },
207
271
  onClick: () => ui.select({ type: 'marker', id: m.id }, '[data-cal3-mk="' + cssId(m.id) + '"]'),
208
272
  });
209
273
  }
210
274
  const cssId = (s) => String(s).replace(/["\\]/g, '\\$&');
275
+ const untag = (t) => String(t).replace(/\s*\((scheduled|not issued)\)$/, '');
276
+
277
+ /* the dates strip of one day (initiative c3-cal-strip-popup): every report of the day folds into one Reports chip that opens in place,
278
+ * except a late one, which stays out with the other dated items. A Day lays them in one wrapping line; a Week column stacks them. */
279
+ function stripMarks(ui, date, one) {
280
+ const all = marksIn(ui.M, ui.st, date, date);
281
+ const folded = all.filter((m) => m.kind === 'report' && m.tone !== 'warn' && m.tone !== 'bad');
282
+ const rest = all.filter((m) => !folded.includes(m));
283
+ const max = one ? 12 : 4;
284
+ const out = [...rest.slice(0, max).map((m) => marker(ui, m, !one)), rest.length > max ? moreBtn(ui, rest.length - max, date, date, 'day') : null];
285
+ if (!folded.length) return out;
286
+ const st = ui.st;
287
+ if (!st.repOpen) st.repOpen = new Set();
288
+ const held = st.sel && st.sel.type === 'marker' && folded.some((m) => m.id === st.sel.id); // a selected report keeps its fold open
289
+ const open = st.repOpen.has(date) || held;
290
+ const n = (s) => folded.filter((m) => m.state === s).length;
291
+ const words = ['scheduled', 'not issued', 'issued'].filter((s) => n(s)).map((s) => n(s) + ' ' + s).join(' · ');
292
+ const say = plural(folded.length, 'report') + ' on ' + ui.h.dayLabel(date) + (words ? ': ' + words : '') + '. ' + (open ? 'Hide them' : 'Show them');
293
+ out.push(button({
294
+ label: [c3ic('report', 'sm'), el('span', { class: 'cal3-rf-t' }, 'Reports'), el('span', { class: 'cal3-n' }, String(folded.length)),
295
+ one && words ? el('span', { class: 'cal3-rf-bd' }, words) : null],
296
+ variant: 'ghost', size: 'sm',
297
+ attrs: { class: 'cal3-rf', 'data-cal3-rf': date, 'aria-expanded': String(open), 'aria-label': say, title: say },
298
+ onClick: () => { if (open) { st.repOpen.delete(date); if (held) st.sel = null; } else st.repOpen.add(date); ui.redraw('[data-cal3-rf="' + date + '"]'); },
299
+ }));
300
+ if (open) out.push(el('div', { class: 'cal3-rf-open', 'data-cal3-rf-open': date }, ...folded.map((m) => marker(ui, m, false, true))));
301
+ return out;
302
+ }
211
303
 
212
304
  /* the markers of a span, the most important first, at most `max`, and a "+N" for the rest */
213
305
  function markRow(ui, a, b, max, compact, view) {
@@ -273,8 +365,8 @@ function timeGrid(ui, days) {
273
365
 
274
366
  const strip = el('div', { class: 'cal3-row cal3-strip', 'data-cal3': 'strip' }, el('div', { class: 'cal3-gut' }, 'dates'),
275
367
  ...days.map((date) => {
276
- const kids = [...allDayOn(date).map((m) => meetingButton(ui, m, 'cal3-allday')), ...markRow(ui, date, date, days.length === 1 ? 12 : 4, days.length > 1, 'day')];
277
- 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' : '')]));
368
+ const kids = [...allDayOn(date).map((m) => meetingButton(ui, m, 'cal3-allday')), ...stripMarks(ui, date, days.length === 1)];
369
+ return el('div', { class: 'cal3-sc' + (days.length === 1 ? ' line' : ''), 'data-cal3-strip': date }, ...(kids.filter(Boolean).length ? kids : [el('span', { class: 'cal3-none' }, days.length === 1 ? 'No dated item on this day' : '')]));
278
370
  }));
279
371
 
280
372
  const hours = el('div', { class: 'cal3-hours', 'aria-hidden': 'true' });
@@ -519,6 +611,8 @@ function yearCards(ui) {
519
611
 
520
612
  function refLink(ui, ref, extra) {
521
613
  if (!ref || !ref.type || !ref.id) return el('span', { class: 'dim cal3-noptr' }, 'no pointer');
614
+ if (ref.type === 'note' && !ui.r.ctx.overlay) return el('a', { href: '#', class: 'cal3-ref', 'data-cal3-ref': 'note:' + ref.id, title: 'Open the note',
615
+ onClick: (ev) => { ev.preventDefault(); openNote(ui.r, ref.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the note');
522
616
  if (!REF[ref.type]) return el('span', { class: 'dim mono cal3-noptr' }, ref.type + ' ' + ref.id);
523
617
  return el('a', { href: '#', class: 'cal3-ref', 'data-cal3-ref': ref.type + ':' + ref.id, title: REF[ref.type] + ': ' + ref.id,
524
618
  onClick: (ev) => { ev.preventDefault(); openRef(ui, ref, extra || {}); } }, c3ic('out', 'sm'), REF[ref.type]);
@@ -527,6 +621,7 @@ function refLink(ui, ref, extra) {
527
621
  /* open the home of a reference: an initiative on Initiatives, a work item on Work, a report on Reports */
528
622
  function openRef(ui, ref, extra) {
529
623
  const { h, host, r } = ui;
624
+ if (ui.st.sel && ui.st.sel.type === 'event') { ui.st.sel = null; closeMeetingSheet(r); } // the popup does not follow you to another page
530
625
  if (ref.type === 'initiative') { if (!h.goInitiative(ref.id)) h.openInit(host, r, ref.id, extra.title); return; }
531
626
  if (ref.type === 'plan-item' || ref.type === 'release') { const ini = extra.initiative || ref.initiative; if (!(ini && h.goInitiative(ini))) h.gotoTab('init'); return; }
532
627
  if (ref.type === 'item') { h.gotoTab('work'); return; }
@@ -562,18 +657,19 @@ function detail(ui) {
562
657
  }
563
658
  if (st.sel && st.sel.type === 'event') {
564
659
  const hit = M.findEvent(st.sel.id);
565
- if (hit) return box(meetingDetail(ui, hit), { 'data-cal3-sel': hit.e.id });
660
+ if (hit && ui.r.ctx.overlay) return box(meetingDetail(ui, hit), { 'data-cal3-sel': hit.e.id }); // the calendar popup is a drawer already
566
661
  }
567
662
  return box([el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('info'), 'Detail')),
568
663
  el('p', { class: 'dim cal3-note' }, 'Select a meeting to see what came out of it or what to prepare. Select a dated marker to see its date and open its home.')]);
569
664
  }
570
665
 
571
666
  /** The Meeting focus page (initiative c3-meeting-focus): one calm full-screen page for a meeting that is today. It loads when pressed. */
572
- const focusBtn = (e, date) => button({ label: [c3ic('target', 'sm'), 'Focus'], variant: 'primary', size: 'sm',
667
+ /* from the popup, Focus closes it first: the focus page is the whole screen and has its own sheets (c3-cal-strip-popup) */
668
+ const focusBtn = (e, date, ui) => button({ label: [c3ic('target', 'sm'), 'Focus'], variant: 'primary', size: 'sm',
573
669
  attrs: { 'data-cal3': 'focus', title: 'Open the meeting focus page: the clock, the topics, the material and PMOS beside the conversation' },
574
- onClick: () => { import('./c3-focus.js').then((m) => m.openFocus({ event: e, date })).catch(() => toast({ text: 'The focus page could not load.', tone: 'error' })); } });
670
+ onClick: () => { if (ui) { ui.st.sel = null; closeMeetingSheet(ui.r); ui.redraw(); } import('./c3-focus.js').then((m) => m.openFocus({ event: e, date })).catch(() => toast({ text: 'The focus page could not load.', tone: 'error' })); } });
575
671
 
576
- function meetingDetail(ui, hit) {
672
+ function meetingDetail(ui, hit, inSheet) {
577
673
  const { M, h } = ui; const e = hit.e;
578
674
  const s = h.timeOf(e.at); const f = s === null ? null : h.endH(e);
579
675
  const rec = M.ctx ? (M.ctx[e.id] || M.ctx[h.baseId(e)] || null) : null;
@@ -582,11 +678,15 @@ function meetingDetail(ui, hit) {
582
678
  const items = rec && Array.isArray(rec.items) ? rec.items : [];
583
679
  const list = (kinds, title, key, none) => {
584
680
  const rows = items.filter((x) => kinds.includes(x.kind));
681
+ // lines that all come from one note name it once, under the list, instead of a pointer on every line
682
+ const r0 = rows.length > 1 && rows[0].ref && rows[0].ref.type === 'note' ? rows[0].ref : null;
683
+ const one = r0 && rows.every((x) => x.ref && x.ref.type === 'note' && x.ref.id === r0.id) ? r0 : null;
585
684
  return el('div', { class: 'cal3-ctx', 'data-cal3-ctx': key }, el('h4', null, title),
586
685
  rows.length ? el('ul', { class: 'cal3-cl', role: 'list' }, ...rows.map((x) => el('li', { class: 'cal3-ci', 'data-cal3-item': x.kind },
587
686
  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),
588
687
  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,
589
- refLink(ui, x.ref, { title: x.text })))) : el('p', { class: 'dim cal3-note' }, none));
688
+ one ? null : refLink(ui, x.ref, { title: x.text })))) : el('p', { class: 'dim cal3-note' }, none),
689
+ one ? el('p', { class: 'dim cal3-note cal3-src', 'data-cal3': 'from-note' }, 'From the meeting note. ', refLink(ui, one)) : null);
590
690
  };
591
691
  let body;
592
692
  if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
@@ -599,18 +699,31 @@ function meetingDetail(ui, hit) {
599
699
  const iniTitle = iniId ? (b && b.initiative && b.initiative.id === iniId && b.initiative.title) || (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).title) || iniId : null;
600
700
  const iniStatus = b && b.initiative && b.initiative.id === iniId ? b.initiative.status : (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).status || null);
601
701
  const sources = b && b.sources && b.sources.length ? 'Put together from ' + listWords(b.sources) + '. Each line opens its source. ' : '';
702
+ const chipsRow = () => el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag', 'data-cal3': 'person' }, n)),
703
+ iniId ? el('span', { class: 'tag', 'data-cal3': 'meeting-initiative' }, 'Initiative: ' + iniTitle) : null,
704
+ iniStatus && STATUS_TONE[iniStatus] ? el('span', { class: 'tag cal3-tone-' + STATUS_TONE[iniStatus] }, iniStatus) : null);
705
+ const tail = el('p', { class: 'dim cal3-note', 'data-cal3': past ? 'past-note' : 'sources' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : sources + 'PMOS proposes changes. It never edits your calendar.');
706
+ if (inSheet) {
707
+ // the popup: the sheet's header carries the title and Close; at Half and wider the note sits beside what came out of it
708
+ const side = noteCard(ui, e);
709
+ return [
710
+ el('div', { class: 'cal3-dt' }, el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date, ui) : null),
711
+ el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
712
+ chipsRow(),
713
+ past || !M.ctx ? null : levelRow(ui, e, lv),
714
+ el('div', { class: 'cal3-mcols' + (side ? '' : ' one') }, el('div', { class: 'cal3-mcol' }, ...body, tail), side ? el('div', { class: 'cal3-mcol' }, side) : null),
715
+ ];
716
+ }
602
717
  return [
603
718
  el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('cal'), 'Meeting'), el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'),
604
719
  !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date) : null, closeBtn(ui)),
605
720
  el('b', { class: 'cal3-title' }, e.title || e.id),
606
721
  el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
607
- el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag', 'data-cal3': 'person' }, n)),
608
- iniId ? el('span', { class: 'tag', 'data-cal3': 'meeting-initiative' }, 'Initiative: ' + iniTitle) : null,
609
- iniStatus && STATUS_TONE[iniStatus] ? el('span', { class: 'tag cal3-tone-' + STATUS_TONE[iniStatus] }, iniStatus) : null),
722
+ chipsRow(),
610
723
  past || !M.ctx ? null : levelRow(ui, e, lv),
611
724
  noteLink(ui, e),
612
725
  ...body,
613
- el('p', { class: 'dim cal3-note', 'data-cal3': past ? 'past-note' : 'sources' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : sources + 'PMOS proposes changes. It never edits your calendar.'),
726
+ tail,
614
727
  ];
615
728
  }
616
729
 
@@ -603,6 +603,21 @@
603
603
  .c3 .cal3-dh.today b,.c3 .cal3-dh.today button.cal3-dbtn{color:var(--acc)}
604
604
  .c3 .cal3-gut{font:var(--fs-1) var(--mono);color:var(--muted-foreground);padding:6px 4px}
605
605
  .c3 .cal3-sc{border-left:1px solid var(--border);padding:3px;display:grid;gap:2px;align-content:start;min-height:28px;min-width:0}
606
+ .c3 .cal3-sc.line{display:flex;flex-wrap:wrap;align-items:center;gap:4px}
607
+ .c3 .cal3-sc.line button.cal3-mk,.c3 .cal3-sc.line button.cal3-allday{width:auto;max-width:280px;flex:0 1 auto}
608
+ /* the Reports fold of the dates strip (initiative c3-cal-strip-popup) */
609
+ .c3 .cal3 button.cal3-rf{display:inline-flex;align-items:center;gap:6px;width:auto;max-width:100%;height:auto;min-height:22px;padding:1px 8px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--foreground);font:500 var(--fs-1)/1.4 var(--sans);cursor:pointer;white-space:nowrap;overflow:hidden}
610
+ .c3 .cal3-sc:not(.line) button.cal3-rf{width:100%}
611
+ .c3 .cal3 button.cal3-rf>span{display:flex;gap:6px;align-items:center;min-width:0}
612
+ .c3 .cal3-rf .ic{color:var(--muted-foreground)}
613
+ .c3 .cal3-rf .cal3-n{font:500 var(--fs-1) var(--mono);color:var(--acc)}
614
+ .c3 .cal3-rf-bd{font:var(--fs-1) var(--mono);color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;min-width:0}
615
+ .c3 .cal3 button.cal3-rf[aria-expanded="true"]{background:var(--accw)}
616
+ .c3 .cal3-rf-open{display:grid;gap:2px;min-width:0}
617
+ .c3 .cal3-sc.line .cal3-rf-open{flex-basis:100%;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:3px}
618
+ .c3 .cal3-mk-s{margin-left:auto;padding-left:8px;font:var(--fs-1) var(--mono);color:var(--muted-foreground);flex:none}
619
+ .c3 .cal3 button.cal3-mk:has(.cal3-mk-s)>span{width:100%}
620
+ @media (hover:hover){.c3 .cal3 button.cal3-rf:hover{background:var(--accw)}}
606
621
  .c3 .cal3-none{font:var(--fs-1) var(--mono);color:var(--muted-foreground);padding:4px}
607
622
  .c3 .cal3-hours{position:relative;height:var(--cal3-hgt)}
608
623
  .c3 .cal3-hours span{position:absolute;top:var(--cal3-y);right:6px;font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
@@ -714,7 +729,7 @@
714
729
  .c3 .cal3 button.cal3-mk:hover{background:var(--accw)}
715
730
  }
716
731
  @media (pointer:coarse){
717
- .c3 .cal3 button.cal3-chip,.c3 .cal3 button.cal3-mk,.c3 .cal3 button.cal3-dn,.c3 .cal3 button.cal3-dbtn,.c3 .cal3 button.cal3-close,.c3 .cal3 button.cal3-allday,.c3 .cal3 a.cal3-ref{min-height:44px}
732
+ .c3 .cal3 button.cal3-chip,.c3 .cal3 button.cal3-mk,.c3 .cal3 button.cal3-dn,.c3 .cal3 button.cal3-dbtn,.c3 .cal3 button.cal3-close,.c3 .cal3 button.cal3-allday,.c3 .cal3 button.cal3-rf,.c3 .cal3 a.cal3-ref{min-height:44px}
718
733
  }
719
734
  @media (prefers-reduced-motion:reduce){
720
735
  .c3 .cal3 *{animation:none;transition:none}
@@ -2369,3 +2384,14 @@
2369
2384
  .c3ns-err { color: var(--destructive); font-size: var(--fs-2); margin: 0; }
2370
2385
  .c3ns-err:empty { display: none; }
2371
2386
  .c3ns-go { display: flex; justify-content: flex-end; }
2387
+ /* a meeting in a popup (initiative c3-cal-strip-popup): the session sheet's sizes; at Half and wider the note sits beside what came out of it */
2388
+ .c3 .cal3-meet{display:grid;gap:var(--sp-3);min-width:0}
2389
+ .c3 .cal3-msize{flex:none}
2390
+ .c3 .cal3-msize button{padding:4px 8px}
2391
+ .c3.ph .cal3-msize{display:none}
2392
+ .c3 .cal3-mcols{display:grid;gap:var(--sp-4);grid-template-columns:minmax(0,1fr);align-items:start}
2393
+ .c3 .cal3-mcol{display:grid;gap:var(--sp-3);min-width:0;align-content:start}
2394
+ @media (min-width:761px){.c3:not(.ph) .sheet.cal3-ms:not(.tsz-normal) .cal3-mcols:not(.one){grid-template-columns:minmax(0,1.2fr) minmax(0,1fr)}}
2395
+ .c3 .cal3-mnote-b{display:grid;gap:4px;padding:var(--sp-2) var(--sp-3);border:1px solid var(--border);border-radius:8px;background:var(--muted);min-width:0;overflow-wrap:anywhere}
2396
+ .c3 .cal3-mnote-b p{margin:0;font-size:var(--fs-2)}
2397
+ .c3 .cal3-src{display:flex;flex-wrap:wrap;gap:6px;align-items:center}