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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/console-calendar.md +7 -3
- package/workspace/lib/calendar-layers.js +11 -2
- package/workspace/lib/routes-calendar-views.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +128 -15
- package/workspace/public/pm/console3.css +27 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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-
|
|
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
|
|
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: `${
|
|
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
|
-
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.224.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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) => {
|
|
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
|
|
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')), ...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|