create-pmos 0.112.0 → 0.113.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/today.md +3 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +18 -5
- package/workspace/public/pm/notes-links.js +9 -1
- package/workspace/public/pm/today-agenda.js +24 -3
- package/workspace/public/pm/today-copy.js +13 -0
- package/workspace/public/pm/today-day.js +94 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.113.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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T05:19:33Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T05:19:33Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T05:19:33Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/today.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Today: what needs you"
|
|
4
4
|
description: "The Agenda month and the calendar it remembers, the meeting notes of a meeting, the two-column day (your jobs and the agent's jobs), the eight kinds of card on Today, the one button on each card, the count, the empty state, and the pmos today command."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-08
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Today: what needs you
|
|
@@ -52,6 +52,8 @@ Each day of the month answers three questions:
|
|
|
52
52
|
|
|
53
53
|
The switch above the month shows one answer at a time: **Overview** (all three), **Full** (the time in meetings), **Key** (the meeting that matters), **Prep** (how ready you are) or **Free** (the free blocks of 90 minutes or more).
|
|
54
54
|
|
|
55
|
+
A meeting's detail shows one focus. Under the title is one line: the time, how many people join, and a star when the meeting is key. Under it are the goal of the meeting and, for a meeting with prep, one line of progress (the steps done, a bar, the free minutes before the meeting). Only the first open step is expanded, with what its source says, its link and, for a note step, a box for your note. The one primary button follows it: **I have read it**, **Mark done** or **Done**, then **Mark ready** when every step is done, then **Take notes** once the meeting has started. Press the arrow at the end of another step to open it. **More** holds the rare presses: **Mark ready** while steps are left, **Remind me**, **Skip prep**, the key mark and **Add as a goal for today**. **Details** holds the people, the kind of meeting and how often it met, the initiative, the goal it counts toward, During and After, the mail and the links.
|
|
56
|
+
|
|
55
57
|
A meeting's detail ends with **Meeting notes**. Press **Take notes** to start a note linked to the meeting, and to its initiative when the brief names one. PMOS starts to listen at once and keeps the note small at the bottom right. Press it to open the note as a pop-up. When the meeting has a note, the detail shows its summary and **Open notes**, which opens it in the pop-up, and the meeting's row shows **notes**. See [Notes](notes.md).
|
|
56
58
|
|
|
57
59
|
The memory of a meeting says what kind of meeting it is (one-to-one, onboarding or group), how many times it was held, how many are to come, and why it matters.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.113.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.113.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -3608,18 +3608,31 @@
|
|
|
3608
3608
|
.td-day__mark { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 6px 10px; border: 1px solid var(--border); background: var(--muted); font-size: 14px; }
|
|
3609
3609
|
.td-day__mark[data-day-mark-state="done"] { border-color: var(--foreground); }
|
|
3610
3610
|
.td-day__ready { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 12px; }
|
|
3611
|
-
.td-day__prep { display: grid; gap:
|
|
3611
|
+
.td-day__prep { display: grid; gap: 10px; }
|
|
3612
3612
|
.td-day__ended { margin: 0; padding: 8px 12px; border-left: 2px solid var(--foreground); background: var(--muted); font-size: 14px; }
|
|
3613
3613
|
/* an ended meeting keeps its prep, quieter: the steps stay readable, the past is not a call to act */
|
|
3614
|
-
.td-day__detail:has(.td-day__ended) .td-day__prep { background: var(--muted); }
|
|
3614
|
+
.td-day__detail:has(.td-day__ended) .td-day__prep { padding: 10px 12px; background: var(--muted); }
|
|
3615
3615
|
.td-day__fold { border-top: 1px solid var(--border); padding-top: 6px; }
|
|
3616
3616
|
.td-day__fold > summary { cursor: pointer; font-size: 12px; color: var(--muted-foreground); }
|
|
3617
3617
|
.td-day__fold[open] > summary { margin-bottom: 6px; }
|
|
3618
|
-
.td-day__fold > summary:focus-visible, .td-day__fold[open] > summary:focus-visible, .td-day__ref > summary:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
3619
|
-
@media (pointer: coarse) { .td-day__fold > summary, .td-day__fold[open] > summary, .td-day__ref > summary { min-height: 44px; } }
|
|
3618
|
+
.td-day__fold > summary:focus-visible, .td-day__fold[open] > summary:focus-visible, .td-day__ref > summary:focus-visible, .td-day__details > summary:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
3619
|
+
@media (pointer: coarse) { .td-day__fold > summary, .td-day__fold[open] > summary, .td-day__ref > summary, .td-day__details > summary { min-height: 44px; } }
|
|
3620
3620
|
.td-day__fold-head { justify-self: start; }
|
|
3621
3621
|
.td-day__steprow { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
3622
|
-
.td-day__step2:has(.td-day__ref), .td-day__step2:has(.td-day__notebox) { flex-direction: column; align-items: stretch; }
|
|
3622
|
+
.td-day__step2:has(.td-day__ref), .td-day__step2:has(.td-day__notebox), .td-day__step2:has(.td-day__acts) { flex-direction: column; align-items: stretch; }
|
|
3623
|
+
/* the meeting detail with one focus (today-meeting-focus): one open step, the rest one line */
|
|
3624
|
+
.td-day__meta { text-transform: none; letter-spacing: 0; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
3625
|
+
.td-day__purpose { max-width: 64ch; text-wrap: pretty; }
|
|
3626
|
+
.td-day__step2[data-cur="false"] { border-color: transparent; background: transparent; padding-block: 2px; }
|
|
3627
|
+
.td-day__step2[data-cur="true"][data-done="false"] { border-color: var(--foreground); }
|
|
3628
|
+
.td-day__stepopen { margin-left: auto; }
|
|
3629
|
+
.td-day__free[data-tight="true"] { color: var(--warning); }
|
|
3630
|
+
.td-day__free { font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
|
|
3631
|
+
.td-day__foot { position: relative; }
|
|
3632
|
+
.td-day__foot > [data-day="more-menu"] { position: absolute; right: 0; top: 2px; }
|
|
3633
|
+
.td-day__details > summary { padding-right: 96px; line-height: 28px; }
|
|
3634
|
+
.td-day__notes--quiet, .td-day__notes--live { border: 0; padding: 0; background: none; }
|
|
3635
|
+
.td-day__prepfold { border-top: 0; padding-top: 0; }
|
|
3623
3636
|
.td-day__ref { display: grid; gap: 4px; padding: 6px 8px; background: var(--muted); font-size: 14px; }
|
|
3624
3637
|
.td-day__ref > summary { display: flex; justify-content: space-between; gap: 8px; cursor: pointer; font-weight: 600; color: var(--foreground); }
|
|
3625
3638
|
.td-day__reflines { margin: 0; padding-left: 18px; display: grid; gap: 2px; color: var(--foreground); line-height: 1.45; }
|
|
@@ -87,7 +87,15 @@ export function notesBlock(e, opts = {}) {
|
|
|
87
87
|
el('div', { class: 'td-day__acts' }, button({ label: 'Open notes', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => openDock(n.id) })));
|
|
88
88
|
}
|
|
89
89
|
if (!notesKnown()) return null;
|
|
90
|
-
|
|
90
|
+
// opts.mode (the meeting detail, today-meeting-focus F7): 'live' leads with Take notes as the one primary button and one line;
|
|
91
|
+
// 'quiet' is the button alone, with no heading or help text. Without a mode the block reads as it always did.
|
|
92
|
+
const mode = ended ? null : opts.mode || null;
|
|
93
|
+
const take = button({ label: 'Take notes', variant: mode === 'quiet' ? 'ghost' : ended ? undefined : 'primary', size: 'sm', attrs: { 'data-notes-take': e.id || '' }, onClick: () => takeNotes(e, opts, take) });
|
|
94
|
+
if (mode) {
|
|
95
|
+
return el('div', { class: 'td-day__notes td-day__notes--' + mode, 'data-notes-block': 'none' },
|
|
96
|
+
mode === 'live' && opts.liveText ? el('p', { class: 'td-day__note' }, opts.liveText) : null,
|
|
97
|
+
el('div', { class: 'td-day__acts' }, take));
|
|
98
|
+
}
|
|
91
99
|
return el('div', { class: 'td-day__notes', 'data-notes-block': 'none' },
|
|
92
100
|
el('h4', null, 'Meeting notes'),
|
|
93
101
|
el('p', { class: 'td-day__note' }, ended ? 'No notes were taken in PMOS for this meeting.' : 'Take notes when the meeting starts. PMOS listens at once and keeps the note at the bottom right. It is linked to this event' + (opts.initiative ? ' and its initiative.' : '.')),
|
|
@@ -298,22 +298,43 @@ function keyBlock(e) {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/** The series of a meeting: how often it met, what is to come, and the last time, folded. Null when unknown. */
|
|
301
|
-
export function memoryBlock(id) {
|
|
301
|
+
export function memoryBlock(id, { compact = false } = {}) {
|
|
302
302
|
const f = findEvent(id);
|
|
303
303
|
if (!f) return null;
|
|
304
304
|
const s = f.data.series[f.e.key];
|
|
305
305
|
if (!s) return null;
|
|
306
306
|
const last = s.last && s.last.date !== f.day.date ? s.last : null;
|
|
307
|
+
// compact (the meeting detail's Details, today-meeting-focus F5): no heading and no key presses; the presses are in More
|
|
308
|
+
const said = compact && f.e.mark ? A.key[(f.e.imp ? 'marked' : 'not') + f.e.mark] : null;
|
|
307
309
|
return el('div', { class: 'td-day__notes', 'data-ag': 'memory' },
|
|
308
|
-
el('h4', null, A.means),
|
|
310
|
+
compact ? null : el('h4', null, A.means),
|
|
309
311
|
el('p', null, A.kindWord[f.e.kind] + '. ' + (s.count > 1 ? A.series(s.past, s.coming) : A.once)),
|
|
310
312
|
...(f.e.mark ? [] : whyLines(f.e)).map((t) => el('p', null, t)),
|
|
311
|
-
keyBlock(f.e),
|
|
313
|
+
compact ? (said ? el('p', { class: 'td-day__note', 'data-ag': 'key', 'data-ag-key': f.e.imp ? 'true' : 'false' }, said) : null) : keyBlock(f.e),
|
|
312
314
|
last ? el('details', { class: 'td-ag__last', 'data-ag': 'last' }, el('summary', null, A.lastTime(dayLabel(last.date))),
|
|
313
315
|
last.notes ? el('p', null, last.notes) : el('p', { class: 'td-day__note' }, A.noLast),
|
|
314
316
|
last.open && last.open.length ? el('ul', { class: 'td-day__chk' }, ...last.open.map((x) => el('li', null, x))) : null) : null);
|
|
315
317
|
}
|
|
316
318
|
|
|
319
|
+
/** What the meeting detail's one line needs: how many people join, and whether the meeting is key. Null when unknown. */
|
|
320
|
+
export function meetingFacts(id) {
|
|
321
|
+
const f = findEvent(id);
|
|
322
|
+
return f ? { people: f.e.attendees || 0, key: !!f.e.imp } : null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The key-mark presses of a meeting as menu items for More (the same presses as keyBlock, same marks). */
|
|
326
|
+
export function keyItems(id) {
|
|
327
|
+
const f = findEvent(id);
|
|
328
|
+
if (!f) return [];
|
|
329
|
+
const e = f.e;
|
|
330
|
+
const K = A.key;
|
|
331
|
+
const it = (label, scope, on, mark) => ({ label, attrs: { 'data-ag-mark': mark }, onSelect: () => setMark(e, scope, on) });
|
|
332
|
+
const items = e.mark ? [it(K.clear, e.mark, null, 'clear')]
|
|
333
|
+
: e.imp ? [it(K.unmark, 'one', false, 'not-one'), e.repeats ? it(K.unmarkAll, 'series', false, 'not-series') : null]
|
|
334
|
+
: [it(K.mark, 'one', true, 'one'), e.repeats ? it(K.markAll, 'series', true, 'series') : null];
|
|
335
|
+
return items.filter(Boolean);
|
|
336
|
+
}
|
|
337
|
+
|
|
317
338
|
export function meetingDetail(ctx, id, h) {
|
|
318
339
|
const f = findEvent(id);
|
|
319
340
|
if (!f) return null;
|
|
@@ -385,6 +385,19 @@ export const COPY = {
|
|
|
385
385
|
hold: 'Waiting on…',
|
|
386
386
|
notMine: 'Not mine',
|
|
387
387
|
skip: 'Skip prep',
|
|
388
|
+
// the meeting detail with one focus (initiative today-meeting-focus, PRD F1 to F7)
|
|
389
|
+
focus: {
|
|
390
|
+
more: 'More', details: 'Details', openStep: 'Open this step',
|
|
391
|
+
press: { read: 'I have read it', do: 'Mark done', note: 'Done' },
|
|
392
|
+
free: (min) => `${min} min free`,
|
|
393
|
+
before: (d, n, at) => `${d} of ${n} before ${at}`,
|
|
394
|
+
remindAt: (when, at) => `Remind me ${when} · ${at}`,
|
|
395
|
+
skipWhy: (why) => `Skip prep · ${why}`,
|
|
396
|
+
people: (n) => (n === 1 ? '1 person' : `${n} people`),
|
|
397
|
+
key: '★ Key',
|
|
398
|
+
live: 'The meeting has started. PMOS listens when you start the note, and links it to this event.',
|
|
399
|
+
prepFold: (d, n) => `Prep · ${d} of ${n} done`,
|
|
400
|
+
},
|
|
388
401
|
snoozeTo: { hour: 'In 1 hour', later: 'Later today, 16:00', due: 'Before it is due', end: 'End of the day, 17:30', m30: '30 min before it', m15: '15 min before it' },
|
|
389
402
|
holdWho: 'Who or what you wait on',
|
|
390
403
|
holdHint: 'For example: Raj, or the cost model',
|
|
@@ -31,7 +31,7 @@ import { FIXTURE } from './state.js';
|
|
|
31
31
|
import { COPY } from './today-copy.js';
|
|
32
32
|
import { goalRows, goalAdd, goalDetail, addGoal, goalsSig } from './today-goals.js';
|
|
33
33
|
import { peopleLinks } from './people-link.js';
|
|
34
|
-
import { agendaPanel, meetingDetail, memoryBlock, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
|
|
34
|
+
import { agendaPanel, meetingDetail, memoryBlock, meetingFacts, keyItems, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
|
|
35
35
|
import { notesBlock, onNotes } from './notes-links.js';
|
|
36
36
|
|
|
37
37
|
const D = COPY.day;
|
|
@@ -62,7 +62,7 @@ const ui = {
|
|
|
62
62
|
sel: null, mtab: 'all', openSrc: null, showIgn: {}, showIgnDigests: false,
|
|
63
63
|
revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
|
|
64
64
|
// today-act-prepare: the folded groups the PM opened, the item whose hold form is open, and its draft
|
|
65
|
-
groupsOpen: new Set(), holding: null, holdDraft: { who: '', when: 'h2' }, compose: new Map(), verdict: new Map(),
|
|
65
|
+
cur: new Map(), groupsOpen: new Set(), holding: null, holdDraft: { who: '', when: 'h2' }, compose: new Map(), verdict: new Map(),
|
|
66
66
|
};
|
|
67
67
|
let rerender = () => {};
|
|
68
68
|
let notesSub = null; // the redraw when the meeting notes list is read (pm/notes-links.js)
|
|
@@ -670,16 +670,35 @@ function markBanner(ctx, id, mark, prep) {
|
|
|
670
670
|
}
|
|
671
671
|
/** The steps the PM checks off. A step the brief already marked done stays done. A step that reads a source shows what
|
|
672
672
|
* the source says under it (PRD X2), and a note step holds the PM's own note (PRD F3), so no other file needs opening. */
|
|
673
|
-
function stepChecks(ctx, id, steps, { refs = [], mark = null } = {}) {
|
|
673
|
+
function stepChecks(ctx, id, steps, { refs = [], mark = null, focus = false } = {}) {
|
|
674
674
|
if (!steps.length) return null;
|
|
675
|
+
const F = A.focus;
|
|
676
|
+
// focus (the meeting detail, today-meeting-focus F2): only one step is open, the PM's own pick or else the first step left
|
|
677
|
+
const want = focus ? ui.cur.get(id) : null;
|
|
678
|
+
const cur = focus ? steps.find((x) => x.t === want) || steps.find((x) => !x.done) || null : null;
|
|
675
679
|
return el('ul', { class: 'td-day__steps2', 'data-day': 'steps' }, ...steps.map((st) => {
|
|
680
|
+
const isCur = !focus || (!!cur && st.t === cur.t);
|
|
676
681
|
const f = field({ kind: 'check', label: st.t, value: !!st.done, attrs: { 'data-day-step': st.t, disabled: st.fixed ? true : null },
|
|
677
682
|
onCommit: (v) => markPress(ctx.ws, id, { action: 'step', step: st.t, done: !!v }) });
|
|
678
683
|
const r = Number.isInteger(st.ref) ? refs[st.ref] : null;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
st.note ?
|
|
684
|
+
const noteEl = st.note && isCur ? noteBox(ctx, id, mark) : null;
|
|
685
|
+
let press = null;
|
|
686
|
+
if (focus && isCur && !st.done) {
|
|
687
|
+
press = button({ label: F.press[st.note ? 'note' : r ? 'read' : 'do'], variant: 'primary', size: 'sm', attrs: { 'data-day-step-press': st.t, onMousedown: (ev) => ev.preventDefault() }, onClick: async () => {
|
|
688
|
+
// a note typed in the box is saved first, in this press, so the box losing focus does not redraw the card under the pointer
|
|
689
|
+
const ta = noteEl && noteEl.querySelector('textarea');
|
|
690
|
+
if (ta && ta.value !== ((mark && mark.note) || '')) await markPress(ctx.ws, id, { action: 'note', text: ta.value }, A.noteSaved);
|
|
691
|
+
ui.cur.delete(id);
|
|
692
|
+
await markPress(ctx.ws, id, { action: 'step', step: st.t, done: true });
|
|
693
|
+
} });
|
|
694
|
+
}
|
|
695
|
+
const opener = focus && !isCur && (r || st.note)
|
|
696
|
+
? iconButton({ icon: 'chevron-down', ariaLabel: `${F.openStep}: ${st.t}`, size: 'sm', attrs: { 'data-day-step-open': st.t, class: 'td-day__stepopen' }, onClick: () => { ui.cur.set(id, st.t); rerender(); } }) : null;
|
|
697
|
+
return el('li', { class: 'td-day__step2', 'data-done': st.done ? 'true' : 'false', 'data-cur': focus ? String(isCur) : null },
|
|
698
|
+
el('div', { class: 'td-day__steprow' }, f, st.who ? el('span', { class: 'td-day__who' }, D.who[st.who] || st.who) : null, opener),
|
|
699
|
+
r && isCur ? refBlock(r, { open: !st.done, step: true }) : null,
|
|
700
|
+
noteEl,
|
|
701
|
+
press ? el('div', { class: 'td-day__acts' }, press) : null);
|
|
683
702
|
}));
|
|
684
703
|
}
|
|
685
704
|
/** One source in a few lines, in the card: what it says, and a small link to the source for when the PM wants more. */
|
|
@@ -838,27 +857,57 @@ function freeBefore(m, e) {
|
|
|
838
857
|
return Math.max(0, Math.round((e.s - m.now - busy) / MIN_MS));
|
|
839
858
|
}
|
|
840
859
|
|
|
841
|
-
/** A meeting's prep (
|
|
842
|
-
*
|
|
843
|
-
|
|
860
|
+
/** A meeting's prep (today-act-prepare A1, C1, M1, M6, D2; today-meeting-focus F1 to F3): the prep sentence, one line of how
|
|
861
|
+
* ready the PM is, the Before steps with only the first open one expanded, and the one primary press that fits. The
|
|
862
|
+
* other presses are in More. `fold` (the meeting has started) folds all of it under one line. */
|
|
863
|
+
function prepBlock(ctx, e, { fold = false } = {}) {
|
|
844
864
|
const { m } = ctx;
|
|
865
|
+
const F = A.focus;
|
|
845
866
|
const steps = e.before.map((st) => ({ t: String(st.t || ''), who: st.who, done: st.done, ref: st.ref, note: !!st.note, fixed: !!(e.steps && e.steps.before && e.steps.before.find((x) => x.t === st.t && x.done)) }));
|
|
846
867
|
const refs = Array.isArray(e.refs) ? e.refs : [];
|
|
847
868
|
const d = steps.filter((x) => x.done).length; const total = steps.length;
|
|
848
869
|
const left = total - d;
|
|
849
870
|
const free = freeBefore(m, e);
|
|
850
|
-
const
|
|
871
|
+
const tight = !!left && free < left * 15;
|
|
851
872
|
const open = !e.mark || !e.mark.state;
|
|
852
873
|
const link = Array.isArray(e.links) ? e.links.find((l) => isUrl(l.url)) : null;
|
|
853
|
-
|
|
854
|
-
el('div', { class: 'td-day__jobtop' }, verbChip('prepare'), e.ended ? null : dueChip(m, e.mark, e.s, true)),
|
|
874
|
+
const body = el('div', { class: 'td-day__prep', 'data-day': 'prep' },
|
|
855
875
|
e.prep ? el('p', { class: 'td-day__todo', 'data-day': 'todo' }, e.prep) : null,
|
|
856
876
|
markBanner(ctx, e.id, e.mark, true),
|
|
857
|
-
total ? el('div', { class: 'td-day__ready', 'data-day': 'readiness' }, meter({ done: d, total, label:
|
|
858
|
-
|
|
877
|
+
total ? el('div', { class: 'td-day__ready', 'data-day': 'readiness' }, meter({ done: d, total, label: F.before(d, total, e.time) }),
|
|
878
|
+
e.ended ? null : left ? el('span', { class: 'td-day__free', 'data-tight': String(tight), title: tight ? A.fit.short(left, free) : A.fit.time(left, free) }, F.free(free)) : el('span', { class: 'td-day__dim' }, A.fit.ok)) : null,
|
|
879
|
+
stepChecks(ctx, e.id, steps, { refs, mark: e.mark, focus: true }),
|
|
859
880
|
refsBlock(refs, steps),
|
|
860
|
-
open && link && left && !e.ended ? el('div', { class: 'td-day__acts', 'data-day': 'cta' },
|
|
861
|
-
|
|
881
|
+
open && link && left && !e.ended ? el('div', { class: 'td-day__acts', 'data-day': 'cta' }, extLink((link.label || link.url) + ' ↗', link.url, { 'data-day-link': 'prep' })) : null,
|
|
882
|
+
open && !left ? el('div', { class: 'td-day__acts', 'data-day': 'marks' }, button({ label: A.ready, variant: 'primary', size: 'sm', icon: 'check', disabled: ui.busy.has('mark:' + e.id), attrs: { 'data-day-mark': 'done' },
|
|
883
|
+
onClick: () => markPress(ctx.ws, e.id, { action: 'done' }, A.did.ready(e.title)) })) : null);
|
|
884
|
+
return fold ? el('details', { class: 'td-day__fold td-day__prepfold', 'data-day': 'prep-fold' }, el('summary', null, F.prepFold(d, total)), body) : body;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** The presses that are rarely needed, in one menu (today-meeting-focus F4): Mark ready while steps are left, Remind me, Skip
|
|
888
|
+
* prep, the key mark and, from `extra`, Add as a goal. Each press writes the same record, with the same toast and Undo, as before. */
|
|
889
|
+
function moreMenu(ctx, e, extra) {
|
|
890
|
+
const { m, ws } = ctx;
|
|
891
|
+
const F = A.focus;
|
|
892
|
+
const st = e.mark && e.mark.state;
|
|
893
|
+
const items = [];
|
|
894
|
+
if (e.hadPrep) {
|
|
895
|
+
const left = e.before.filter((x) => !x.done).length;
|
|
896
|
+
if (st !== 'done' && left > 0) items.push({ label: A.ready, attrs: { 'data-day-mark': 'done' }, onSelect: () => markPress(ws, e.id, { action: 'done' }, A.did.ready(e.title)) });
|
|
897
|
+
for (const [l, t] of snoozeTimes(m, { start: e.s })) {
|
|
898
|
+
items.push({ label: F.remindAt(l, hhmm(t)), attrs: { 'data-day-mark': 'snooze', 'data-day-snooze': hhmm(t) },
|
|
899
|
+
onSelect: () => markPress(ws, e.id, { action: 'snooze', until: new Date(t).toISOString() }, A.did.snooze(hhmm(t))) });
|
|
900
|
+
}
|
|
901
|
+
if (st !== 'dismissed') for (const w of ['nothing', 'elsewhere']) items.push({ label: F.skipWhy(A.why[w]), attrs: { 'data-day-mark': 'dismiss', 'data-day-why': w },
|
|
902
|
+
onSelect: () => markPress(ws, e.id, { action: 'dismiss', why: w }, A.did.dismiss(A.why[w])) });
|
|
903
|
+
}
|
|
904
|
+
const rest = [...keyItems(e.id), ...extra];
|
|
905
|
+
if (items.length && rest.length) items.push({ sep: true });
|
|
906
|
+
items.push(...rest);
|
|
907
|
+
if (!items.length) return null;
|
|
908
|
+
const b = button({ label: F.more, size: 'sm', variant: 'ghost', iconEnd: 'chevron-down', attrs: { 'data-day': 'more-menu', 'aria-haspopup': 'menu' },
|
|
909
|
+
onClick: () => menu({ anchor: b, ariaLabel: F.more, items }) });
|
|
910
|
+
return b;
|
|
862
911
|
}
|
|
863
912
|
|
|
864
913
|
/* ── the right column ─────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -878,6 +927,7 @@ function stepItem(st) {
|
|
|
878
927
|
|
|
879
928
|
function eventDetail(ctx, e) {
|
|
880
929
|
const { m } = ctx;
|
|
930
|
+
const F = A.focus;
|
|
881
931
|
const n = e.goal ? m.goalNo.get(e.goal) : null;
|
|
882
932
|
const g = n ? m.allGoals[n - 1] : null;
|
|
883
933
|
const ini = e.initiative && e.initiative.id ? e.initiative : null;
|
|
@@ -885,6 +935,8 @@ function eventDetail(ctx, e) {
|
|
|
885
935
|
const notes = e.notes || {};
|
|
886
936
|
const w = notes.wispr || null;
|
|
887
937
|
const mail = notes.mail || null;
|
|
938
|
+
const started = Number.isFinite(e.s) && e.s <= m.now && !e.ended;
|
|
939
|
+
const facts = meetingFacts(e.id);
|
|
888
940
|
const mailBlock = mail ? el('div', { class: 'td-day__notes', 'data-day': 'mail' }, el('div', { class: 'td-day__src' }, D.mail, ' · ', mail.from || '', ' ', isUrl(mail.link) ? el('a', { href: mail.link, target: '_blank', rel: 'noopener noreferrer' }, D.openLink) : null),
|
|
889
941
|
el('p', null, el('b', null, (mail.subject || '') + '. '), mail.gist || '')) : null;
|
|
890
942
|
const after = e.ended ? el('div', { class: 'td-day__notes', 'data-day': 'notes' }, el('h4', null, D.notesTitle),
|
|
@@ -893,29 +945,35 @@ function eventDetail(ctx, e) {
|
|
|
893
945
|
list(D.decisions, (w.decisions || []).map(String), 'td-day__chk--ok'),
|
|
894
946
|
list(D.actions, (w.actions || []).map((a) => (typeof a === 'string' ? a : stepItem(a))))] : note(D.noWispr),
|
|
895
947
|
mailBlock || note(D.noMail)) : null;
|
|
896
|
-
const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text))
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
948
|
+
const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text)) : null;
|
|
949
|
+
const addGoalItem = !g && m.allGoals.length < m.maxGoals
|
|
950
|
+
? [{ label: D.addAsGoal, attrs: { 'data-day-act': 'add-goal' }, onSelect: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null)) toast({ text: D.goalAdded }); } }] : [];
|
|
951
|
+
// the one line under the title: the time, how many people join, and a star for a key meeting (F5)
|
|
952
|
+
const meta = [`${e.time}${e.end ? '–' + hhmm(e.end) : ''}`, facts && facts.people ? F.people(facts.people) : null, facts && facts.key ? F.key : null].filter(Boolean).join(' · ');
|
|
953
|
+
const duringAfter = [list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem))];
|
|
954
|
+
const links = Array.isArray(e.links) && e.links.length ? el('div', { class: 'td-day__acts', 'data-day': 'links' }, ...e.links.map((l) => extLink((l.label || l.url) + ' ↗', l.url))) : null;
|
|
955
|
+
const details = [
|
|
903
956
|
e.with ? peopleLinks(ctx.ws, e.with, { 'data-day': 'people' }) : null,
|
|
904
|
-
e.
|
|
905
|
-
e.hadPrep ? prepBlock(ctx, e) : null,
|
|
957
|
+
memoryBlock(e.id, { compact: true }),
|
|
906
958
|
ini ? el('div', { class: 'td-day__ini', 'data-day': 'initiative' }, el('span', null, D.initiative + ': ', el('b', { class: 'td-day__mono' }, ini.id), ini.why ? el('span', { class: 'td-day__dim' }, ' (' + D.suggested(ini.why) + ')') : null),
|
|
907
959
|
ctx.openIni ? button({ label: D.openInitiative, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'open-initiative' }, onClick: () => ctx.openIni(ini.id) }) : null) : null,
|
|
908
960
|
tie,
|
|
909
|
-
|
|
961
|
+
duringAfter,
|
|
962
|
+
e.ended ? null : mailBlock,
|
|
963
|
+
links,
|
|
964
|
+
];
|
|
965
|
+
const more = moreMenu(ctx, e, addGoalItem);
|
|
966
|
+
return [
|
|
967
|
+
el('div', { class: 'td-day__dhead' }, el('span', { class: 'td-day__kind td-day__meta', 'data-day': 'meta' }, meta), el('h3', null, e.title)),
|
|
968
|
+
e.ended ? el('p', { class: 'td-day__ended', 'data-day': 'ended', role: 'status' }, pill(D.pill.ended, ''), ' ', A.endedAt(hhmm(e.end || e.f))) : null,
|
|
969
|
+
e.purpose ? el('p', { class: 'td-day__purpose', 'data-day': 'purpose' }, e.purpose) : null,
|
|
970
|
+
started ? notesBlock(e, { ws: ctx.ws, initiative: ini, mode: 'live', liveText: F.live }) : null,
|
|
971
|
+
e.hadPrep ? prepBlock(ctx, e, { fold: started }) : null,
|
|
910
972
|
after,
|
|
911
|
-
notesBlock(e, { ws: ctx.ws, initiative: ini }),
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
? el('details', { class: 'td-day__fold', 'data-day': 'more' }, el('summary', null, A.moreMeeting((steps.during || []).length, (steps.after || []).length)),
|
|
916
|
-
list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), e.ended ? null : mailBlock) : null,
|
|
917
|
-
!e.hadPrep ? [list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), e.ended ? null : mailBlock] : null,
|
|
918
|
-
Array.isArray(e.links) && e.links.length ? el('div', { class: 'td-day__acts', 'data-day': 'links' }, ...e.links.map((l) => extLink((l.label || l.url) + ' ↗', l.url))) : null,
|
|
973
|
+
started ? null : notesBlock(e, { ws: ctx.ws, initiative: ini, mode: 'quiet' }),
|
|
974
|
+
el('div', { class: 'td-day__foot', 'data-day': 'foot' },
|
|
975
|
+
el('details', { class: 'td-day__fold td-day__details', 'data-day': 'details' }, el('summary', null, F.details), ...details),
|
|
976
|
+
more),
|
|
919
977
|
];
|
|
920
978
|
}
|
|
921
979
|
|
|
@@ -1281,6 +1339,6 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
1281
1339
|
|
|
1282
1340
|
export function daySig(ws) {
|
|
1283
1341
|
const c = cache.get(ws) || {};
|
|
1284
|
-
return JSON.stringify([forced, ui.open, ui.section, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
|
|
1342
|
+
return JSON.stringify([forced, ui.open, ui.section, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], [...ui.cur], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
|
|
1285
1343
|
+ dataSig(c.data) + goalsSig() + agendaSig();
|
|
1286
1344
|
}
|