create-pmos 0.143.0 → 0.144.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.143.0",
3
+ "version": "0.144.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@7aa3b30
1
+ kernel-source: pmos@9f8623c
2
2
  assembled: 2026-10-12
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-12T01:40:01Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:54:19Z"}
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-12T01:40:01Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:54:19Z"}
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-12T01:40:01Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:54:19Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,14 +1,30 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Guide: the Calendar tab of the console page"
4
- description: "The Calendar tab of the console page (/c3.html): the five zooms, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
5
- timestamp: 2026-10-11
4
+ description: "The Calendar tab of the console page (/c3.html): Calendar and Notes, the five zooms, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
5
+ timestamp: 2026-10-12
6
6
  ---
7
7
 
8
8
  # Guide: the Calendar tab of the console page
9
9
 
10
10
  The Calendar tab of the console page (`/c3.html`) shows time. It shows what happens and what falls due, by date. At the top it shows a short deck of headline cards about your calendar, for example a conflict or a meeting with no time to prepare. The deck shows nothing when PMOS has no card about your calendar. The tab shows no task list and no strategy panel. Your due items live in Work. The strategy and progress panels live in Reports and Initiatives.
11
11
 
12
+ ## Calendar and Notes
13
+
14
+ The switch at the start of the bar has two parts. **Calendar** shows the grid. **Notes** shows your notes.
15
+
16
+ In **Notes**, the list is at the left. The notes are in groups, one for each initiative. **No initiative** is the last group. The number shows how many notes the list shows. Type in **Search notes** to keep only the notes whose title, initiative or summary has the words. A row shows when the note was made and its length. A row also shows **recording** or **paused** while the note is held, **no meeting** when the note has no meeting, and a warning mark when a statement differs from a file.
17
+
18
+ Press a row to open the note at the right. The note has three stages. Press **Before**, **During** or **After** to change the stage.
19
+
20
+ - **Before** shows what the note kept about its meeting: the agenda, what the meeting is for, what to do to be ready, what to read first and the people. A tick shows an agenda item that was heard in the transcript. A note with no meeting says so, and has **Link to a meeting** and **Link to an initiative**.
21
+ - **During** shows the live notes while the note is recorded or paused. At other times it shows the transcript.
22
+ - **After** shows the overview, the decisions with why, the next steps with owner and due date, the open questions, the statements that differ from a file and the highlights. The transcript is folded at the bottom. When the final notes are written, click a text to correct it. Leave the text to save it. Press `Esc` to cancel. **Make task** adds a next step to the note's initiative.
23
+
24
+ A note that is recorded now opens on **During**. Other notes open on **After**. **Show in the calendar** goes back to the grid, on the day of the meeting, with the meeting selected. The copy button puts the note on the clipboard as Markdown.
25
+
26
+ In **Notes**, the zoom, the arrows and the calendar keys do nothing.
27
+
12
28
  ## Zoom and move
13
29
 
14
30
  Use **Day**, **Week**, **Month**, **Quarter** or **Year** to change the zoom. Use the arrows to go to the previous or the next period. Press **Today** to come back to today.
@@ -53,6 +69,8 @@ Press a meeting to show its context in the side column. Under the title, a row s
53
69
  - 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.
54
70
  - For a coming meeting, **Prepared** lists what is ready, and **To prepare** lists what is still open.
55
71
 
72
+ When the meeting has a note, the detail has **Open the note**. It opens the note in **Notes**.
73
+
56
74
  When PMOS holds nothing for a meeting, the detail says so. It does not guess.
57
75
 
58
76
  ## Free time
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: notes"
4
4
  description: "How to take notes in a meeting: one note top to bottom, typed notes as timed lines, key points written each minute and labelled by Jev, delete a note; final notes written after Stop from the whole recording, with speakers and a quote for each item, decisions with their why and who, next steps with an open question, a discussion paragraph, and Wispr Flow's notes merged in and fetched by hand when the fetch fails; listen to your microphone and the meeting on this machine, see decisions, actions and questions caught as they are said, check them against the repository for conflicts, capture highlights during the meeting, create tasks from next steps, correct and tag lines, see what changed in your notes, replay the meeting, see screenshots of the shared screen beside the transcript, link a note to its meeting and its initiative, and get a summary."
5
- timestamp: 2026-10-11
5
+ timestamp: 2026-10-12
6
6
  ---
7
7
 
8
8
  # Guide: notes
@@ -22,7 +22,7 @@ Starting a note, from the Record control or from **Take notes** on a calendar ev
22
22
 
23
23
  ### The Record control in the console
24
24
 
25
- The console (the page at `/c3.html`) has the same Record control in its top bar, beside the Sessions button. It is always there, also when nothing is recording. **Record** (the microphone) starts an untitled note and listens at once. The arrow lists the meetings of today that have not started. Press one to start a note linked to that calendar event. While a meeting records, the red group shows its name and clock, **Highlight**, **Pause** and **Stop**. While it is paused, the group shows **Resume** and **Stop**. **Stop** asks first and offers **Pause instead**. A red dot shows on the Calendar tab while a meeting is held. The console has no Notes page, so the name in the red group opens the note in the main app.
25
+ The console (the page at `/c3.html`) has the same Record control in its top bar, beside the Sessions button. It is always there, also when nothing is recording. **Record** (the microphone) starts an untitled note and listens at once. The arrow lists the meetings of today that have not started. Press one to start a note linked to that calendar event. While a meeting records, the red group shows its name and clock, **Highlight**, **Pause** and **Stop**. While it is paused, the group shows **Resume** and **Stop**. **Stop** asks first and offers **Pause instead**. A red dot shows on the Calendar tab while a meeting is held. Press the name in the red group to open the note on the console's Calendar tab, in **Notes**, on the **During** stage. See [the Calendar tab of the console page](console-calendar.md).
26
26
 
27
27
  When Whisper or Qwen3-ASR transcribes the meeting, the red group also names the engine and the language, for example **Whisper · English**. Check it when lines come out in the wrong language: the language is set in the Notes settings.
28
28
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.143.0",
3
+ "version": "0.144.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.143.0",
9
+ "version": "0.144.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.143.0",
3
+ "version": "0.144.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",
@@ -21,6 +21,8 @@ import { el } from './dom.js';
21
21
  import { c3ic } from './pm-work-view.js';
22
22
  import { button, ensureComponentsCss } from './primitives.js';
23
23
  import { toast } from './c3-toast.js';
24
+ import { notesView, openNote, noteOfEvent, primeNotes } from './c3-notes.js'; // Calendar › Notes (initiative c3-notes-place)
25
+ import { onNotes } from './notes-links.js';
24
26
 
25
27
  const LAYERS = [
26
28
  ['meetings', 'Meetings'],
@@ -63,6 +65,8 @@ const setVar = (node, name, value) => { node.style.setProperty('--cal3-' + name,
63
65
  function state(r) {
64
66
  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 });
65
67
  if (r.sel) { st.sel = { type: 'event', id: r.sel }; r.sel = null; } // a meeting chosen on the Briefing (openDay)
68
+ if (r.pendingMode) { st.mode = r.pendingMode; r.pendingMode = null; } // a note opened before the page drew (the Record chip)
69
+ if (!st.mode) st.mode = 'cal';
66
70
  return st;
67
71
  }
68
72
 
@@ -99,10 +103,21 @@ export function drawCalendarPage(host, r, h) {
99
103
  ensureComponentsCss(); // the Tidy-up buttons are pmc-btn
100
104
  const st = state(r);
101
105
  const phone = r.root.classList.contains('ph');
106
+ const page = (b, ...kids) => el('div', { class: 'cal3' + (r.ctx.overlay ? ' ov' : ''), 'data-cal3': 'page', 'data-cal3-show': st.mode }, r.ctx.deck || null, b, ...kids);
107
+ const redraw0 = () => h.redraw(host, r);
108
+ const modes = r.ctx.overlay ? null : modeSeg(st, redraw0);
109
+ if (st.mode === 'notes' && modes) { // Calendar › Notes: the list and the open note; the zoom and the period have nothing to step here
110
+ return page(el('div', { class: 'calbar' }, el('div', { class: 'lens' }, modes)), notesView({ host, r, h, st, redraw: redraw0 }));
111
+ }
112
+ if (!r.notesOff) { // a meeting's "Open the note" needs the shared notes read; a new note redraws the open detail
113
+ r.notesOff = onNotes(() => { if (r.root.isConnected && st.mode === 'cal' && st.sel) h.redraw(host, r); });
114
+ primeNotes();
115
+ }
102
116
  const bar = h.bar(host, r, phone);
103
- const page = (...kids) => el('div', { class: 'cal3' + (r.ctx.overlay ? ' ov' : ''), 'data-cal3': 'page' }, r.ctx.deck || null, bar, ...kids);
104
- if (r.error) return page(fold('alert', 'The calendar could not be read: ' + r.error, { 'data-cv': 'error', role: 'status' }));
105
- if (!r.data) return page(fold('clock', 'Reading the repository', { 'data-cv': 'loading', role: 'status' }));
117
+ if (modes) { const lens = bar.querySelector('.lens'); if (lens) lens.prepend(modes); }
118
+ const pageBar = (...kids) => page(bar, ...kids);
119
+ if (r.error) return pageBar(fold('alert', 'The calendar could not be read: ' + r.error, { 'data-cv': 'error', role: 'status' }));
120
+ if (!r.data) return pageBar(fold('clock', 'Reading the repository', { 'data-cv': 'loading', role: 'status' }));
106
121
 
107
122
  const d = r.data; const M = model(d, h);
108
123
  const redraw = (focusSel) => { h.redraw(host, r); if (focusSel) { const n = r.root.querySelector(focusSel); if (n) n.focus(); } };
@@ -133,7 +148,22 @@ export function drawCalendarPage(host, r, h) {
133
148
  el('div', { class: 'cal3-wrap' },
134
149
  el('div', { class: 'cal3-main' }, empty, grid),
135
150
  el('div', { class: 'cal3-side' }, detail(ui), st.tidy ? tidyList(ui) : null, st.free ? freeList(ui, range) : null, lanes(ui, range))));
136
- return page(chips(ui, range), body);
151
+ return pageBar(chips(ui, range), body);
152
+ }
153
+
154
+ /* "Open the note": the note recorded for this meeting, on Calendar › Notes; nothing when no note names the meeting */
155
+ function noteLink(ui, e) {
156
+ if (ui.r.ctx.overlay) return null;
157
+ const n = noteOfEvent(e); if (!n) return null;
158
+ return el('div', { class: 'cal3-ctx', 'data-cal3-ctx': 'note' }, el('h4', null, 'Notes'),
159
+ el('a', { href: '#', class: 'cal3-ref', 'data-cal3': 'open-note', 'data-cal3-open-note': n.id, title: 'Open the note: ' + (n.title || 'Untitled note'),
160
+ onClick: (ev) => { ev.preventDefault(); openNote(ui.r, n.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the note'));
161
+ }
162
+
163
+ /* the Calendar | Notes switch, before the zoom (the mockup the PM approved on 2026-10-12) */
164
+ function modeSeg(st, redraw) {
165
+ const b = (id, icon, label) => { const x = button({ variant: 'ghost', attrs: { class: 'cal3-b', 'aria-pressed': String(st.mode === id), 'data-cal3-mode': id, title: label }, onClick: () => { if (st.mode !== id) { st.mode = id; redraw(); } } }); x.replaceChildren(c3ic(icon, 'sm'), el('span', null, label)); return x; };
166
+ return el('div', { class: 'seg', role: 'group', 'aria-label': 'Show', 'data-cal3': 'mode' }, b('cal', 'cal', 'Calendar'), b('notes', 'notes', 'Notes'));
137
167
  }
138
168
 
139
169
  function fold(icon, text, attrs) { return el('div', { class: 'fold', ...(attrs || {}) }, c3ic(icon, 'sm'), text); }
@@ -424,6 +454,7 @@ function meetingDetail(ui, hit) {
424
454
  el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
425
455
  el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag' }, n)),
426
456
  e.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === e.initiative) || {}).title || e.initiative)) : null),
457
+ noteLink(ui, e),
427
458
  ...body,
428
459
  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.'),
429
460
  ];
@@ -0,0 +1,316 @@
1
+ /* pm/c3-notes.js — Calendar › Notes on the console3 Calendar tab (initiative c3-notes-place, PRD done-criteria 1 to 7).
2
+ *
3
+ * c3-calendar.js draws it when the Calendar | Notes switch is on Notes: notesView(ui) returns one persistent box, which this
4
+ * file redraws on its own (a read, a choice, a stage, a search), so the grid's data call is not repeated.
5
+ *
6
+ * the list "By initiative" with a count and a search field, one group per initiative and "No initiative" last (dest-notes.js
7
+ * groupNotes). A row: the time or day, the length, recording or paused, "no meeting", and a dot for a conflict.
8
+ * the note the Calendar stage's layout (calendar-views.js stageNode, the mockup the PM approved on 2026-10-12): a head with
9
+ * Before, During and After, then the stage's cards.
10
+ * Before the meeting context the note kept (meeting.context): agenda (an item heard is ticked), what it is for, to be ready,
11
+ * links and references, people. A note with no meeting says so and offers the main app's link pickers.
12
+ * During the main app's live block while the note is recorded or paused (view-notes.js renderLive), else the transcript.
13
+ * After the Notes card (decisions with why, next steps with owner, due and Make task, questions, conflicts, highlights,
14
+ * the folded transcript) from final_notes, else from what was caught live (dest-notes.js partsOf).
15
+ *
16
+ * Nothing is invented: a part the note does not hold is left out or says so. The mockup's Rejected column, Settle, told and
17
+ * not told, Route actions and the drafts have no data or route yet, so they are not drawn.
18
+ *
19
+ * openNote(r, id, phase) shows a note (from the Record chip or a meeting's detail) on the instance r
20
+ * noteOfEvent(e) the note linked to a calendar event, from the shared notes read (notes-links.js), or null
21
+ */
22
+ import { el } from './dom.js';
23
+ import { button } from './primitives.js';
24
+ import { c3ic } from './pm-work-view.js';
25
+ import { pmGet, pmPost } from './http.js';
26
+ import { toast } from './c3-toast.js';
27
+ import { onNotes, notesFor, refreshNotes } from './notes-links.js';
28
+ import { agendaHeard, peopleOf } from './notes-context.js';
29
+ import { groupNotes, partsOf, conflictsOf, whenShort } from './dest-notes.js';
30
+ import * as dock from './note-dock.js';
31
+ import * as V from './view-notes.js';
32
+ import { makeTask } from './notes-task.js';
33
+
34
+ const PHASES = [['before', 'clip', 'Before'], ['during', 'mic', 'During'], ['after', 'notes', 'After']];
35
+ const startOf = (n) => (n.meeting && n.meeting.start) || n.recorded_from || n.created_at || n.updated_at;
36
+ const iniName = (i) => (i ? i.title || i.id : null);
37
+ const mins = (ms) => (ms >= 60000 ? Math.round(ms / 60000) + ' min' : '');
38
+ const lenOf = (n) => n.meeting_ms || Math.max(0, ...(n.transcript || []).map((x) => x.t || 0));
39
+ const dayOf = (iso) => { const t = new Date(iso); return Number.isFinite(t.getTime()) ? t.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) : ''; };
40
+ const hm = (iso) => { const t = new Date(iso); return Number.isFinite(t.getTime()) ? String(t.getHours()).padStart(2, '0') + ':' + String(t.getMinutes()).padStart(2, '0') : ''; };
41
+ const avatar = (name) => el('span', { class: 'av', title: name }, String(name).split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase());
42
+ /* A native button from primitives.js drawn as the console3 control `cls` (chip2 lk, ib, sgb, …); cal3-b takes the primitive's own
43
+ * height, padding and hover off (console3.css), as c3-ini-ui.js btn() does with ini-b. */
44
+ function btn(cls, attrs, kids, onClick) {
45
+ const b = button({ variant: 'ghost', onClick, attrs: { ...(attrs || {}), class: 'cal3-b ' + cls } });
46
+ b.replaceChildren(...[].concat(kids).filter((k) => k != null && k !== false));
47
+ return b;
48
+ }
49
+ const fold = (icon, text, attrs) => el('div', { class: 'fold', ...(attrs || {}) }, c3ic(icon, 'sm'), text);
50
+ const held = (id) => { const s = dock.recordState(); return s.mode !== 'idle' && s.id === id ? s.mode : null; };
51
+ const linked = (n) => !!(n.meeting && (n.meeting.kind === 'event' || n.meeting.kind === 'adhoc'));
52
+ const transcriptOf = (n) => (n.final && n.final.segments && n.final.segments.length ? n.final.segments.map((g) => ({ t: g.t, text: g.text, who: g.spk })) : (n.transcript || []).filter((x) => !x.dropped).map((x) => ({ t: x.t, text: x.text, who: x.source })));
53
+
54
+ /** The notes state on the calendar instance r. */
55
+ function stateOf(r) {
56
+ if (!r.notes) r.notes = { box: el('div', { class: 'cal3-notes', 'data-cal3': 'notes' }), rows: null, err: null, open: null, note: null, phase: null, q: '', seq: 0, live: null, unsub: null };
57
+ return r.notes;
58
+ }
59
+
60
+ /** The note the Calendar shows next: from the Record chip (phase During) or a meeting's detail (its default stage). */
61
+ export function openNote(r, id, phase) {
62
+ const s = stateOf(r);
63
+ s.open = id; s.note = null; s.phase = phase || null;
64
+ r.page = r.page || null;
65
+ if (r.page) r.page.mode = 'notes';
66
+ else r.pendingMode = 'notes';
67
+ }
68
+
69
+ /** The note linked to this calendar event (its id, its id without the day of a repeat, or its start and title), from the
70
+ * shared read. The calendar's events carry their start as `at`. Null when none is known. */
71
+ export function noteOfEvent(e) {
72
+ if (!e) return null;
73
+ const start = e.start || e.at; const base = String(e.id || '').split('@')[0];
74
+ for (const x of [{ id: e.id, start, title: e.title }, { id: base, start, title: e.title }]) { const f = notesFor(x); if (f.length) return f[0]; }
75
+ return null;
76
+ }
77
+
78
+ /** The Notes view: the list and the note, in one box that keeps its identity across the page's redraws. */
79
+ export function notesView(ui) {
80
+ const s = stateOf(ui.r);
81
+ s.ui = ui;
82
+ if (!s.unsub) s.unsub = onNotes(() => { if (s.box.isConnected) read(s); });
83
+ if (!s.rows || (s.open && (!s.note || s.note.id !== s.open))) read(s);
84
+ draw(s);
85
+ return s.box;
86
+ }
87
+
88
+ async function read(s) {
89
+ const seq = ++s.seq;
90
+ const [l, one] = await Promise.all([pmGet('/api/notes'), s.open ? pmGet('/api/notes/one?id=' + encodeURIComponent(s.open)) : null]);
91
+ if (seq !== s.seq) return;
92
+ if (l && l.ok) { s.rows = l.notes || []; s.err = null; } else { s.rows = s.rows || []; s.err = (l && l.error) || 'The notes could not be read.'; }
93
+ if (!s.open && s.rows.length) { s.open = s.rows[0].id; return read(s); }
94
+ if (one && one.ok && one.note && one.note.id === s.open) s.note = one.note;
95
+ draw(s);
96
+ }
97
+
98
+ function choose(s, id) {
99
+ if (s.open === id) return;
100
+ s.open = id; s.note = null; s.phase = null;
101
+ draw(s); read(s);
102
+ }
103
+
104
+ function draw(s) {
105
+ if (!s.box.isConnected && s.drawn) { s.drawn = false; }
106
+ // a field being typed in is not redrawn under the cursor; the live block keeps its own fields
107
+ const active = typeof document !== 'undefined' ? document.activeElement : null;
108
+ if (active && s.box.contains(active) && active.matches('textarea, [contenteditable="true"]') && !active.closest('[data-nt-part="live"]')) return;
109
+ const q = s.box.querySelector('[data-cal3-notes="search"]');
110
+ const typing = !!q && active === q; const caret = typing ? q.selectionStart : null;
111
+ s.box.replaceChildren(el('div', { class: 'cal3-nwrap' }, listCard(s), noteCol(s)));
112
+ s.drawn = true;
113
+ if (typing) { const nq = s.box.querySelector('[data-cal3-notes="search"]'); if (nq) { nq.focus(); nq.setSelectionRange(caret, caret); } }
114
+ }
115
+
116
+ /* ── the list ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
117
+
118
+ function listCard(s) {
119
+ const qtext = s.q.trim().toLowerCase();
120
+ const shown = (s.rows || []).filter((r) => !qtext || String(r.title || '').toLowerCase().includes(qtext) || String(iniName(r.initiative) || '').toLowerCase().includes(qtext)
121
+ || (r.summary_lines || []).some((x) => String(x).toLowerCase().includes(qtext)));
122
+ const search = el('input', { type: 'text', class: 'cal3-nsearch', placeholder: 'Search notes', 'aria-label': 'Search notes', value: s.q, 'data-cal3-notes': 'search' });
123
+ search.addEventListener('input', () => { s.q = search.value; draw(s); });
124
+ let body;
125
+ if (!s.rows) body = fold('clock', 'Reading the notes', { role: 'status' });
126
+ else if (!s.rows.length) body = fold('mic', s.err || 'No notes yet. Press Record at the top right to start one.', { 'data-cal3-notes': 'none' });
127
+ else if (!shown.length) body = fold('search', 'No note matches “' + s.q.trim() + '”.', { 'data-cal3-notes': 'no-match' });
128
+ else body = groupNotes(shown).map((g) => el('div', { class: 'cal3-ctx', 'data-cal3-notes-group': g.label }, el('h4', null, g.label),
129
+ el('ul', { class: 'list', role: 'list' }, g.rows.map((r) => noteRow(s, r)))));
130
+ return el('section', { class: 'card sect', 'data-cal3-notes': 'list', 'aria-label': 'Notes' },
131
+ el('div', { class: 'sh2' }, el('h2', null, c3ic('notes', 'sm'), 'By initiative ', el('span', { class: 'bdg g', 'data-cal3-notes': 'count' }, String(shown.length)))),
132
+ el('div', { class: 'cal3-nsearchw' }, search), body);
133
+ }
134
+
135
+ function noteRow(s, r) {
136
+ const mode = held(r.id) || (r.state === 'recording' || r.state === 'paused' ? r.state : null);
137
+ const on = s.open === r.id;
138
+ const len = mins(r.meeting_ms || 0);
139
+ return el('li', { class: 'nrow cal3-nrow' + (on ? ' on' : ''), 'data-cal3-note-row': r.id },
140
+ el('span', { class: 'ki' + (mode ? ' hot' : '') }, c3ic(mode ? 'mic' : 'doc')),
141
+ btn('cal3-nopen', { title: 'Open ' + (r.title || 'Untitled note'), 'aria-current': on ? 'true' : 'false' }, [
142
+ el('span', { class: 'tt' }, r.title || 'Untitled note'),
143
+ el('span', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, [mode ? 'now' : whenShort(startOf(r)), len].filter(Boolean).join(' · ')),
144
+ mode ? el('span', { class: 'tag', 'data-cal3-notes': 'live' }, mode === 'paused' ? 'paused' : 'recording') : null,
145
+ !linked(r) && !mode ? el('span', { class: 'tag', title: 'Not linked to a meeting', 'data-cal3-notes': 'no-meeting' }, 'no meeting') : null,
146
+ r.conflicts ? el('span', { class: 'st warn', title: r.conflicts + (r.conflicts === 1 ? ' statement differs' : ' statements differ') + ' from a file' }, c3ic('alert', 'sm'), String(r.conflicts)) : null)],
147
+ () => choose(s, r.id)),
148
+ el('span'));
149
+ }
150
+
151
+ /* ── the note ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
152
+
153
+ function noteCol(s) {
154
+ const n = s.note;
155
+ if (!s.open) return el('div', { 'data-cal3-notes': 'note' }, el('section', { class: 'card' }, fold('doc', s.rows && s.rows.length ? 'Choose a note to read it.' : 'Notes you record show here.')));
156
+ if (!n) return el('div', { 'data-cal3-notes': 'note' }, el('section', { class: 'card' }, fold('clock', 'Reading the note', { role: 'status' })));
157
+ const mode = held(n.id);
158
+ const ph = s.phase || (mode ? 'during' : 'after');
159
+ const ev = n.meeting && n.meeting.kind === 'event' ? n.meeting : null;
160
+ const len = mins(lenOf(n));
161
+ const head = el('section', { class: 'card', 'data-cv': 'stage-head' },
162
+ el('div', { class: 'cal3-nhead' },
163
+ el('span', { class: 'chips2' }, el('b', { 'data-cal3-notes': 'title' }, n.title || 'Untitled note'), el('span', { class: 'mono dim cal3-sm' }, mode ? 'now' : hm(startOf(n)))),
164
+ el('div', { class: 'stg', role: 'group', 'aria-label': 'Note stage', 'data-cv': 'phase' },
165
+ PHASES.map(([k, ic, l]) => btn('sgb', { 'data-cal3-phase': k, 'aria-pressed': String(ph === k), title: l }, [c3ic(ic), el('span', null, l)], () => { s.phase = k; draw(s); })))),
166
+ el('div', { class: 'chips2 dim cal3-sm' },
167
+ [dayOf(startOf(n)), len, iniName(n.initiative) || 'No initiative'].filter(Boolean).join(' · '),
168
+ ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('cal', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev))
169
+ : linked(n) ? null : el('span', { class: 'tag' }, 'no meeting'),
170
+ btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n))));
171
+ const body = ph === 'before' ? before(s, n) : ph === 'during' ? during(s, n, mode) : after(s, n, mode);
172
+ return el('div', { class: 'cal3-ncol', 'data-cal3-notes': 'note', 'data-cal3-note': n.id, 'data-cal3-stage': ph }, head, ...[].concat(body));
173
+ }
174
+
175
+ const vctx = (s) => ({ state: {}, setState: () => { if (s.box.isConnected) { read(s); } } });
176
+ const linkButtons = (s, n) => el('div', { class: 'chips2' },
177
+ btn('chip2 lk', { 'data-cal3-notes': 'link-meeting' }, [c3ic('cal', 'sm'), 'Link to a meeting'], () => V.linkMeeting(vctx(s), n.id)),
178
+ btn('chip2 lk', { 'data-cal3-notes': 'link-initiative' }, [c3ic('init', 'sm'), 'Link to an initiative'], () => V.linkInitiative(vctx(s), n.id)));
179
+
180
+ /* a row of the stage's prepared list: the icon, the label, what it holds */
181
+ const prepRow = (icon, label, detail, done) => el('div', { class: 'chips2 cal3-prep', 'data-cal3-prep': label },
182
+ el('span', { class: 'ki' }, c3ic(icon)),
183
+ el('div', { class: 'cal3-prepb' }, el('b', null, label), el('div', { class: 'dim' }, detail)),
184
+ done ? el('span', { class: 'st ok', title: done }, c3ic('check', 'sm')) : null);
185
+
186
+ function before(s, n) {
187
+ const ev = n.meeting && n.meeting.kind === 'event' ? n.meeting : null;
188
+ const h3 = el('h3', null, c3ic('spark'), 'Before the meeting');
189
+ if (!ev) {
190
+ return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3,
191
+ fold('info', n.meeting && n.meeting.kind === 'adhoc' ? 'A meeting that is not on the calendar, so nothing was prepared for it.' : 'Not linked to a meeting, so nothing was prepared. Link it to a meeting to bring in its people and agenda.', { 'data-cal3-notes': 'no-meeting-before' }),
192
+ linkButtons(s, n));
193
+ }
194
+ const c = ev.context || null;
195
+ const lines = transcriptOf(n).map((x) => ({ t: x.t, text: x.text }));
196
+ const heard = c ? agendaHeard(c, lines) : [];
197
+ const rows = [];
198
+ const isHeard = (i) => heard[i] !== null && heard[i] !== undefined;
199
+ if (c && c.agenda && c.agenda.length) rows.push(prepRow('spark', 'Agenda', el('span', null, c.agenda.map((a, i) => el('span', { class: 'cal3-agi', 'data-cal3-heard': isHeard(i) ? 'true' : 'false', title: isHeard(i) ? 'Heard in the transcript' : null }, (i ? ' · ' : '') + a, isHeard(i) ? c3ic('check', 'sm') : null))), heard.some((x) => x !== null) ? heard.filter((x) => x !== null).length + ' of ' + c.agenda.length + ' heard' : null));
200
+ if (c && c.purpose) rows.push(prepRow('target', 'What it is for', c.purpose));
201
+ if (c && c.prep) rows.push(prepRow('scale', 'To be ready', c.prep));
202
+ const files = [...((c && c.links) || []).map((l) => el('a', { class: 'chip2 lk', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
203
+ ...((c && c.refs) || []).map((x) => (x.link ? el('a', { class: 'chip2 lk', href: x.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('doc', 'sm'), x.label) : el('span', { class: 'chip2' }, c3ic('doc', 'sm'), x.label)))];
204
+ if (files.length) rows.push(prepRow('doc', 'Read first', el('span', { class: 'chips2 cal3-wrapl' }, files)));
205
+ const names = peopleOf(c);
206
+ if (names.length) rows.push(prepRow('user', 'People', el('span', { class: 'chips2 cal3-wrapl' }, names.map(avatar))));
207
+ else if (c && c.attendees) rows.push(prepRow('user', 'People', c.attendees + ' attendees. Names are not kept while calendar names are off.'));
208
+ const when = (ev.start ? dayOf(ev.start) + ' ' + hm(ev.start) : '') + (ev.end ? ' to ' + hm(ev.end) : '');
209
+ rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null].filter(Boolean).join(' · ')));
210
+ return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3, ...rows,
211
+ c ? null : el('p', { class: 'dim cal3-sm' }, 'The calendar gave no more about this meeting.'));
212
+ }
213
+
214
+ function during(s, n, mode) {
215
+ const head = el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('mic'), mode ? 'Live notes' : 'Transcript'), recordButton(s, n, mode));
216
+ if (mode) {
217
+ if (!s.live || s.live.id !== n.id) s.live = { id: n.id, host: el('div', { 'data-cal3-notes': 'live' }) };
218
+ s.live.host.replaceChildren();
219
+ const drawn = V.renderLive(s.live.host, vctx(s), n.id);
220
+ return el('section', { class: 'card', 'data-cv': 'stage-during' }, head,
221
+ drawn ? s.live.host : fold('mic', 'This note is being recorded in another window.'),
222
+ el('div', { class: 'chips2 dim mono cal3-sm' }, c3ic('lock', 'sm'), 'transcribed on this machine' + (mode === 'paused' ? ' · paused' : '')));
223
+ }
224
+ const tr = transcriptOf(n);
225
+ return el('section', { class: 'card', 'data-cv': 'stage-during' }, head,
226
+ tr.length ? el('div', { class: 'term cal3-term', 'data-cal3-notes': 'transcript' }, tr.map((x) => V.fmtTime(x.t || 0) + ' ' + (x.who ? V.speakerOf(n, x.who) + ': ' : '') + x.text).join('\n'))
227
+ : fold('mic', 'Nothing was transcribed for this note.', { 'data-cal3-notes': 'no-transcript' }),
228
+ el('div', { class: 'chips2 dim mono cal3-sm' }, c3ic('lock', 'sm'), 'transcribed on this machine' + (tr.length ? ' · ' + tr.length + (tr.length === 1 ? ' line' : ' lines') : '')));
229
+ }
230
+
231
+ /* Record, Resume or Stop, as the note allows (dest-notes.js noteParts): a note being recorded stops; a paused one resumes;
232
+ * a note never recorded records; a stopped meeting has none. */
233
+ function recordButton(s, n, mode) {
234
+ if (n.private) return null;
235
+ const rb = (label, icon, title, on) => btn('ib pri cal3-recb', { 'data-cal3-notes': 'rec', title, 'aria-label': label }, [c3ic(icon), label], async () => { await on(); setTimeout(() => read(s), 400); });
236
+ if (mode === 'recording') return rb('Stop', 'stop', 'Stop and write the final notes', () => dock.stopNow());
237
+ if (mode === 'paused') return rb('Resume', 'play', 'Resume the paused meeting', () => dock.resumeNow());
238
+ if (n.state === 'idle' || (n.state === undefined && !(n.transcript || []).length)) {
239
+ const busy = dock.recordState().mode === 'recording';
240
+ const b = rb('Record', 'mic', busy ? 'Pause or stop the meeting being recorded first' : 'Record this note. Notes stay on this machine.', () => (busy ? null : dock.recordNote(n.id)));
241
+ if (busy) b.disabled = true;
242
+ return b;
243
+ }
244
+ return null;
245
+ }
246
+
247
+ function after(s, n, mode) {
248
+ const p = partsOf(n);
249
+ const names = n.meeting && n.meeting.context ? peopleOf(n.meeting.context) : [];
250
+ const cf = conflictsOf(n);
251
+ const td = (text) => el('td', null, text);
252
+ const kids = [
253
+ el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('notes'), 'Notes'), recordButton(s, n, mode)),
254
+ el('div', { class: 'chips2' }, el('b', null, n.title || 'Untitled note'), el('span', { class: 'dim mono cal3-sm' }, [dayOf(startOf(n)), mins(lenOf(n))].filter(Boolean).join(' · ')), ...names.map(avatar)),
255
+ ...cf.map((c) => el('div', { class: 'fold w', 'data-cal3-notes': 'conflict' }, c3ic('alert', 'sm'), '“' + c.text + '” differs from ', el('span', { class: 'mono' }, c.doc + (c.line ? ':' + c.line : '')))),
256
+ p.overview ? editable(p.from === 'final', 'p', p.overview, 'Overview', (t) => saveFinal(s, n, 'overview', 0, t), { class: 'cal3-ov', 'data-cal3-notes': 'overview' })
257
+ : el('p', { class: 'dim cal3-ov', 'data-cal3-notes': 'no-overview' }, mode ? 'The overview is written when you stop recording.' : 'No overview yet.'),
258
+ ];
259
+ if (p.decisions.length) kids.push(el('table', { class: 'tbl', 'data-cv': 'decisions' }, el('thead', null, el('tr', null, el('th', null, 'Decision'), el('th', null, 'Why'))),
260
+ el('tbody', null, p.decisions.map((d) => el('tr', { 'data-cal3-notes': 'decision' }, el('td', null, editable(p.from === 'final', 'span', d.text, 'Decision', (t) => saveFinal(s, n, d.sec, d.i, t))), el('td', { class: 'dim' }, d.why || d.by || ''))))));
261
+ if (p.actions.length) kids.push(el('table', { class: 'tbl', 'data-cv': 'actions' }, el('thead', null, el('tr', null, el('th', null, 'Next step'), el('th', null, 'Owner'), el('th', null, 'Due'), el('th'))),
262
+ el('tbody', null, p.actions.map((a) => el('tr', { 'data-cal3-notes': 'action' }, el('td', null, editable(p.from === 'final', 'span', a.text, 'Next step', (t) => saveFinal(s, n, a.sec, a.i, t))),
263
+ td(a.owner || el('span', { class: 'dim' }, 'No owner')), td(a.due || el('span', { class: 'dim' }, '—')),
264
+ el('td', { class: 'cal3-tdr' }, btn('chip2 lk', { 'data-cal3-notes': 'make-task', title: n.initiative ? 'Add this step to ' + iniName(n.initiative) + ' as a plan item' : 'Link the note to an initiative first' },
265
+ [c3ic('plus', 'sm'), 'Make task'], (e) => makeTask(n, a, { btn: e.currentTarget }))))))));
266
+ if (p.questions.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'questions' }, el('h4', null, 'Open questions'),
267
+ el('ul', { class: 'cal3-cl', role: 'list' }, p.questions.map((x) => el('li', { class: 'cal3-ci' }, el('span', { class: 'tag' }, 'Question'), el('span', { class: 'cal3-ct' }, x.text))))));
268
+ const hl = n.highlights || [];
269
+ if (hl.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'highlights' }, el('h4', null, 'Highlights'),
270
+ el('ul', { class: 'cal3-cl', role: 'list' }, hl.map((x) => el('li', { class: 'cal3-ci' }, el('span', { class: 'dim mono cal3-at' }, typeof x.t === 'number' ? V.fmtTime(x.t) : ''), el('span', { class: 'tag' }, x.label || 'Highlight'), el('span', { class: 'cal3-ct' }, x.text))))));
271
+ if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
272
+ const tr = transcriptOf(n);
273
+ if (!mode && tr.length) kids.push(el('details', { 'data-cal3-notes': 'transcript-fold' }, el('summary', { class: 'dim cal3-sm' }, 'Transcript · ' + tr.length + (tr.length === 1 ? ' line' : ' lines')),
274
+ el('div', { class: 'term cal3-term' }, tr.map((x) => V.fmtTime(x.t || 0) + ' ' + x.text).join('\n'))));
275
+ return el('section', { class: 'card cal3-ncard', 'data-cv': 'notes' }, kids);
276
+ }
277
+
278
+ /** A text the PM corrects in place once the notes are final: a click puts the cursor in it, leaving it saves, Esc puts it back. */
279
+ function editable(on, tag, text, label, save, attrs) {
280
+ if (!on) return el(tag, attrs || null, text);
281
+ const node = el(tag, { ...(attrs || {}), contenteditable: 'true', spellcheck: 'true', role: 'textbox', 'aria-label': 'Edit ' + label, 'data-cal3-notes-edit': '' }, text);
282
+ let last = text;
283
+ node.addEventListener('keydown', (e) => {
284
+ if (e.key === 'Enter' && tag !== 'p') { e.preventDefault(); node.blur(); }
285
+ if (e.key === 'Escape') { e.preventDefault(); node.textContent = last; node.blur(); }
286
+ });
287
+ node.addEventListener('blur', async () => {
288
+ const v = node.textContent.replace(/\s+/g, ' ').trim();
289
+ if (!v || v === last) { node.textContent = last; return; }
290
+ if (await save(v)) last = v; else node.textContent = last;
291
+ });
292
+ return node;
293
+ }
294
+ async function saveFinal(s, n, sec, i, text) {
295
+ const r = await pmPost('/api/notes/final-edit', { id: n.id, sec, i, text });
296
+ if (!r || !r.ok) { toast('Not saved' + ((r && r.error) ? ': ' + r.error : '')); return false; }
297
+ n.final_notes = r.final_notes;
298
+ toast('Saved');
299
+ return true;
300
+ }
301
+ async function copy(n) {
302
+ try { await navigator.clipboard.writeText(V.noteMarkdown(n)); toast('The note is on the clipboard as Markdown, with its meeting context.'); }
303
+ catch { toast('The clipboard was not available.'); }
304
+ }
305
+
306
+ /* "Show in the calendar": the Calendar switch back to the grid, on the meeting's day, with the meeting selected */
307
+ function showMeeting(s, ev) {
308
+ const ui = s.ui; if (!ui) return;
309
+ ui.st.mode = 'cal';
310
+ const date = String(ev.start || '').slice(0, 10);
311
+ ui.r.sel = ev.event_id || null;
312
+ if (/^\d{4}-\d{2}-\d{2}$/.test(date)) ui.h.go(ui.host, ui.r, { view: 'day', date, anim: 'zi' }); else ui.redraw();
313
+ }
314
+
315
+ /** Read the shared notes list now (a meeting's "Open the note" needs it on the first draw). */
316
+ export const primeNotes = () => refreshNotes();
@@ -20,6 +20,7 @@ import { mountQuarter, mountYear, dispose as disposeStrategy } from './c3-cal-st
20
20
  import { simDock, simStart, simState, isDemo } from './shell-recording.js';
21
21
  import * as dock from './note-dock.js';
22
22
  import { drawCalendarPage } from './c3-calendar.js'; // the Calendar tab as a page about time (ctx.page)
23
+ import { openNote as showNote } from './c3-notes.js'; // the Record chip opens Calendar › Notes (c3-open-note)
23
24
 
24
25
  /* ── shared state of the /c3.html page (console3 wave C3) ──────────────────────────────────────────────────────────
25
26
  * `live` holds every mounted instance, so a press in the Briefing's ribbon can select a meeting on the Calendar tab's instance.
@@ -975,12 +976,13 @@ function wire(host, r) {
975
976
  const up = (e) => {
976
977
  delete pts[e.pointerId]; if (Object.keys(pts).length) return; pinch = null;
977
978
  if (!s) return; const dx = e.clientX - s.x; const dy = e.clientY - s.y; s = null;
979
+ if (r.page && r.page.mode === 'notes') return;
978
980
  if (Math.abs(dx) > 80 && Math.abs(dy) < 45) { hush(); go(host, r, { date: shift(r.view, r.date, dx < 0 ? 1 : -1), anim: dx < 0 ? 'sn' : 'sp' }); }
979
981
  };
980
982
  root.addEventListener('pointerup', up); root.addEventListener('pointercancel', (e) => { delete pts[e.pointerId]; s = null; pinch = null; });
981
983
  root.addEventListener('click', (e) => { if (sup) { sup = false; e.stopPropagation(); e.preventDefault(); } }, true);
982
984
  root.addEventListener('wheel', (e) => {
983
- if (!(e.ctrlKey || e.metaKey)) return; e.preventDefault(); const n = Date.now(); if (n - wt < 350) return; wt = n; zoomBy(e.deltaY > 0 ? 1 : -1);
985
+ if (!(e.ctrlKey || e.metaKey) || (r.page && r.page.mode === 'notes')) return; e.preventDefault(); const n = Date.now(); if (n - wt < 350) return; wt = n; zoomBy(e.deltaY > 0 ? 1 : -1);
984
986
  }, { passive: false });
985
987
  const onKey = (e) => {
986
988
  if (e.metaKey || e.ctrlKey || e.altKey || e.defaultPrevented) return;
@@ -988,6 +990,7 @@ function wire(host, r) {
988
990
  const t = e.target; if (t && t.closest && t.closest('input,select,textarea,[contenteditable="true"]')) return;
989
991
  if (document.querySelector('[data-shell="sheet"]')) return; // a sheet is open
990
992
  if (!root.closest('[data-shell="calpop"]') && document.querySelector('[data-shell="calpop"]')) return; // the calendar popup is open: it takes the keys, not the tab behind it
993
+ if (r.page && r.page.mode === 'notes') return; // Calendar › Notes has no zoom or period to step
991
994
  if ((e.key === '+' || e.key === '=') && zoomBy(-1)) e.preventDefault();
992
995
  else if ((e.key === '-' || e.key === '_') && zoomBy(1)) e.preventDefault();
993
996
  else if (rib) return;
@@ -998,6 +1001,16 @@ function wire(host, r) {
998
1001
  r.off = () => { document.removeEventListener('keydown', onKey); clearTimeout(supT); };
999
1002
  }
1000
1003
 
1004
+ /* the Record chip (shell-recording.js) asks for the note being recorded: Calendar › Notes on the During stage of the Calendar tab's
1005
+ * instance; if that tab has not mounted yet, its instance opens the note when it does */
1006
+ let PENDING_NOTE = null;
1007
+ if (typeof window !== 'undefined') window.addEventListener('c3-open-note', (ev) => {
1008
+ const id = (ev.detail && ev.detail.id) || null;
1009
+ const cal = [...live].find((x) => isCalTab(x) && x.ctx.page);
1010
+ if (cal) { showNote(cal, id, 'during'); draw(cal.host, cal); } else PENDING_NOTE = { id };
1011
+ gotoTab('cal');
1012
+ });
1013
+
1001
1014
  /* the simulated recorder (shell-recording.js, demo only): a start, a stop or a pause redraws the stage under the day's cards, and the group's title opens the During stage */
1002
1015
  let simOnWin = false;
1003
1016
  function installSim() {
@@ -1014,6 +1027,7 @@ export function mountCalendarViews(host, ctx) {
1014
1027
  if (!r) {
1015
1028
  r = { ctx: ctx || {}, root: el('section', { class: 'c3 calwrap', 'data-board': 'calendar-views', 'aria-label': 'Calendar' }), view: (ctx && ctx.view) || 'week', zoom: (ctx && ctx.view === 'day') ? 'today' : (ctx && ctx.view) || 'week', date: todayStr(), data: null, error: null, host, done: {}, ready: {}, sel: null, phase: null };
1016
1029
  if (PENDING && ctx && ctx.plan) { r.sel = PENDING.sel; if (PENDING.date) r.date = PENDING.date; PENDING = null; }
1030
+ if (PENDING_NOTE && isCalTab(r)) { showNote(r, PENDING_NOTE.id, 'during'); PENDING_NOTE = null; }
1017
1031
  live.add(r);
1018
1032
  r.stopPhone = c3Phone(r.root);
1019
1033
  roots.set(host, r);
@@ -507,6 +507,46 @@
507
507
  .c3 .cal3-sw-mine{background:transparent;border:1px dashed var(--foreground)}
508
508
  .c3 .cal3-wrap{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--sp-3);align-items:start}
509
509
  @container (min-width:1100px){.c3 .cal3-wrap{grid-template-columns:minmax(0,1fr) 340px}}
510
+ /* Calendar › Notes (c3-notes.js): the list beside the open note; one column on a narrow page */
511
+ .c3 .cal3-nwrap{display:grid;grid-template-columns:minmax(260px,340px) minmax(0,1fr);gap:var(--sp-3);align-items:start}
512
+ .c3 .cal3-nwrap [data-cal3-notes="note"] .card{display:grid;gap:var(--sp-2)}
513
+ .c3 .cal3-nwrap details>summary{cursor:pointer;padding:var(--sp-1) 0}
514
+ .c3 .cal3-ncol{display:grid;gap:var(--sp-3);align-content:start;min-width:0}
515
+ .c3 .cal3-nhead{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
516
+ .c3 .cal3-nsearchw{padding:0 0 var(--sp-2)}
517
+ .c3 .cal3-nsearch{width:100%}
518
+ .c3 .cal3-nrow{cursor:pointer}
519
+ .c3 .cal3-nrow.on{background:var(--muted)}
520
+ .c3 .cal3-sm{font-size:var(--fs-2)}
521
+ .c3 .cal3-xs{font-size:var(--fs-1)}
522
+ .c3 .cal3-prep{align-items:flex-start;padding:6px 0;border-bottom:1px solid var(--border)}
523
+ .c3 .cal3-prep .ki{flex:none}
524
+ .c3 .cal3-prepb{flex:1;min-width:0;font-size:var(--fs-3)}
525
+ .c3 .cal3-agi{display:inline-flex;align-items:center;gap:2px}
526
+ .c3 .cal3-agi[data-cal3-heard="true"] svg{color:var(--acc)}
527
+ .c3 .cal3-wrapl{display:inline-flex;flex-wrap:wrap}
528
+ .c3 .cal3-term{max-height:60vh;overflow:auto}
529
+ .c3 .cal3-ov{margin:0}
530
+ .c3 .cal3-tdr{text-align:right}
531
+ .c3 .cal3-ncard{display:grid;gap:var(--sp-2)}
532
+ /* a primitives.js button drawn as a console3 control: the primitive's own height, padding, nowrap and hover come off */
533
+ .c3 .cal3-b{height:auto;min-height:0;line-height:inherit;font-weight:inherit;justify-content:flex-start;text-align:left}
534
+ .c3 .cal3-b.chip2{padding:0;border:0;background:transparent;white-space:nowrap}
535
+ .c3 .cal3-b.sgb{height:auto;padding:4px 8px}
536
+ .c3 .cal3-b.ib{padding:0;justify-content:center}
537
+ .c3 .cal3-b.cal3-recb{width:auto;height:34px;padding:0 14px;gap:6px;display:inline-flex;align-items:center}
538
+ .c3 .cal3-b.cal3-nopen{display:grid;gap:2px;min-width:0;padding:0;border:0;background:transparent;white-space:normal;font:inherit;color:inherit}
539
+ .c3 .seg .cal3-b{height:auto;padding:4px 10px;border:0;white-space:nowrap}
540
+ @media (hover:hover){
541
+ .c3 .cal3-b.chip2:hover:not(:disabled),.c3 .cal3-b.cal3-nopen:hover:not(:disabled),.c3 .seg .cal3-b:hover:not(:disabled){background:transparent}
542
+ .c3 .cal3-b.chip2.lk:hover:not(:disabled){color:var(--acc)}
543
+ .c3 .cal3-b.sgb:hover:not(:disabled){background:var(--card)}
544
+ .c3 .cal3-b.sgb[aria-pressed="true"]:hover:not(:disabled){background:var(--accw)}
545
+ .c3 .cal3-b.ib:hover:not(:disabled){background:var(--background);color:var(--foreground)}
546
+ .c3 .cal3-b.ib.pri:hover:not(:disabled){background:var(--foreground);color:var(--background)}
547
+ .c3 .seg .cal3-b[aria-pressed="true"]:hover:not(:disabled){background:var(--background);color:var(--foreground)}
548
+ }
549
+ @media (max-width:760px){.c3 .cal3-nwrap{grid-template-columns:minmax(0,1fr)}}
510
550
  .c3 .cal3-main,.c3 .cal3-side{display:grid;gap:var(--sp-3);min-width:0;align-content:start}
511
551
  .c3 .cal3-scroll{overflow-x:auto;min-width:0}
512
552
  /* a marker: glyph and short title; the left edge carries the tone */
@@ -143,8 +143,12 @@ const NS_SVG = 'http://www.w3.org/2000/svg';
143
143
  const glyph = (d) => { const s = document.createElementNS(NS_SVG, 'svg'); s.setAttribute('class', 'ic sm'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); const p = document.createElementNS(NS_SVG, 'path'); p.setAttribute('d', d); s.appendChild(p); return s; };
144
144
  const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14M16 5v14'), play: () => c3ic('play', 'sm'), square: () => c3ic('stop', 'sm'), mic: () => c3ic('mic', 'sm'), down: () => glyph('M6 9l6 6 6-6') };
145
145
 
146
- /** The chip opens the note. console3 has no Notes place, so it opens the main app, where the full note is. */
147
- const openNote = () => { try { window.open('/', '_blank', 'noopener'); } catch { /* a blocked pop-up */ } };
146
+ /** The chip opens the note: on console3, Calendar › Notes on the During stage (calendar-views.js listens for c3-open-note);
147
+ * elsewhere the main app, where the full note is. */
148
+ const openNote = (id) => {
149
+ if (document.querySelector('[data-c3-tab="cal"]')) { window.dispatchEvent(new CustomEvent('c3-open-note', { detail: { id: id || dock.activeId() || null } })); return; }
150
+ try { window.open('/', '_blank', 'noopener'); } catch { /* a blocked pop-up */ }
151
+ };
148
152
 
149
153
  /* The engine and language the live transcript uses, on the chip (notes-recording-reliability): a meeting transcribed in the wrong
150
154
  * language because of a setting is visible while it records. Read once per recording from GET /api/notes/engine. */
@@ -206,7 +210,7 @@ function mountReal(host) {
206
210
  const v = recView(st, reducedMotion());
207
211
  rec.clock = el('span', { class: 'recclk', 'data-fr-rec-clock': '' }, chipText(st));
208
212
  const chip = button({ variant: 'ghost', onClick: () => openNote(st.id),
209
- attrs: { class: 'c3rec__chip', title: (recording ? 'Recording: ' : 'Paused: ') + v.title + '. Open the note in the main app', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + v.title + '. Open the note.', 'data-fr-rec': st.mode, 'data-shell-rec-open': '' },
213
+ attrs: { class: 'c3rec__chip', title: (recording ? 'Recording: ' : 'Paused: ') + v.title + '. Open the note', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + v.title + '. Open the note.', 'data-fr-rec': st.mode, 'data-shell-rec-open': '' },
210
214
  label: [el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
211
215
  ...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : [])] });
212
216
  const group = [chip];
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Initiative c3-notes-place: Calendar › Notes on the console3 Calendar tab (the notes list, the note's Before, During and After, the Record chip and a meeting's detail opening the note).",
3
+ "panels": ["pm-c3-notes-test.js"],
4
+ "seconds": { "pm-c3-notes-test.js": 45 }
5
+ }