create-pmos 0.180.0 → 0.182.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/c3-driver-test.js +6 -1
- package/workspace/c3-phone-test.js +38 -2
- package/workspace/guide/initiative-driver.md +2 -2
- package/workspace/guide/phone-companion.md +15 -7
- package/workspace/lib/c3-full-console.js +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-phone.js +16 -0
- package/workspace/public/pm/c3-ini-ui.js +15 -1
- package/workspace/public/pm/c3-initiatives.js +3 -6
- package/workspace/public/pm/c3-phone-detail.js +83 -0
- package/workspace/public/pm/c3-phone.css +0 -2
- package/workspace/public/pm/c3-phone.js +100 -25
- package/workspace/public/pm/c3-shell.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.182.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-13T03:18:01Z"}
|
|
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-13T03:18:01Z"}
|
|
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-13T03:18:01Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* work a Work task with a follow-up shows the agent line; Review opens the sheet with the draft and "the agent sent nothing";
|
|
14
14
|
* Keep records the keep; a task with a next-step shows Keep and Drop in the line
|
|
15
15
|
* setting Board settings has the Agent setting; pressing Shadow posts /api/driver/mode and the ledger holds it
|
|
16
|
-
* briefing Needs you shows the agent's two kinds of row and a press opens the initiative
|
|
16
|
+
* briefing Needs you shows the agent's two kinds of row, a long title wraps inside the row, and a press opens the initiative
|
|
17
17
|
* phone at 400 wide the strip has no horizontal scroll
|
|
18
18
|
* Every wait polls the page. The page must raise no script error.
|
|
19
19
|
*/
|
|
@@ -150,6 +150,11 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
150
150
|
await sel('[data-drv-need]');
|
|
151
151
|
const needs = await st(() => [...document.querySelectorAll('[data-drv-need]')].map((r) => r.dataset.drvNeed + ':' + r.textContent.replace(/\s+/g, ' ')));
|
|
152
152
|
ok('B1 Needs you shows the agent\'s rows: the drafts left on the initiative, and the initiative waiting on you', needs.some((t) => /^driver-drafts:Agent has 1 draft on Partner API onboarding/.test(t)) && needs.some((t) => /^driver-blocked:.*Partner API onboarding is waiting on you/.test(t)), needs.join(' | '));
|
|
153
|
+
const fit = await st(() => { const r = [...document.querySelectorAll('[data-drv-need="driver-drafts"]')].find((x) => x.offsetParent); const t = r.querySelector('.tt'); const was = t.textContent;
|
|
154
|
+
t.textContent = 'Agent has 2 drafts on I need to create recommendations for Scarlet product strategy, including whether to change it with something else entirely or keep it';
|
|
155
|
+
const nb = r.querySelector('.nb'); const out = { ws: getComputedStyle(t).whiteSpace, over: nb.scrollWidth - nb.clientWidth, right: t.getBoundingClientRect().right - r.getBoundingClientRect().right };
|
|
156
|
+
t.textContent = was; return out; });
|
|
157
|
+
ok('B3 a long title wraps inside the row: the title button is not nowrap and nothing runs past the card', fit.ws !== 'nowrap' && fit.over <= 1 && fit.right <= 1, JSON.stringify(fit));
|
|
153
158
|
await page.click('[data-drv-need="driver-drafts"] .tt');
|
|
154
159
|
await until(() => page.$('[data-drv="strip"]'));
|
|
155
160
|
ok('B2 a press on the row opens the initiative with its Agent strip', !!(await page.$('[data-drv="strip"]')));
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
/* c3-phone-test.js — the phone companion (initiative c3-phone-companion; PRD done-criteria 1 to 6 and 8).
|
|
3
|
+
/* c3-phone-test.js — the phone companion (initiative c3-phone-companion; PRD done-criteria 1 to 6 and 8): c3-phone.js, its sheets c3-phone-detail.js and its demo notes c3-demo-phone.js.
|
|
4
4
|
* A real server and a real Chromium drive /c3.html in demo mode (clock frozen at Thu 8 Oct 2026 08:12; ?at=HH:MM moves the companion's clock).
|
|
5
5
|
*
|
|
6
6
|
* phone at 400 wide the companion shows and the desktop grid does not
|
|
7
7
|
* moments 08:12 morning; 10:10 before Checkout sync; 10:45 in the meeting with the Record offer
|
|
8
8
|
* done Done on the waiting item records a write to /api/pm-items/done
|
|
9
9
|
* tell text sent from the ask bar records a capture and clears the bar
|
|
10
|
-
*
|
|
10
|
+
* brief the morning card counts every row the desktop Briefing counts; See all lists them; a row opens its details
|
|
11
|
+
* meeting a Later today row and the morning card open the meeting's sheet with its brief (GET /api/meeting-focus)
|
|
12
|
+
* record the red button shows the recording line and the notes being taken (just heard, decisions); Stop brings the bar back
|
|
11
13
|
* debrief 11:25 shows the decisions and actions of the recorded Checkout sync; a meeting with no recording asks you to tell PMOS
|
|
12
14
|
* desk Full console shows the desktop grid and survives a reload; More > Phone companion brings it back
|
|
13
15
|
* desktop at 1280 wide no companion is drawn
|
|
16
|
+
* 620 the companion shows up to 640 wide, where the phone styling starts
|
|
14
17
|
* No script error may be raised.
|
|
15
18
|
*/
|
|
16
19
|
const path = require('path');
|
|
@@ -76,6 +79,31 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
76
79
|
ok('done: Done records a write to /api/pm-items/done', await waitFor(page, (n) => ((window.__c3demoWrites || []).filter((w) => String(w.path || w.url || '').startsWith('/api/pm-items/done')).length) > n, before, 5000));
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/* ── the brief: everything the desktop Briefing counts waits here, and each row opens ── */
|
|
83
|
+
ok('brief: the morning card counts what waits on you', await waitFor(page, () => /wait on you|waits on you/.test(document.querySelector('[data-c3ph-moment="morning"]')?.textContent || ''), null, 8000));
|
|
84
|
+
ok('brief: the briefing needs are in the waiting list', await waitFor(page, () => !!document.querySelector('[data-c3ph-all-open]'), null, 8000));
|
|
85
|
+
if (await page.$('[data-c3ph-all-open]')) {
|
|
86
|
+
await page.click('[data-c3ph-all-open]');
|
|
87
|
+
const n = await waitFor(page, () => document.querySelectorAll('[data-c3ph-all] [data-c3ph-all-row]').length > 1, null, 5000);
|
|
88
|
+
ok('brief: See all lists every row that waits', n);
|
|
89
|
+
const nonPm = await page.evaluate(() => [...document.querySelectorAll('[data-c3ph-all-row]')].length);
|
|
90
|
+
await page.click('[data-c3ph-all-row] .pmc-row__open');
|
|
91
|
+
ok('brief: a row opens its details', await waitFor(page, () => !!document.querySelector('[data-c3ph-need]'), null, 5000), nonPm);
|
|
92
|
+
await page.keyboard.press('Escape');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ── a meeting opens its brief ── */
|
|
96
|
+
const laterRow = await waitFor(page, () => !!document.querySelector('[data-c3ph-later-row] .pmc-row__open'), null, 8000);
|
|
97
|
+
ok('meeting: the Later today rows can be opened', laterRow);
|
|
98
|
+
if (laterRow) {
|
|
99
|
+
await page.click('[data-c3ph-later-row] .pmc-row__open');
|
|
100
|
+
ok('meeting: the sheet shows the meeting', await waitFor(page, () => !!document.querySelector('[data-c3ph-meeting]'), null, 5000));
|
|
101
|
+
ok('meeting: the sheet reads the brief of the meeting', await waitFor(page, () => document.querySelector('[data-c3ph-brief]')?.dataset.c3phBrief === 'ready' && !!document.querySelector('[data-c3ph-sect="gist"]'), null, 8000),
|
|
102
|
+
await page.evaluate(() => document.querySelector('[data-c3ph-brief]')?.outerHTML.slice(0, 300)));
|
|
103
|
+
await page.keyboard.press('Escape');
|
|
104
|
+
}
|
|
105
|
+
ok('meeting: the morning card opens the brief of the next meeting', !!(await page.$('[data-c3ph-moment="morning"] [data-c3ph-meeting-open]')));
|
|
106
|
+
|
|
79
107
|
/* ── tell PMOS ── */
|
|
80
108
|
await page.fill('[data-c3ph-input]', 'Ask Maya about the beta date');
|
|
81
109
|
await page.press('[data-c3ph-input]', 'Enter');
|
|
@@ -86,6 +114,8 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
86
114
|
await page.click('[data-c3ph-record]');
|
|
87
115
|
ok('record: the recording line shows with Stop', await waitFor(page, () => !!document.querySelector('[data-c3ph-recbar] [data-c3ph-stop]'), null, 8000));
|
|
88
116
|
ok('record: the ask bar gives way while recording', !(await page.evaluate(() => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]'))));
|
|
117
|
+
ok('record: the notes being taken show while recording', await waitFor(page, () => { const c = document.querySelector('[data-c3ph-moment="live"]'); return !!c && c.dataset.c3phLive === 'ready' && !!c.querySelector('[data-c3ph-heard]') && !!c.querySelector('[data-c3ph-live-list="decisions"]'); }, null, 10000),
|
|
118
|
+
await page.evaluate(() => document.querySelector('[data-c3ph-moment]')?.outerHTML.slice(0, 300)));
|
|
89
119
|
if (await page.$('[data-c3ph-stop]')) await page.click('[data-c3ph-stop]');
|
|
90
120
|
ok('record: Stop brings the bar back', await waitFor(page, () => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]') && !document.querySelector('[data-c3ph-recbar]'), null, 8000));
|
|
91
121
|
await page.close();
|
|
@@ -131,6 +161,12 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
131
161
|
await page.close();
|
|
132
162
|
await pctx.close();
|
|
133
163
|
|
|
164
|
+
/* ── 620 wide: console3's phone styling starts at 640, so the companion must too (no squeezed desktop between) ── */
|
|
165
|
+
const mctx = await ctxOf(620);
|
|
166
|
+
page = await open(mctx, '/c3.html?demo=1');
|
|
167
|
+
ok('phone: the companion shows at 620 wide', await page.evaluate(shown, '[data-c3ph]'));
|
|
168
|
+
await mctx.close();
|
|
169
|
+
|
|
134
170
|
/* ── desktop ── */
|
|
135
171
|
const dctx = await ctxOf(1280);
|
|
136
172
|
page = await open(dctx, '/c3.html?demo=1', () => !!document.querySelector('nav.c3tabs [role="tab"]'));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "The Initiative Driver: drafts the agent files for you"
|
|
4
4
|
description: "What the Initiative Driver does when an initiative is created or moved, what stops it, how it files proposals you keep, send back or drop, the board modes, and the pmos driver commands."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-13
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# The Initiative Driver: drafts the agent files for you
|
|
@@ -55,7 +55,7 @@ In Discover, the agent writes the problem statement, the evidence, the opportuni
|
|
|
55
55
|
- **The Agent strip.** Open an initiative and stay on Overview. The Agent section is at the top. It shows the state of the agent (working, idle, waiting on you, paused or off), the last thing it did, and what stops it ("Stopped by: ..."). Each open proposal is one row with Keep, Send back and Drop. Press a row to read its text and its sources. A source that is a link opens in a new tab. Pause and Resume are in the header. The text "1 of 3 runs used today" opens the list of today's runs.
|
|
56
56
|
- **Keep, Send back and Drop on a row.** A kept row stays in the strip, marked "Kept by you", until you leave the page. Send back asks for a one-line note. A kept stage move changes the stage on the page at once. These three acts cannot be undone: each adds one line to the ledger.
|
|
57
57
|
- **One line in Work.** A Work task with an open proposal shows one line under its title, for example "Agent drafted a follow-up to Dana. Nothing is sent." Press Review to read the draft. Keep copies the text so you can send it yourself, and records your decision. If the browser does not allow copying, the text is selected and you press Ctrl+C. A suggested next step shows Keep and Drop in the line. Shadow and Off show no line.
|
|
58
|
-
- **Needs you.** In Briefing, the agent has two kinds of row: "Agent has 3 drafts on ..." and "... is waiting on you". Press a row to open the initiative.
|
|
58
|
+
- **Needs you.** In Briefing, the agent has two kinds of row: "Agent has 3 drafts on ..." and "... is waiting on you". Press a row to open the initiative. A long title wraps inside the card.
|
|
59
59
|
- **The Agent setting.** Open the board, press Board settings, and find Agent. Choose Off, Shadow or Approve. The setting is saved when you press it.
|
|
60
60
|
|
|
61
61
|
## Keep, send back, drop
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-13
|
|
|
7
7
|
|
|
8
8
|
# Guide: the phone companion
|
|
9
9
|
|
|
10
|
-
When the console (`/`) is
|
|
10
|
+
When the console (`/`) is 640 pixels wide or less, for example on a phone, it shows the companion and not the desktop pages. The companion is not a small copy of the desktop. It shows the one thing that is useful now, and it keeps decisions for your computer.
|
|
11
11
|
|
|
12
12
|
## The moment card
|
|
13
13
|
|
|
@@ -15,9 +15,9 @@ The top card shows where you are in your day and why it is useful now. PMOS choo
|
|
|
15
15
|
|
|
16
16
|
| Moment | When | What the card shows |
|
|
17
17
|
|---|---|---|
|
|
18
|
-
| Your day | Before noon, with no meeting in the next 30 minutes | The next meeting, the number of
|
|
19
|
-
| Before you walk in | The next meeting starts in 30 minutes or less | What is prepared for it,
|
|
20
|
-
| In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one
|
|
18
|
+
| Your day | Before noon, with no meeting in the next 30 minutes | The next meeting, the number of things that wait on you and how many are overdue, and **The brief of this meeting** |
|
|
19
|
+
| Before you walk in | The next meeting starts in 30 minutes or less | What is prepared for it, the items to close first, each with **Done**, and **The brief of this meeting** |
|
|
20
|
+
| In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one**, and **Open the meeting companion**, which opens the meeting focus page. |
|
|
21
21
|
| Just ended | A meeting ended less than 20 minutes ago | **What came out of it?** If you recorded the meeting, the card shows the summary, the decisions, the actions and the open questions from its notes, the same notes as on the Notes page. While PMOS writes the notes, the card says so. If you did not record it, **Tell PMOS** puts the meeting name in the bar so you can type the result. |
|
|
22
22
|
| Free time | 15 minutes or more before the next meeting | The items that fit in the time: overdue items first, then items due today, at most two. Each has **Done** and **Later today**. |
|
|
23
23
|
| Wrapping up | From 17:00, or after noon with no meeting left | What is still open and what waits for your computer |
|
|
@@ -26,7 +26,15 @@ When the moment changes, a short message shows at the top for 6 seconds. The mes
|
|
|
26
26
|
|
|
27
27
|
## Waiting on you
|
|
28
28
|
|
|
29
|
-
Under the moment card,
|
|
29
|
+
Under the moment card, the things that wait on you show one at a time ("1 of 9"). They are the same things that the **Needs you** section of the desktop Briefing counts: your own items, the Jira and plan items assigned to you that are overdue, blocked or due today, the items of the morning brief, and the decisions.
|
|
30
|
+
|
|
31
|
+
- For your own Work items, **Done** marks the item done; the message that follows has **Undo**. **Later today** shows the item again later.
|
|
32
|
+
- For the other rows, **Details** shows why the row is there, its facts and a link to where it lives. Decisions wait for your computer.
|
|
33
|
+
- **Next** shows the next row. **See all** lists every row; tap a row to see its details.
|
|
34
|
+
|
|
35
|
+
## Meetings
|
|
36
|
+
|
|
37
|
+
Under the card, **Later today** lists the rest of today's meetings. Tap a meeting to see its brief: the time, the people, the initiative, then the same brief as the meeting card of the desktop Calendar (in short, what matters today, what came out of the last one, what to be ready with, and the links). From the brief you can record the meeting or open the meeting companion.
|
|
30
38
|
|
|
31
39
|
## Tell PMOS
|
|
32
40
|
|
|
@@ -34,11 +42,11 @@ Type a note, a to-do or an idea in the bar at the bottom and press Enter. PMOS p
|
|
|
34
42
|
|
|
35
43
|
## Record
|
|
36
44
|
|
|
37
|
-
The red button records a conversation or a meeting with the same recorder as **Record** on the desktop. While it records, the bar shows the name, the time and **Stop
|
|
45
|
+
The red button records a conversation or a meeting with the same recorder as **Record** on the desktop. While it records, the bar shows the name, the time and **Stop**, and the top card shows the notes that are being taken: the last lines that were heard, and the decisions, actions and open questions that PMOS has found so far. The card reads the note again every 5 seconds.
|
|
38
46
|
|
|
39
47
|
## The full console
|
|
40
48
|
|
|
41
|
-
**Full console** at the top shows the desktop pages on this device, and the device keeps this choice. To go back, open **More** and choose **Phone companion**. In a window wider than
|
|
49
|
+
**Full console** at the top shows the desktop pages on this device, and the device keeps this choice. To go back, open **More** and choose **Phone companion**. In a window wider than 640 pixels the console always shows the desktop pages.
|
|
42
50
|
|
|
43
51
|
## Limits
|
|
44
52
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/* lib/c3-full-console.js — for browser suites that test the console's desktop layout at phone width.
|
|
3
|
-
* Since c3-phone-companion, console3 at
|
|
3
|
+
* Since c3-phone-companion, console3 at 640px or less shows the phone companion. The desktop layout stays reachable there with
|
|
4
4
|
* "Full console", which stores pmos.c3.phone = 'desk' on the device. fullConsole(browser) makes every new context start with
|
|
5
5
|
* that choice, so a suite that resizes to 400 wide still tests the desktop layout. c3-phone-test.js tests the companion itself. */
|
|
6
6
|
function fullConsole(browser) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.182.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.182.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -12,3 +12,19 @@ const NOTE = {
|
|
|
12
12
|
};
|
|
13
13
|
export const demoNoteFor = (m) => (m && (m.id === 'm2' || m.title === NOTE.title) ? NOTE : null);
|
|
14
14
|
export const demoNote = (id) => (id === NOTE.id ? { ok: true, note: NOTE } : null);
|
|
15
|
+
|
|
16
|
+
/* The note of a recording under way (?demo=1): what the phone companion shows while it records. The shapes are the real ones of GET
|
|
17
|
+
* /api/notes/one: transcript lines with their source, and the bullets the live notes pick out, each with its category. */
|
|
18
|
+
const LIVE = {
|
|
19
|
+
state: 'recording', speakers: { mic: 'You', meeting: 'Dara' },
|
|
20
|
+
transcript: [
|
|
21
|
+
{ t: 12, source: 'meeting', text: 'The sandbox keys are two days late, so 24 Oct is gone.' },
|
|
22
|
+
{ t: 31, source: 'mic', text: 'Then we hold beta on 30 Oct for the two partners already set up.' },
|
|
23
|
+
{ t: 48, source: 'meeting', text: 'I can get the new keys date in writing by Friday.' },
|
|
24
|
+
],
|
|
25
|
+
bullets: [
|
|
26
|
+
{ id: 0, cat: 'decision', text: 'Beta stays on 30 Oct for the two partners already set up' },
|
|
27
|
+
{ id: 1, cat: 'action', text: 'Dara gets the sandbox keys date in writing by Friday' },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
export const demoLive = (st) => ({ ok: true, note: { ...LIVE, id: st.id, title: st.title } });
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* day(iso) '12 Oct' · dayLong(iso) 'Mon 12 Oct 2026' · ago(iso, today) · daysBetween(a, b) · plural(n, one, many) · trunc(s, n)
|
|
25
25
|
*/
|
|
26
26
|
import { el, frag } from './dom.js';
|
|
27
|
-
import { button } from './primitives.js';
|
|
27
|
+
import { button, ensureComponentsCss } from './primitives.js';
|
|
28
28
|
import { c3ic } from './pm-work-view.js';
|
|
29
29
|
import { makeXp, attachExpand } from './c3-expand.js';
|
|
30
30
|
|
|
@@ -59,9 +59,23 @@ export function put(node, ...kids) {
|
|
|
59
59
|
return node;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Link c3-initiatives.css once, after components.css, so the `ini-b` reset comes after the primitive's own rules. btn() calls it: the
|
|
64
|
+
* Briefing, Work, Reports and Scan pages draw rows with these builders too, and without the reset a row title kept the primitive's nowrap
|
|
65
|
+
* and ran past its card. Every rule in the sheet is under an `ini-` or `in-` class, so it changes nothing else on those pages.
|
|
66
|
+
*/
|
|
67
|
+
export function ensureIniCss() {
|
|
68
|
+
if (typeof document === 'undefined' || document.querySelector('link[data-c3-ini]')) return;
|
|
69
|
+
ensureComponentsCss();
|
|
70
|
+
const l = document.createElement('link');
|
|
71
|
+
l.rel = 'stylesheet'; l.href = new URL('./c3-initiatives.css', import.meta.url).href; l.setAttribute('data-c3-ini', '');
|
|
72
|
+
document.head.appendChild(l);
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
/* ── controls ── */
|
|
63
76
|
/** A native button drawn as a console3 control. attrs: class (the console3 class), onclick, and any attribute. */
|
|
64
77
|
export function btn(attrs, ...kids) {
|
|
78
|
+
ensureIniCss();
|
|
65
79
|
const { onclick, class: cls, disabled, ...rest } = attrs || {};
|
|
66
80
|
const b = button({ variant: 'ghost', onClick: onclick, disabled: !!disabled, attrs: { ...rest, class: 'ini-b' + (cls ? ' ' + cls : '') } });
|
|
67
81
|
b.replaceChildren(frag(...kids.filter((k) => k != null && k !== false)));
|
|
@@ -43,7 +43,7 @@ import { openSheet, closeSheet } from './c3-sheet.js';
|
|
|
43
43
|
import { toast } from './c3-toast.js';
|
|
44
44
|
import {
|
|
45
45
|
btn, geo, icon, pbtn, pill, lk, ib, chip, tag, st, note, lbl, mono, sw, jiraLabel, plural, trunc, day, daysBetween, statusChip, healthSt, HEALTH, ownerChip, ownerText,
|
|
46
|
-
stageChip, stepsMini, progChip, dateChip, list, empty, sect, card, nrow, crumbs, lh, segTabs, cols, hbs, hbr, kv, chgl, chgli, openMenu, closeMenu, CATS, PORTFOLIO, put
|
|
46
|
+
stageChip, stepsMini, progChip, dateChip, list, empty, sect, card, nrow, crumbs, lh, segTabs, cols, hbs, hbr, kv, chgl, chgli, openMenu, closeMenu, CATS, PORTFOLIO, put, ensureIniCss
|
|
47
47
|
} from './c3-ini-ui.js';
|
|
48
48
|
import { TEMPLATES, BOARD_TEMPLATES, isBuildStage, templateBuilds, offered, wfOf, catOf, kindOf, tplName, TOOL_META, toolLabel, toolIcon, loadItems, dropItems, workTlRows, iniTlRow, rescheduleTask, lockedToast } from './c3-ini-work.js';
|
|
49
49
|
import { timeline, tlInit, tlRange } from './c3-ini-timeline.js';
|
|
@@ -102,11 +102,8 @@ export function formatLoc(loc) {
|
|
|
102
102
|
|
|
103
103
|
function ensureCss() {
|
|
104
104
|
ensureC3Css();
|
|
105
|
-
ensureComponentsCss();
|
|
106
|
-
|
|
107
|
-
const l = document.createElement('link');
|
|
108
|
-
l.rel = 'stylesheet'; l.href = new URL('./c3-initiatives.css', import.meta.url).href; l.setAttribute('data-c3-ini', '');
|
|
109
|
-
document.head.appendChild(l);
|
|
105
|
+
ensureComponentsCss();
|
|
106
|
+
ensureIniCss(); // after components.css, so the reset in c3-initiatives.css comes after the primitive's own rules
|
|
110
107
|
}
|
|
111
108
|
const warnChip = (w) => st('warn', 'alert', w, { title: 'An order rule, shown as a warning. It never blocks.', 'data-ini-warning': w });
|
|
112
109
|
const buildTag = (b) => (b.build ? el('span', { class: 'tag' }, icon('jira', 'sm'), jiraLabel(b) ? 'Jira · ' + jiraLabel(b) : 'Jira not set') : el('span', { class: 'tag' }, 'PMOS tasks only'));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* pm/c3-phone-detail.js — the phone companion's sheets (initiative c3-phone-companion). Each opens as the console3 bottom sheet (c3-sheet.js).
|
|
2
|
+
* openMeeting(m, opts) a meeting of today: when, who, the initiative, what is prepared, then its brief as GET /api/meeting-focus
|
|
3
|
+
* returns it (lib/calendar-layers.js briefOf, the same brief as the desktop Calendar card): In short, What matters,
|
|
4
|
+
* Since the last one, To prepare, links. opts: { onRecord(m), onFocus(m), recording } adds Record it and
|
|
5
|
+
* Open the meeting companion. A failed read keeps what the companion already knew and says so.
|
|
6
|
+
* openNeed(r, opts) one thing that waits on the PM (a row of waitRows in c3-phone.js): why, the facts, the source link, and
|
|
7
|
+
* Done and Later today when the row is the PM's own Work item. opts: { onDone(r), onLater(r) }
|
|
8
|
+
* openAll(rows, opts) every row that waits, as a list; a press opens that row (openNeed). opts as openNeed.
|
|
9
|
+
* Nothing here writes: the buttons call back into c3-phone.js, which owns the writes and their Undo. */
|
|
10
|
+
import { el } from './dom.js';
|
|
11
|
+
import { button, row } from './primitives.js';
|
|
12
|
+
import { pmGet } from './http.js';
|
|
13
|
+
import { c3ic } from './pm-work-view.js';
|
|
14
|
+
import { openSheet, closeSheet } from './c3-sheet.js';
|
|
15
|
+
|
|
16
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
17
|
+
const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p2(d.getMinutes()); };
|
|
18
|
+
const dayOf = (iso) => { const d = new Date(iso); return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()); };
|
|
19
|
+
const line = (t, attrs) => el('div', { class: 'c3ph-line', ...(attrs || {}) }, t);
|
|
20
|
+
const sect = (key, title, ...kids) => el('div', { class: 'c3ph-fit', 'data-c3ph-sect': key }, el('b', null, title), ...kids.filter(Boolean));
|
|
21
|
+
const btn = (label, variant, icon, attrs, onClick) => button({ label, variant, icon, attrs: { ...(attrs || {}), class: 'c3ph-b' }, onClick });
|
|
22
|
+
const link = (url, label) => el('a', { class: 'chip2', href: /^https?:\/\//.test(url) ? url : 'https://' + url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), label);
|
|
23
|
+
|
|
24
|
+
/** The parts of a briefOf brief, as phone sections. Pure over the brief; null parts are left out. */
|
|
25
|
+
export function briefParts(b) {
|
|
26
|
+
if (!b) return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
if (b.gist && b.gist.length) out.push(sect('gist', 'In short', line(b.gist.join(' '))));
|
|
29
|
+
if (b.matters && b.matters.length) out.push(sect('matters', b.planning ? 'To settle in planning' : 'What matters today', ...b.matters.map((x) => line([x.tag ? x.tag + ': ' : '', x.text, x.meta ? ' · ' + x.meta : ''].join('')))));
|
|
30
|
+
if (b.last) {
|
|
31
|
+
const l = b.last;
|
|
32
|
+
out.push(sect('last', 'Since the last one',
|
|
33
|
+
l.private ? line('That note is private, so only its title shows: ' + (l.title || 'Untitled note') + '.') : line(l.overview || 'That note has no summary yet.'),
|
|
34
|
+
...(l.private ? [] : (l.decisions || []).map((t) => line('Decision: ' + t))),
|
|
35
|
+
...(l.private ? [] : (l.actions || []).map((a) => line('From last time: ' + a.text + [a.owner, a.due && 'due ' + a.due].filter(Boolean).map((s) => ' · ' + s).join(''))))));
|
|
36
|
+
}
|
|
37
|
+
if (b.prep) out.push(sect('prep', 'To be ready', line(b.prep)));
|
|
38
|
+
const links = [...(b.links || []).filter((l) => l && l.url).map((l) => link(l.url, l.label)), ...(b.refs || []).filter((r) => r && r.link).map((r) => link(r.link, r.label))];
|
|
39
|
+
if (links.length) out.push(el('div', { class: 'chips2', 'data-c3ph-sect': 'links' }, ...links));
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function openMeeting(m, opts = {}) {
|
|
44
|
+
const known = [
|
|
45
|
+
line(hm(m.start) + (m.end ? '–' + hm(m.end) : '') + (m.minutes ? ' · ' + m.minutes + ' min' : '')),
|
|
46
|
+
m.people && m.people.length ? line(m.people.join(', ')) : null,
|
|
47
|
+
m.initiative ? line('Initiative: ' + m.initiative) : null,
|
|
48
|
+
...(m.prepared || []).map((x) => el('div', { class: 'c3ph-prep' }, c3ic(x.kind === 'prep' ? 'spark' : 'doc', 'sm'), x.text)),
|
|
49
|
+
...(m.before || []).map((x) => line('Before you go: ' + x.title)),
|
|
50
|
+
].filter(Boolean);
|
|
51
|
+
const brief = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-brief': 'loading' }, line('Reading the brief of this meeting.'));
|
|
52
|
+
const acts = el('div', { class: 'c3ph-acts' },
|
|
53
|
+
opts.onRecord && !opts.recording ? btn('Record it', 'primary', 'mic', { 'data-c3ph-sheet-record': m.id }, () => { closeSheet(); opts.onRecord(m); }) : null,
|
|
54
|
+
opts.onFocus ? btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-sheet-focus': m.id }, () => { closeSheet(); opts.onFocus(m); }) : null);
|
|
55
|
+
const sh = openSheet({ title: m.title, icon: 'calendar', body: [el('div', { class: 'c3ph-sheetbody', 'data-c3ph-meeting': m.id }, ...known), brief, acts] });
|
|
56
|
+
pmGet('/api/meeting-focus?event=' + encodeURIComponent(m.id) + '&date=' + dayOf(m.start))
|
|
57
|
+
.then((j) => {
|
|
58
|
+
const parts = j && j.ok !== false ? briefParts(j.brief) : null;
|
|
59
|
+
brief.dataset.c3phBrief = parts ? (parts.length ? 'ready' : 'empty') : 'error';
|
|
60
|
+
brief.replaceChildren(...(parts === null ? [line('The brief could not be read' + (j && j.error ? ': ' + j.error : '.'))]
|
|
61
|
+
: parts.length ? parts : [line('PMOS holds nothing more on this meeting. Link it to an initiative on your computer, and what matters there shows here.')]));
|
|
62
|
+
}, () => { brief.dataset.c3phBrief = 'error'; brief.replaceChildren(line('The brief could not be read.')); });
|
|
63
|
+
return sh;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function openNeed(r, opts = {}) {
|
|
67
|
+
const body = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-need': r.id },
|
|
68
|
+
el('span', { class: 'c3ph-step' }, r.label.toUpperCase()),
|
|
69
|
+
r.why ? line(r.why) : null,
|
|
70
|
+
...(r.facts || []).map((f) => line(f)),
|
|
71
|
+
r.url ? el('div', { class: 'chips2' }, link(r.url, r.urlLabel || 'Open the source')) : null,
|
|
72
|
+
r.pm ? el('div', { class: 'c3ph-acts two' },
|
|
73
|
+
btn('Done', 'primary', 'check', { 'data-c3ph-sheet-done': r.id }, () => { closeSheet(); opts.onDone && opts.onDone(r); }),
|
|
74
|
+
btn('Later today', 'ghost', 'clock', { 'data-c3ph-sheet-later': r.id }, () => { closeSheet(); opts.onLater && opts.onLater(r); }))
|
|
75
|
+
: line(r.decide ? 'A decision: make it on your computer, where the options and their evidence are.' : 'Change it where it lives, or on your computer.'));
|
|
76
|
+
return openSheet({ title: r.headline, icon: 'inbox', body });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function openAll(rows, opts = {}) {
|
|
80
|
+
const list = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-all': String(rows.length) },
|
|
81
|
+
...rows.map((r) => row({ title: r.headline, meta: [r.label], compact: true, onOpen: () => openNeed(r, opts), openLabel: 'Open ' + r.headline, attrs: { 'data-c3ph-all-row': r.id } })));
|
|
82
|
+
return openSheet({ title: rows.length + (rows.length === 1 ? ' thing waits on you' : ' things wait on you'), icon: 'inbox', body: list });
|
|
83
|
+
}
|
|
@@ -27,8 +27,6 @@
|
|
|
27
27
|
.c3 .c3ph-cnt{display:flex;justify-content:space-between;font-weight:600;font-size:var(--fs-2)}
|
|
28
28
|
.c3 .c3ph-cnt .dim{font-weight:400;font-family:var(--mono)}
|
|
29
29
|
.c3 .c3ph-later{display:grid;gap:4px;font-size:var(--fs-2);color:var(--muted-foreground);padding:0 4px}
|
|
30
|
-
.c3 .c3ph-later > div{display:flex;gap:10px}
|
|
31
|
-
.c3 .c3ph-later .mono{font-family:var(--mono);min-width:42px}
|
|
32
30
|
.c3 .c3ph-ft{position:sticky;bottom:0;display:flex;align-items:center;gap:10px;padding:8px 12px calc(12px + env(safe-area-inset-bottom));border-top:1px solid var(--border);background:var(--card);z-index:2}
|
|
33
31
|
.c3 .c3ph-ask{flex:1;display:flex;align-items:center;gap:8px;border:1px solid var(--border);border-radius:8px;padding:0 4px 0 12px;min-height:44px;color:var(--muted-foreground);background:var(--background)}
|
|
34
32
|
.c3 .c3ph-ask input.c3ph-in{flex:1;min-width:0;border:0;background:transparent;font-family:inherit;font-size:var(--fs-3);color:var(--foreground);padding:10px 0;outline:none;border-radius:0}
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* It has no tabs and no inbox to visit. It brings the one thing that matters now (c3-phone-model.js momentOf), then the PM's own items one
|
|
5
5
|
* at a time, then what comes later today. Two controls sit at the bottom: "Tell PMOS" and the red Record button.
|
|
6
|
-
* moment card meeting under way: "Record this meeting?" (Record / Not this one)
|
|
6
|
+
* moment card meeting under way: "Record this meeting?" (Record / Not this one), and the meeting companion (c3-focus.js, compact)
|
|
7
|
+
* recording (any recording): the live notes of GET /api/notes/one, read every 5 s: what was just heard, and the decisions,
|
|
8
|
+
* actions and questions picked out so far (dest-notes.js partsOf), as the desktop note shows them
|
|
7
9
|
* before a meeting: when it starts, who, what is prepared, the "before you go" items with Done
|
|
8
10
|
* just ended: tell PMOS what came out of it a free gap: the own items that fit, with Done and Later today
|
|
9
11
|
* morning: the next meeting and how much waits wrap: what is still open, what waits for the desk
|
|
10
|
-
* waiting card
|
|
11
|
-
*
|
|
12
|
+
* waiting card what waits on the PM, as the desktop Briefing counts it: the own items of GET /api/brief-page, then the needs of
|
|
13
|
+
* GET /api/briefing (assigned Jira and WBS items, Work items, morning brief items, decisions), one at a time, with See all.
|
|
14
|
+
* An own Work item has Done (POST /api/pm-items/done, Undo: /reopen) and Later today (POST /api/pm-items/notyet); any other row
|
|
15
|
+
* opens its details (c3-phone-detail.js openNeed). Decisions are not answered on the phone: they wait for the desk.
|
|
16
|
+
* meetings the next meeting and each row of Later today open the meeting's sheet with its brief (c3-phone-detail.js openMeeting).
|
|
12
17
|
* Tell PMOS a line of text, filed by POST /api/capture { ws, text }: it lands in the Work inbox, where triage files it. The small mic
|
|
13
18
|
* points to the keyboard's own dictation; this page does not send audio anywhere.
|
|
14
19
|
* Record the red button records a conversation or a meeting, the same recorder as the desktop Record control (note-dock.js).
|
|
@@ -23,6 +28,9 @@ import { onBriefPage, loadBriefPage } from './c3-brief-data.js';
|
|
|
23
28
|
import { onBriefing, loadBriefing } from './shell-ticker.js';
|
|
24
29
|
import { toast } from './c3-toast.js';
|
|
25
30
|
import { momentOf, inText } from './c3-phone-model.js';
|
|
31
|
+
import { openMeeting, openNeed, openAll } from './c3-phone-detail.js';
|
|
32
|
+
import { cardView } from './briefing-model.js';
|
|
33
|
+
import { row } from './primitives.js';
|
|
26
34
|
|
|
27
35
|
const CSS_ID = 'c3-phone-css';
|
|
28
36
|
const p2 = (n) => String(n).padStart(2, '0');
|
|
@@ -30,6 +38,7 @@ const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p
|
|
|
30
38
|
const first = (n) => String(n || '').trim().split(/\s+/)[0];
|
|
31
39
|
const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
32
40
|
const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
41
|
+
const LIVE_MS = 5000; // how often the note being recorded is read again
|
|
33
42
|
const clockOf = (msec) => { const s = Math.max(0, Math.floor(msec / 1000)); return Math.floor(s / 60) + ':' + p2(s % 60); };
|
|
34
43
|
|
|
35
44
|
function ensureCss() {
|
|
@@ -39,7 +48,7 @@ function ensureCss() {
|
|
|
39
48
|
|
|
40
49
|
/** Is this device in phone mode? Phone width, and the PM has not chosen the full console on it. */
|
|
41
50
|
export function phoneWanted(store) {
|
|
42
|
-
if (typeof matchMedia !== 'function' || !matchMedia('(max-width:
|
|
51
|
+
if (typeof matchMedia !== 'function' || !matchMedia('(max-width:640px)').matches) return false;
|
|
43
52
|
return !(store && store.get('pmos.c3.phone') === 'desk');
|
|
44
53
|
}
|
|
45
54
|
|
|
@@ -90,6 +99,20 @@ export function mountPhone(root, ctx = {}) {
|
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
if (S.clock) S.clock.textContent = st.mode === 'recording' && st.started ? clockOf(dock.now() - st.started) : st.mode === 'paused' ? 'paused' : '';
|
|
102
|
+
if (st.mode !== 'idle' && st.id && !S.liveBusy && (!S.live || S.live.id !== st.id || Date.now() - S.live.at > LIVE_MS)) readLive(st);
|
|
103
|
+
if (st.mode === 'idle' && S.live) { S.live = null; draw(); }
|
|
104
|
+
}
|
|
105
|
+
/* ── the note being recorded (GET /api/notes/one): what was just heard, and the decisions, actions and questions picked out so far ── */
|
|
106
|
+
async function readLive(st) {
|
|
107
|
+
S.liveBusy = true;
|
|
108
|
+
try {
|
|
109
|
+
const [src, dn, vn] = await Promise.all([demo ? import('./c3-demo-phone.js') : null, import('./dest-notes.js'), import('./view-notes.js')]);
|
|
110
|
+
const one = demo ? src.demoLive(st) : await pmGet('/api/notes/one?id=' + encodeURIComponent(st.id));
|
|
111
|
+
const n = one && one.ok !== false ? one.note : null;
|
|
112
|
+
S.live = { id: st.id, at: Date.now(), note: n, parts: n ? dn.partsOf(n) : null, speakerOf: vn.speakerOf };
|
|
113
|
+
} catch { S.live = { id: st.id, at: Date.now(), note: null, parts: null }; }
|
|
114
|
+
S.liveBusy = false;
|
|
115
|
+
draw();
|
|
93
116
|
}
|
|
94
117
|
|
|
95
118
|
/* ── writes ── */
|
|
@@ -146,24 +169,68 @@ export function mountPhone(root, ctx = {}) {
|
|
|
146
169
|
const card = (hero, attrs, ...kids) => el('section', { class: 'card c3ph-card' + (hero ? ' lead' : ''), ...(attrs || {}) }, ...kids);
|
|
147
170
|
const btn = (label, variant, icon, attrs, onClick) => button({ label, variant, icon, attrs: { ...(attrs || {}), class: 'c3ph-b' }, onClick });
|
|
148
171
|
const who = (m) => (m.people && m.people.length ? m.people.map(first).join(', ') : '');
|
|
149
|
-
const own = () => ((S.page && S.page.needs) || []).filter((n) => n && n.id && !S.gone.has(n.id));
|
|
150
|
-
|
|
172
|
+
const own = () => ((S.page && S.page.needs) || []).filter((n) => n && n.id && n.kind !== 'decision' && !S.gone.has(n.id));
|
|
173
|
+
/* everything that waits on the PM, as the desktop Briefing's Needs you counts it (c3-brief-needs.js needsCount): the own items of
|
|
174
|
+
* GET /api/brief-page, then the briefing's needs (assigned Jira and WBS items, own Work items, morning brief items, decisions). */
|
|
175
|
+
function waitRows() {
|
|
176
|
+
const out = []; const seen = new Set();
|
|
177
|
+
own().forEach((n) => { seen.add(n.id); out.push({ id: n.id, pm: true, headline: n.headline, raw: n, why: n.evidence && n.evidence.text ? 'Looks done: ' + n.evidence.text + '.' : null,
|
|
178
|
+
label: n.kind === 'overdue' ? (n.days_late === 1 ? '1 day late' : n.days_late + ' days late') : n.kind === 'due-today' ? 'Due today' : n.person ? 'Talk to ' + first(n.person) : 'Outside PMOS' }); });
|
|
179
|
+
((S.briefing && S.briefing.needs) || []).forEach((n, i) => {
|
|
180
|
+
if (!n) return;
|
|
181
|
+
const pm = n.origin === 'work' && !!n.itemId;
|
|
182
|
+
const id = pm ? n.itemId : String(n.key || 'need' + i);
|
|
183
|
+
if (seen.has(id) || (n.itemId && seen.has(n.itemId)) || S.gone.has(id)) return;
|
|
184
|
+
seen.add(id);
|
|
185
|
+
if (n.card) {
|
|
186
|
+
const v = cardView(n.card);
|
|
187
|
+
out.push({ id, pm: false, decide: true, headline: v.headline, label: v.tag + (v.deadline && v.deadline.text ? ' · ' + v.deadline.text : ''), why: n.card.why && n.card.why.text, url: n.card.url, urlLabel: n.card.url_label, facts: [] });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
out.push({ id, pm, decide: n.kind === 'Decide', headline: n.headline, label: [n.kind, n.dueText && n.dueText !== n.kind ? n.dueText : null, n.p].filter(Boolean).join(' · '),
|
|
191
|
+
why: n.why, url: n.url, urlLabel: n.url_label, facts: (n.facts || []).map((f) => f && f.text).filter(Boolean) });
|
|
192
|
+
});
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
const decisions = () => waitRows().filter((r) => !r.pm).length;
|
|
196
|
+
const needOpts = { onDone: (r) => itemDone(r), onLater: (r) => itemLater(r, 'later-today') };
|
|
197
|
+
const focus = (m) => import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.id, start: m.start }, pop: true })).catch(() => toast('The meeting companion did not open.'));
|
|
198
|
+
/* a meeting's sheet (c3-phone-detail.js): Record it while the meeting has not ended, and the meeting companion */
|
|
199
|
+
function meetingSheet(m) {
|
|
200
|
+
const t = now().getTime(); const ends = Date.parse(m.end || m.start) || 0;
|
|
201
|
+
openMeeting(m, { recording: recState().mode !== 'idle', onRecord: t < ends ? (x) => startRec(x) : null, onFocus: focus });
|
|
202
|
+
}
|
|
151
203
|
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
204
|
+
function liveCard(st, mo) {
|
|
205
|
+
const L = S.live && S.live.id === st.id ? S.live : null;
|
|
206
|
+
const n = L && L.note; const p = L && L.parts;
|
|
207
|
+
const ev = (n && n.meeting && n.meeting.event_id) ? { id: n.meeting.event_id, start: (mo.meeting && mo.meeting.start) || now().toISOString() } : (mo.kind === 'meeting' ? mo.meeting : null);
|
|
208
|
+
const heard = n ? (n.transcript || []).filter((x) => x && x.text && !x.dropped).slice(-3) : [];
|
|
209
|
+
const list = (label, rows) => (rows && rows.length ? el('div', { class: 'c3ph-fit', 'data-c3ph-live-list': label.toLowerCase() }, el('b', null, label + ' · ' + rows.length), ...rows.slice(-2).map((x) => line(x.text))) : null);
|
|
210
|
+
const ov = p && p.overview ? (p.overview.length > 220 ? p.overview.slice(0, 217) + '…' : p.overview) : '';
|
|
211
|
+
return card(true, { 'data-c3ph-moment': 'live', 'data-c3ph-live': !L ? 'loading' : n ? 'ready' : 'none' },
|
|
212
|
+
why(st.mode === 'paused' ? 'Paused' : 'Recording now'), big(st.title || 'Conversation'),
|
|
213
|
+
ov ? line(ov) : null,
|
|
214
|
+
p ? list('Decisions', p.decisions) : null, p ? list('Actions', p.actions) : null, p ? list('Open questions', p.questions) : null,
|
|
215
|
+
heard.length ? el('div', { class: 'c3ph-fit', 'data-c3ph-heard': String(heard.length) }, el('b', null, 'Just heard'), ...heard.map((x) => line((L.speakerOf ? L.speakerOf(n, x.source) + ': ' : '') + x.text)))
|
|
216
|
+
: line(!L ? 'Opening the notes.' : n ? 'Listening. What is said shows here, and PMOS picks out the decisions, actions and questions as they come.' : 'The notes of this recording could not be read. They are kept on your computer.'),
|
|
217
|
+
ev ? acts(btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-focus': ev.id }, () => focus(ev))) : null);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function itemCard(r, hero) {
|
|
221
|
+
if (!r.label) return itemCard(waitRows().find((x) => x.id === r.id) || { ...r, pm: true, label: 'Yours' }, hero);
|
|
222
|
+
const looks = r.raw && r.raw.evidence;
|
|
223
|
+
return [el('span', { class: 'c3ph-step' }, r.label.toUpperCase()), el('b', { class: 'c3ph-tt' }, r.headline),
|
|
224
|
+
r.why ? line(r.why) : null,
|
|
225
|
+
r.pm ? acts(btn(looks ? 'Confirm done' : 'Done', hero ? 'primary' : 'outline', 'check', { 'data-c3ph-done': r.id }, () => itemDone(r)),
|
|
226
|
+
btn('Later today', 'ghost', 'clock', { 'data-c3ph-later': r.id }, () => itemLater(r, 'later-today')))
|
|
227
|
+
: acts(btn('Details', 'outline', 'chevron-right', { 'data-c3ph-details': r.id }, () => openNeed(r, needOpts)))];
|
|
158
228
|
}
|
|
159
229
|
|
|
160
230
|
function momentCard(mo) {
|
|
161
231
|
const m = mo.meeting;
|
|
162
232
|
if (mo.kind === 'meeting') {
|
|
163
|
-
const
|
|
164
|
-
const recording = st.mode !== 'idle';
|
|
165
|
-
const open = btn('Open the meeting companion', recording ? 'primary' : 'outline', 'target', { 'data-c3ph-focus': m.id }, () => import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.id, start: m.start }, pop: true })).catch(() => toast('The meeting companion did not open.')));
|
|
166
|
-
if (recording) return card(true, { 'data-c3ph-moment': 'meeting' }, why('Under way · ends ' + inText(mo.minutes)), big(m.title), line('Recording. PMOS follows the talk and writes the notes.'), acts(open));
|
|
233
|
+
const open = btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-focus': m.id }, () => focus(m));
|
|
167
234
|
if (S.noRec.has(m.id)) return card(true, { 'data-c3ph-moment': 'meeting' }, why('Under way · ends ' + inText(mo.minutes)), big(m.title), line('Not recording. PMOS follows the agenda and what you mark.'), acts(open, btn('Record from now', 'ghost', 'mic', { 'data-c3ph-rec-meeting': m.id }, () => startRec(m))));
|
|
168
235
|
return card(true, { 'data-c3ph-moment': 'meeting' }, why('Starting now' + (who(m) ? ' · ' + who(m) : '')), big('Record ' + m.title + '?'),
|
|
169
236
|
el('div', { class: 'c3ph-rec-why' }, 'Recording lets PMOS follow the talk, bring up what you need at the right topic, and write the notes for you.'),
|
|
@@ -177,7 +244,8 @@ export function mountPhone(root, ctx = {}) {
|
|
|
177
244
|
...before.map((x) => el('div', { class: 'c3ph-before', 'data-c3ph-before': x.id }, el('span', null, 'Before you go: ' + x.title),
|
|
178
245
|
btn('Done', 'outline', 'check', { 'data-c3ph-done': x.id }, () => itemDone({ id: x.id, headline: x.title })))),
|
|
179
246
|
!(m.prepared || []).length && !before.length ? line('Nothing is prepared for it.') : null,
|
|
180
|
-
line('When it starts, PMOS asks to record it.', { 'data-c3ph-recnote': '' })
|
|
247
|
+
line('When it starts, PMOS asks to record it.', { 'data-c3ph-recnote': '' }),
|
|
248
|
+
acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': m.id }, () => meetingSheet(m))));
|
|
181
249
|
}
|
|
182
250
|
if (mo.kind === 'after') {
|
|
183
251
|
const db = debriefOf(m);
|
|
@@ -208,34 +276,40 @@ export function mountPhone(root, ctx = {}) {
|
|
|
208
276
|
...(fits.length ? fits.map((n) => el('div', { class: 'c3ph-fit', 'data-c3ph-fit': n.id }, ...itemCard(n, false))) : [line('Nothing of yours is due. A good moment to prepare the next meeting.')]));
|
|
209
277
|
}
|
|
210
278
|
if (mo.kind === 'morning') {
|
|
211
|
-
const
|
|
279
|
+
const w = waitRows(); const late = w.filter((r) => /overdue|late/i.test(r.label)).length;
|
|
212
280
|
return card(true, { 'data-c3ph-moment': 'morning' }, why('Your day'), big(mo.next ? mo.next.title + ' at ' + hm(mo.next.start) : 'No meetings today'),
|
|
213
|
-
line([mo.next ? 'First up, ' + inText((Date.parse(mo.next.start) - now().getTime()) / 60000) + '.' : null,
|
|
281
|
+
line([mo.next ? 'First up, ' + inText((Date.parse(mo.next.start) - now().getTime()) / 60000) + '.' : null,
|
|
282
|
+
w.length ? w.length + (w.length === 1 ? ' thing waits on you' : ' things wait on you') + (late ? ', ' + late + ' overdue.' : '.') : 'Nothing waits on you.'].filter(Boolean).join(' ')),
|
|
283
|
+
mo.next ? acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': mo.next.id }, () => meetingSheet(mo.next))) : null);
|
|
214
284
|
}
|
|
215
|
-
const o =
|
|
285
|
+
const o = waitRows().filter((r) => r.pm);
|
|
216
286
|
return card(true, { 'data-c3ph-moment': 'wrap' }, why('Wrapping up'), big(o.length ? o.length + ' still open today' : 'Nothing left for today'),
|
|
217
287
|
line(decisions() ? decisions() + (decisions() === 1 ? ' decision waits' : ' decisions wait') + ' for your desk. It opens first on your Briefing.' : 'Nothing waits for your desk.'));
|
|
218
288
|
}
|
|
219
289
|
|
|
220
290
|
function waitingCard(mo) {
|
|
221
291
|
const skipIds = new Set(((mo.kind === 'gap' && mo.fits) || []).map((n) => n.id));
|
|
222
|
-
const
|
|
292
|
+
const all = waitRows().filter((r) => !skipIds.has(r.id));
|
|
293
|
+
const list = all.filter((r) => !S.skipped.has(r.id));
|
|
223
294
|
if (!list.length) return null;
|
|
224
295
|
const n = list[0];
|
|
225
296
|
return card(false, { 'data-c3ph-waiting': n.id }, el('div', { class: 'c3ph-cnt' }, el('span', null, 'Waiting on you'), el('span', { class: 'dim' }, '1 of ' + list.length)),
|
|
226
|
-
...itemCard(n, false),
|
|
297
|
+
...itemCard(n, false),
|
|
298
|
+
el('div', { class: 'c3ph-acts two' },
|
|
299
|
+
list.length > 1 ? button({ label: 'Next', variant: 'ghost', size: 'sm', iconEnd: 'chevron-right', attrs: { 'data-c3ph-skip': n.id }, onClick: () => { S.skipped.add(n.id); draw(); } }) : null,
|
|
300
|
+
all.length > 1 ? button({ label: 'See all ' + all.length, variant: 'ghost', size: 'sm', attrs: { 'data-c3ph-all-open': '' }, onClick: () => openAll(all, needOpts) }) : null));
|
|
227
301
|
}
|
|
228
302
|
|
|
229
303
|
function laterList(mo) {
|
|
230
304
|
const rows = (mo.later || []).slice(0, 3);
|
|
231
305
|
if (!rows.length) return null;
|
|
232
306
|
return el('div', { class: 'c3ph-later', 'data-c3ph-later-list': '' }, el('span', { class: 'c3ph-step' }, 'LATER TODAY'),
|
|
233
|
-
...rows.map((m) =>
|
|
307
|
+
...rows.map((m) => row({ title: m.title, meta: [hm(m.start), who(m)], compact: true, onOpen: () => meetingSheet(m), openLabel: 'Open ' + m.title, attrs: { 'data-c3ph-later-row': m.id } })));
|
|
234
308
|
}
|
|
235
309
|
|
|
236
310
|
function headLine() {
|
|
237
311
|
const d = now();
|
|
238
|
-
const n =
|
|
312
|
+
const n = waitRows().length;
|
|
239
313
|
head.replaceChildren(el('b', null, WD[d.getDay()] + ' ' + d.getDate() + ' ' + MO[d.getMonth()]),
|
|
240
314
|
el('span', { class: 'c3ph-hdr' }, n ? el('span', { class: 'c3ph-dotn', 'data-c3ph-count': String(n) }, c3ic('inbox', 'sm'), n + ' waiting') : null,
|
|
241
315
|
button({ label: 'Full console', variant: 'ghost', size: 'sm', attrs: { 'data-c3ph-desk': '' }, onClick: () => ctx.onDesk && ctx.onDesk() })));
|
|
@@ -255,7 +329,8 @@ export function mountPhone(root, ctx = {}) {
|
|
|
255
329
|
S.kindKey = key;
|
|
256
330
|
host.dataset.moment = mo.kind;
|
|
257
331
|
headLine();
|
|
258
|
-
|
|
332
|
+
const st = recState();
|
|
333
|
+
body.replaceChildren(st.mode !== 'idle' ? liveCard(st, mo) : momentCard(mo), waitingCard(mo) || el('span'), laterList(mo) || el('span'));
|
|
259
334
|
}
|
|
260
335
|
|
|
261
336
|
const off1 = onBriefPage((j) => { S.page = j; draw(); });
|
|
@@ -151,7 +151,7 @@ function toggleMore() {
|
|
|
151
151
|
document.addEventListener('keydown', S.menuOff); document.addEventListener('pointerdown', S.menuOff);
|
|
152
152
|
const first = S.menu.querySelector('.c3pg-mi:not([disabled])'); if (first) first.focus();
|
|
153
153
|
}
|
|
154
|
-
/* The phone companion (c3-phone.js): at
|
|
154
|
+
/* The phone companion (c3-phone.js): at 640px or less, unless the PM chose the full console on this device, it fills the page and the
|
|
155
155
|
* desktop grid is hidden. "Full console" stores that choice; the More menu's "Phone companion" clears it. */
|
|
156
156
|
function syncPhone() {
|
|
157
157
|
const want = phoneWanted(store);
|
|
@@ -607,7 +607,7 @@ async function boot() {
|
|
|
607
607
|
mountAmbient(root); // the ambient motion canvas, once, behind the grid
|
|
608
608
|
mountQuiet(root); // the quiet layout: two rows above the content, no teaching sentences (c3-quiet-pages)
|
|
609
609
|
syncPhone(); // at phone width the companion replaces the desktop frame (c3-phone-companion)
|
|
610
|
-
if (typeof matchMedia === 'function') matchMedia('(max-width:
|
|
610
|
+
if (typeof matchMedia === 'function') matchMedia('(max-width:640px)').addEventListener('change', syncPhone);
|
|
611
611
|
|
|
612
612
|
S.dock.setWorkspaces(S.workspaces, S.ws);
|
|
613
613
|
mountBell(S.bellHost, { go: (_, t) => goTarget(t) });
|