create-pmos 0.172.0 → 0.174.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 +2 -2
- 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/payload/state/log.md +1 -1
- package/workspace/c3-driver-fixture.js +11 -8
- package/workspace/c3-ib-items-test.js +1 -0
- package/workspace/c3-quiet-test.js +142 -0
- package/workspace/c3-reports-test.js +17 -16
- package/workspace/guide/console-calendar.md +1 -1
- package/workspace/guide/console3-sessions.md +1 -1
- package/workspace/guide/data-freshness.md +1 -1
- package/workspace/guide/initiative-boards.md +3 -1
- package/workspace/lib/routes-scan.js +3 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-quiet.css +100 -0
- package/workspace/public/pm/c3-quiet.js +164 -0
- package/workspace/public/pm/c3-shell.js +10 -5
- package/workspace/test-files.json +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.174.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
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
kernel-source: pmos@
|
|
2
|
-
assembled: 2026-10-
|
|
1
|
+
kernel-source: pmos@7e5ec99
|
|
2
|
+
assembled: 2026-10-13
|
|
3
3
|
mode: vendor
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T01:01:21Z"}
|
|
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-13T01:01:21Z"}
|
|
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-13T01:01:21Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/payload/state/log.md
CHANGED
|
@@ -42,19 +42,22 @@ async function start({ name = 'c3-driver' } = {}) {
|
|
|
42
42
|
const b = (await post('/api/pm-items/add', { line: 'Send the partner onboarding list to Lina' })).body.item;
|
|
43
43
|
const tI = { kind: 'initiative', ws, id: 'drv-ui' };
|
|
44
44
|
const now = Date.now();
|
|
45
|
+
// Today's run must fall on today: a run 5 hours back is yesterday's between midnight and 05:00, and the strip then reads
|
|
46
|
+
// "0 of 3 runs used today". So the same-day timeline is squeezed into the time since midnight when that is shorter.
|
|
47
|
+
const H = Math.min(HOUR, Math.floor((now - new Date(now).setHours(0, 0, 0, 0)) / 6));
|
|
45
48
|
const w = (line, at) => D.append(ledger, line, { now: at });
|
|
46
49
|
const prop = (id, target, what, data, why, sources, at, run) => { w({ ev: 'proposal', id, target, what, data, why, sources: sources || [], run, by: 'agent:sess-4.0a1b2c3d' }, at); return id; };
|
|
47
50
|
const ids = { problem: 'p-aaaaaaaa01', evidence: 'p-aaaaaaaa02', sizing: 'p-aaaaaaaa03', move: 'p-aaaaaaaa04', follow: 'p-aaaaaaaa05', step: 'p-aaaaaaaa06' };
|
|
48
51
|
w({ ev: 'epoch' }, now - 3 * 24 * HOUR);
|
|
49
|
-
w({ ev: 'run-start', run: 'run-aaaaaaaa01', target: tI, board: ws, wakes: [], session: 'sess-4.0a1b2c3d' }, now - 5 *
|
|
50
|
-
prop(ids.problem, tI, 'problem', { text: 'New partners wait 9 days on average from signed contract to first live API call. Support handles 23 onboarding tickets a month.' }, 'first draft from the Jira and Slack threads', [{ label: 'Jira', ref: 'IDPF-1204' }, { label: 'Slack', ref: '#partner-support, 6 Oct' }], now - 4 *
|
|
51
|
-
prop(ids.evidence, tI, 'evidence', { text: 'Six sources back the problem statement. Four name sandbox access as the main delay.', items: [{ text: 'Partner X waited 9 days for sandbox access', ref: 'IDPF-1204' }, { text: 'Two partners paused go-live this quarter', ref: 'https://example.invalid/partner-council/1-oct' }] }, 'evidence for the problem', [{ label: 'Meeting note', ref: 'IDPF DSU, 8 Oct' }], now - 4 *
|
|
52
|
-
prop(ids.sizing, tI, 'sizing', { text: '14 partners are in the pipeline this half. Cutting the wait from 9 days to 3 moves about 5 go-lives into Q4.' }, 'low confidence: two data points', [{ label: 'Slack', ref: '#sales-asks, 7 Oct' }], now - 3 *
|
|
52
|
+
w({ ev: 'run-start', run: 'run-aaaaaaaa01', target: tI, board: ws, wakes: [], session: 'sess-4.0a1b2c3d' }, now - 5 * H);
|
|
53
|
+
prop(ids.problem, tI, 'problem', { text: 'New partners wait 9 days on average from signed contract to first live API call. Support handles 23 onboarding tickets a month.' }, 'first draft from the Jira and Slack threads', [{ label: 'Jira', ref: 'IDPF-1204' }, { label: 'Slack', ref: '#partner-support, 6 Oct' }], now - 4 * H, 'run-aaaaaaaa01');
|
|
54
|
+
prop(ids.evidence, tI, 'evidence', { text: 'Six sources back the problem statement. Four name sandbox access as the main delay.', items: [{ text: 'Partner X waited 9 days for sandbox access', ref: 'IDPF-1204' }, { text: 'Two partners paused go-live this quarter', ref: 'https://example.invalid/partner-council/1-oct' }] }, 'evidence for the problem', [{ label: 'Meeting note', ref: 'IDPF DSU, 8 Oct' }], now - 4 * H + H / 6, 'run-aaaaaaaa01');
|
|
55
|
+
prop(ids.sizing, tI, 'sizing', { text: '14 partners are in the pipeline this half. Cutting the wait from 9 days to 3 moves about 5 go-lives into Q4.' }, 'low confidence: two data points', [{ label: 'Slack', ref: '#sales-asks, 7 Oct' }], now - 3 * H, 'run-aaaaaaaa01');
|
|
53
56
|
prop(ids.move, tI, 'move', { to: 'Plan' }, 'the problem, evidence and sizing are in', [], now - 50 * HOUR, 'run-aaaaaaaa01');
|
|
54
|
-
w({ ev: 'run-end', run: 'run-aaaaaaaa01', ok: true, agent_run: null }, now - 3 *
|
|
55
|
-
w({ ev: 'blocker', key: `initiative:${ws}:drv-ui:pm-open`, day: D.localDay(now), target: tI, board: ws, code: 'pm-open', owner: 'pm', text: "The agent's drafts have waited on you since " + new Date(now - 50 * HOUR).toISOString().slice(0, 10) }, now -
|
|
56
|
-
prop(ids.follow, { kind: 'work', ws, id: a.id }, 'follow-up', { text: 'Hi Dana,\n\nQuick follow-up on the sandbox access for Nusa Pay. Can you tell me which day your engineers can use the sandbox?\n\nThanks,\nWawan', to: 'Dana' }, 'waiting 4 days, no reply', [{ label: 'Slack', ref: '#partner-support' }], now - 2 *
|
|
57
|
-
prop(ids.step, { kind: 'work', ws, id: b.id }, 'next-step', { text: 'Send the list to Lina', due: '2026-10-15' }, 'overdue with no next step', [], now -
|
|
57
|
+
w({ ev: 'run-end', run: 'run-aaaaaaaa01', ok: true, agent_run: null }, now - 3 * H + H / 60);
|
|
58
|
+
w({ ev: 'blocker', key: `initiative:${ws}:drv-ui:pm-open`, day: D.localDay(now), target: tI, board: ws, code: 'pm-open', owner: 'pm', text: "The agent's drafts have waited on you since " + new Date(now - 50 * HOUR).toISOString().slice(0, 10) }, now - H);
|
|
59
|
+
prop(ids.follow, { kind: 'work', ws, id: a.id }, 'follow-up', { text: 'Hi Dana,\n\nQuick follow-up on the sandbox access for Nusa Pay. Can you tell me which day your engineers can use the sandbox?\n\nThanks,\nWawan', to: 'Dana' }, 'waiting 4 days, no reply', [{ label: 'Slack', ref: '#partner-support' }], now - 2 * H, null);
|
|
60
|
+
prop(ids.step, { kind: 'work', ws, id: b.id }, 'next-step', { text: 'Send the list to Lina', due: '2026-10-15' }, 'overdue with no next step', [], now - H, null);
|
|
58
61
|
const stop = () => { try { srv.kill('SIGKILL'); } catch { /* gone */ } T.killTmux(); R.cleanup(); };
|
|
59
62
|
return { BASE, ws, board: ws, team, ledger, ids, item: a, itemDue: b, post, get, stop, T, D, tI };
|
|
60
63
|
}
|
|
@@ -58,6 +58,7 @@ const { harness } = require('./c3-ini-harness');
|
|
|
58
58
|
await page.click('[data-wk-src="pmos"]');
|
|
59
59
|
await until(() => st(() => document.querySelector('[data-wk-src="pmos"]').getAttribute('aria-pressed') === 'true'));
|
|
60
60
|
ok('F2 the Source filter keeps PMOS tasks only', await st(() => { const s = [...document.querySelectorAll('[data-ini-card]')].map((c) => c.dataset.src); return s.length > 0 && s.every((x) => x === 'pmos'); }));
|
|
61
|
+
await page.click('[data-ini="board-tabs"] .qt-more'); // Backlog sits behind More (c3-quiet-pages)
|
|
61
62
|
await page.click('[data-ini-tab="backlog"]');
|
|
62
63
|
await until(() => st(() => !!document.querySelector('[data-wk-tab="backlog"]')));
|
|
63
64
|
ok('F3 the filters stay on when the tab changes to Backlog', await st(() => document.querySelector('[data-wk-src="pmos"]').getAttribute('aria-pressed') === 'true' && [...document.querySelectorAll('[data-ini-item]')].every((n) => n.dataset.src === 'pmos')));
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/* c3-quiet-test.js — the quieter console3 pages (initiative c3-quiet-pages; PRD done-criteria 1 to 4 and 6).
|
|
4
|
+
* A real server and a real Chromium drive /c3.html: live mode for the header and a board, demo mode for Calendar and Reports.
|
|
5
|
+
*
|
|
6
|
+
* board at 1280 wide no page title, freshness line or sunrise text shows; the breadcrumb, the board tabs and Board settings
|
|
7
|
+
* share one row, and the content starts right under it; Dashboard is not shown
|
|
8
|
+
* fresh the freshness line is one dot beside the date; pressing it shows the full line
|
|
9
|
+
* more Backlog, Releases, Docs and Sprints show only after More; each still opens
|
|
10
|
+
* calendar one Layers button shows and hides the layer chips
|
|
11
|
+
* reports no deck is drawn
|
|
12
|
+
* phone at 400 wide nothing changes: the title shows and no dot is drawn (the phone gets its own companion design)
|
|
13
|
+
* No script error may be raised.
|
|
14
|
+
*/
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawn } = require('child_process');
|
|
17
|
+
const { findBrowser } = require('./lib/find-browser');
|
|
18
|
+
const T = require('./test-run').configure({ name: 'c3-quiet' });
|
|
19
|
+
|
|
20
|
+
const BASE = T.BASE;
|
|
21
|
+
const results = [];
|
|
22
|
+
const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + String(extra).slice(0, 400) : ''}`); };
|
|
23
|
+
let srv = null; let browser = null;
|
|
24
|
+
const finish = async (code) => {
|
|
25
|
+
if (browser) await browser.close().catch(() => {});
|
|
26
|
+
if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
|
|
27
|
+
T.killTmux();
|
|
28
|
+
const bad = results.filter((x) => !x.cond).length;
|
|
29
|
+
console.log(`\n${results.length - bad}/${results.length} c3-quiet assertions passed`);
|
|
30
|
+
process.exit(code !== undefined ? code : (bad ? 1 : 0));
|
|
31
|
+
};
|
|
32
|
+
process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
33
|
+
|
|
34
|
+
async function waitFor(page, fn, arg, ms = 30000) {
|
|
35
|
+
try { await page.waitForFunction(fn, arg, { timeout: ms }); return true; } catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
const shown = (s) => { const n = document.querySelector(s); return !!n && getComputedStyle(n).display !== 'none' && n.getBoundingClientRect().height > 0; };
|
|
38
|
+
|
|
39
|
+
(async () => {
|
|
40
|
+
const exe = findBrowser();
|
|
41
|
+
if (!exe) { console.log('✗ no Chrome/Chromium on this machine — the c3-quiet suite cannot run'); return finish(1); }
|
|
42
|
+
let chromium;
|
|
43
|
+
try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed — the c3-quiet suite cannot run'); return finish(1); }
|
|
44
|
+
|
|
45
|
+
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
46
|
+
await T.waitForServer(BASE);
|
|
47
|
+
browser = await chromium.launch({ executablePath: exe });
|
|
48
|
+
const errors = [];
|
|
49
|
+
const open = async (w, url) => {
|
|
50
|
+
const ctx = await browser.newContext({ viewport: { width: w, height: 860 } });
|
|
51
|
+
const page = await ctx.newPage();
|
|
52
|
+
page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
|
|
53
|
+
await page.goto(BASE + url, { waitUntil: 'load' });
|
|
54
|
+
await waitFor(page, () => document.querySelector('.c3')?.dataset.quiet === '1' && document.querySelector('nav.c3tabs [role="tab"]'));
|
|
55
|
+
return page;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/* ── board and header, live mode, 1280 ── */
|
|
59
|
+
let page = await open(1280, '/c3.html?tab=init');
|
|
60
|
+
const opened = await waitFor(page, () => !!document.querySelector('[data-ini-open-board]'));
|
|
61
|
+
ok('B0 the portfolio lists a board to open', opened);
|
|
62
|
+
if (opened) {
|
|
63
|
+
await page.evaluate(() => document.querySelector('[data-ini-open-board]').click());
|
|
64
|
+
const moved = await waitFor(page, () => { const t = document.querySelector('[data-ini-view="board"] [data-ini="board-tabs"]'), c = document.querySelector('[data-ini-view="board"] [data-ini="crumbs"]'); return t && c && t.parentElement === c.parentElement; });
|
|
65
|
+
ok('B1 the board tabs sit on the breadcrumb row', moved);
|
|
66
|
+
await page.waitForTimeout(300);
|
|
67
|
+
const g = await page.evaluate((shownSrc) => {
|
|
68
|
+
const vis = new Function('s', 'return (' + shownSrc + ')(s)');
|
|
69
|
+
const t = document.querySelector('[data-ini-view="board"] [data-ini="board-tabs"]').getBoundingClientRect(), c = document.querySelector('[data-ini-view="board"] [data-ini="crumbs"]').getBoundingClientRect();
|
|
70
|
+
const nav = document.querySelector('nav.c3tabs').getBoundingClientRect();
|
|
71
|
+
const body = document.querySelector('[data-ini-view="board"] .bcols, [data-ini-view="board"] .calbody');
|
|
72
|
+
return {
|
|
73
|
+
title: vis('.c3top h1'), freshLine: vis('.c3top .fr-fresh'), daylight: vis('.c3pg-daylight'),
|
|
74
|
+
sameRow: Math.abs((t.top + t.bottom) / 2 - (c.top + c.bottom) / 2) < 12, ctxGap: Math.round(c.top - nav.bottom),
|
|
75
|
+
contentGap: body ? Math.round(body.getBoundingClientRect().top - Math.max(t.bottom, c.bottom)) : null,
|
|
76
|
+
dash: [...document.querySelectorAll('[data-ini-view="board"] button')].some((b) => b.textContent.trim() === 'Dashboard' && b.getBoundingClientRect().height > 0),
|
|
77
|
+
gear: (() => { const b = document.querySelector('[data-ini="board-settings"]'), row = document.querySelector('[data-ini-view="board"] [data-ini="crumbs"]').parentElement; return !!b && row.contains(b) && b.getBoundingClientRect().height > 0; })(),
|
|
78
|
+
status: (document.querySelector('[data-ini-view="board"] [data-ini="crumbs"] .qt-status') || {}).textContent || '',
|
|
79
|
+
};
|
|
80
|
+
}, shown.toString());
|
|
81
|
+
ok('B2 no page title, freshness line or sunrise text shows', !g.title && !g.freshLine && !g.daylight, JSON.stringify(g));
|
|
82
|
+
ok('B3 the breadcrumb and the tabs share one row, right under the page tabs', g.sameRow && g.ctxGap >= 0 && g.ctxGap < 40, JSON.stringify(g));
|
|
83
|
+
ok('B4 the content starts right under that row', g.contentGap !== null && g.contentGap >= 0 && g.contentGap < 48, JSON.stringify(g));
|
|
84
|
+
ok('B5 the board status reads on the breadcrumb row', g.status.length > 0, JSON.stringify(g));
|
|
85
|
+
ok('B6 Board settings sits on the same row; Dashboard is not shown', g.gear && !g.dash, JSON.stringify(g));
|
|
86
|
+
|
|
87
|
+
/* More */
|
|
88
|
+
const tabVis = (k) => page.evaluate((k) => { const n = document.querySelector(`[data-ini="board-tabs"] [data-ini-tab="${k}"]`); return !!n && getComputedStyle(n).display !== 'none'; }, k);
|
|
89
|
+
const hidden = [];
|
|
90
|
+
for (const k of ['backlog', 'releases', 'docs', 'sprints']) if (await page.$(`[data-ini="board-tabs"] [data-ini-tab="${k}"]`)) hidden.push(!(await tabVis(k)));
|
|
91
|
+
ok('M1 Backlog, Releases, Docs and Sprints are behind More', hidden.length > 0 && hidden.every(Boolean), JSON.stringify(hidden));
|
|
92
|
+
ok('M2 Summary still shows on the row', await tabVis('summary'));
|
|
93
|
+
await page.click('[data-ini="board-tabs"] .qt-more');
|
|
94
|
+
ok('M3 More shows them', await tabVis('backlog') && await page.getAttribute('[data-ini="board-tabs"] .qt-more', 'aria-expanded') === 'true');
|
|
95
|
+
await page.click('[data-ini="board-tabs"] [data-ini-tab="backlog"]');
|
|
96
|
+
ok('M4 Backlog opens and stays shown while it is the open tab',
|
|
97
|
+
await waitFor(page, () => document.querySelector('[data-ini="board-tabs"] [data-ini-tab="backlog"]')?.getAttribute('aria-pressed') === 'true') && await tabVis('backlog'));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* the freshness dot */
|
|
101
|
+
const hasLine = await page.$('.c3top .fr-fresh');
|
|
102
|
+
if (hasLine) {
|
|
103
|
+
const dot = await waitFor(page, () => !!document.querySelector('.c3top [data-c3pg="fresh-dot"]'));
|
|
104
|
+
const label = dot ? await page.getAttribute('[data-c3pg="fresh-dot"]', 'aria-label') : '';
|
|
105
|
+
ok('F1 the freshness line is one dot beside the date, named by what it says', dot && /^(\d+ sources? not read|Sources read)\./.test(label || ''), label);
|
|
106
|
+
await page.click('[data-c3pg="fresh-dot"]');
|
|
107
|
+
ok('F2 pressing the dot shows the full line', await page.evaluate(shown, '.c3top .fr-fresh') && await page.getAttribute('[data-c3pg="fresh-dot"]', 'aria-expanded') === 'true');
|
|
108
|
+
await page.click('[data-c3pg="fresh-dot"]');
|
|
109
|
+
ok('F3 pressing it again hides the line', !(await page.evaluate(shown, '.c3top .fr-fresh')));
|
|
110
|
+
} else ok('F0 this workspace draws no freshness line, so no dot is needed', !(await page.$('[data-c3pg="fresh-dot"]')));
|
|
111
|
+
await page.context().close();
|
|
112
|
+
|
|
113
|
+
/* ── Calendar, demo mode ── */
|
|
114
|
+
page = await open(1280, '/c3.html?demo=1&tab=cal');
|
|
115
|
+
const lay = await waitFor(page, () => !!document.querySelector('[data-c3pg="cal-layers"]'));
|
|
116
|
+
ok('C1 Calendar has one Layers button', lay);
|
|
117
|
+
if (lay) {
|
|
118
|
+
const chips = () => page.evaluate(() => { const n = document.querySelector('[data-cal3="chips"]'); return !!n && getComputedStyle(n).display !== 'none'; });
|
|
119
|
+
ok('C2 the layer chips are folded', !(await chips()));
|
|
120
|
+
await page.click('[data-c3pg="cal-layers"]');
|
|
121
|
+
ok('C3 Layers shows the chips', await chips() && await page.getAttribute('[data-c3pg="cal-layers"]', 'aria-expanded') === 'true');
|
|
122
|
+
await page.click('[data-c3pg="cal-layers"]');
|
|
123
|
+
ok('C4 pressing it again folds them', !(await chips()));
|
|
124
|
+
}
|
|
125
|
+
await page.context().close();
|
|
126
|
+
|
|
127
|
+
/* ── Reports, demo mode ── */
|
|
128
|
+
page = await open(1280, '/c3.html?demo=1&tab=rep');
|
|
129
|
+
await waitFor(page, () => !!document.querySelector('[data-rp3]'));
|
|
130
|
+
ok('R1 Reports draws no deck', await page.evaluate(() => [...document.querySelectorAll('[data-rp3-deck]')].every((n) => getComputedStyle(n).display === 'none')));
|
|
131
|
+
await page.context().close();
|
|
132
|
+
|
|
133
|
+
/* ── phone: unchanged ── */
|
|
134
|
+
page = await open(400, '/c3.html?tab=init');
|
|
135
|
+
await page.waitForTimeout(400);
|
|
136
|
+
const ph = await page.evaluate((shownSrc) => { const vis = new Function('s', 'return (' + shownSrc + ')(s)'); return { title: vis('.c3top h1'), dot: !!document.querySelector('[data-c3pg="fresh-dot"]') }; }, shown.toString());
|
|
137
|
+
ok('P1 at 400 wide the page keeps today\'s layout: the title shows and no dot is drawn', ph.title && !ph.dot, JSON.stringify(ph));
|
|
138
|
+
await page.context().close();
|
|
139
|
+
|
|
140
|
+
ok('E1 no script error', errors.length === 0, errors.join(' | '));
|
|
141
|
+
finish();
|
|
142
|
+
})();
|
|
@@ -78,6 +78,7 @@ document.getElementById('c3-root').setAttribute('data-c3-demo', '1');
|
|
|
78
78
|
</script></body></html>`;
|
|
79
79
|
|
|
80
80
|
const D = '[data-rp3-pane="dash"]';
|
|
81
|
+
const ACT = '[data-rp3]'; // Share, Edit dashboard and Reset sit on the tab row at desktop width (c3-quiet-pages), inside the pane on a phone
|
|
81
82
|
const order = (page) => page.$$eval(D + ' [data-rp3-w]', (a) => a.map((n) => n.getAttribute('data-rp3-w')));
|
|
82
83
|
const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'maturity'];
|
|
83
84
|
|
|
@@ -184,18 +185,18 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
184
185
|
await page.click('[data-rp3-scope="board"]'); await loaded(page, 5);
|
|
185
186
|
const reads = await page.evaluate(() => window.__rp3spy.reads);
|
|
186
187
|
ok('the data route is read once per scope and id, and not again within the minute', JSON.stringify(reads) === JSON.stringify(['board|shop', 'board|enterprise', 'initiative|checkout', 'initiative|sso', 'initiative|onb']), JSON.stringify(reads));
|
|
187
|
-
await page.click(
|
|
188
|
+
await page.click(ACT + ' [data-rp3-edit]');
|
|
188
189
|
const freeB = await page.$$eval(D + ' [data-rp3-free]', (a) => a.map((n) => n.getAttribute('data-rp3-free')));
|
|
189
190
|
ok('the Board add list holds only reports that fit a board (no north star, no maturity)', !freeB.includes('north') && !freeB.includes('maturity'), JSON.stringify(freeB));
|
|
190
|
-
await page.click(
|
|
191
|
+
await page.click(ACT + ' [data-rp3-edit]');
|
|
191
192
|
await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
|
|
192
193
|
|
|
193
194
|
/* ── edit ── */
|
|
194
195
|
await spy(page);
|
|
195
|
-
await page.click(
|
|
196
|
-
ok('Edit dashboard turns edit mode on, with arrows and Remove on each widget', await page.getAttribute(
|
|
196
|
+
await page.click(ACT + ' [data-rp3-edit]');
|
|
197
|
+
ok('Edit dashboard turns edit mode on, with arrows and Remove on each widget', await page.getAttribute(ACT + ' [data-rp3-edit]', 'aria-pressed') === 'true' && (await page.$$(D + ' [data-rp3-rm]')).length === 7 && (await page.$$(D + ' [data-rp3-up]')).length === 7);
|
|
197
198
|
ok('the first widget cannot move up, the last cannot move down', await page.$eval(D + ' [data-rp3-up="north"]', (b) => b.disabled) && await page.$eval(D + ' [data-rp3-down="maturity"]', (b) => b.disabled));
|
|
198
|
-
ok('Reset to default is off while the scope shows its default', await page.$eval(
|
|
199
|
+
ok('Reset to default is off while the scope shows its default', await page.$eval(ACT + ' [data-rp3-reset]', (b) => b.disabled));
|
|
199
200
|
await page.click(D + ' [data-rp3-up="outcomes"]');
|
|
200
201
|
ok('up moves a report up', !!(await until(async () => JSON.stringify(await order(page)) === JSON.stringify(['outcomes', 'north', 'signals', 'delivery', 'bets', 'synergy', 'maturity']))), JSON.stringify(await order(page)));
|
|
201
202
|
let P = await posts(page, '/api/report-dashboards/layout');
|
|
@@ -216,7 +217,7 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
216
217
|
ok('Add puts a library report at the end of the dashboard', !!(await until(async () => (await order(page)).slice(-1)[0] === 'maturity')));
|
|
217
218
|
P = await posts(page, '/api/report-dashboards/layout');
|
|
218
219
|
ok('every change POSTed its layout (move, move, remove, undo, remove, add)', P.length === 6 && P.every((q) => q.body.key === 'portfolio' && Array.isArray(q.body.reports)), String(P.length));
|
|
219
|
-
ok('Reset to default is on once the scope has its own layout', !(await page.$eval(
|
|
220
|
+
ok('Reset to default is on once the scope has its own layout', !(await page.$eval(ACT + ' [data-rp3-reset]', (b) => b.disabled)));
|
|
220
221
|
// a failed POST: the page keeps the change and says why
|
|
221
222
|
await page.evaluate(() => { window.__rp3spy.fail = '/api/report-dashboards/layout'; });
|
|
222
223
|
await page.click(D + ' [data-rp3-down="outcomes"]');
|
|
@@ -225,22 +226,22 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
225
226
|
await page.evaluate(() => { window.__rp3spy.fail = null; });
|
|
226
227
|
await page.click(D + ' [data-rp3-up="outcomes"]');
|
|
227
228
|
await until(async () => (await order(page)).join(',') === 'outcomes,signals,north,delivery,bets,synergy,maturity');
|
|
228
|
-
await page.click(
|
|
229
|
-
ok('Done editing leaves edit mode', await page.getAttribute(
|
|
229
|
+
await page.click(ACT + ' [data-rp3-edit]');
|
|
230
|
+
ok('Done editing leaves edit mode', await page.getAttribute(ACT + ' [data-rp3-edit]', 'aria-pressed') === 'false' && !(await page.$(D + ' [data-rp3-rm]')));
|
|
230
231
|
|
|
231
232
|
/* ── the layout survives a reload ── */
|
|
232
233
|
await open(page); await loaded(page, 7);
|
|
233
234
|
ok('a stored layout is there after a reload', (await order(page)).join(',') === 'outcomes,signals,north,delivery,bets,synergy,maturity', JSON.stringify(await order(page)));
|
|
234
235
|
await spy(page);
|
|
235
|
-
await page.click(
|
|
236
|
+
await page.click(ACT + ' [data-rp3-edit]'); await page.click(ACT + ' [data-rp3-reset]');
|
|
236
237
|
ok('Reset to default returns the default order', !!(await until(async () => JSON.stringify(await order(page)) === JSON.stringify(DEF_P))), JSON.stringify(await order(page)));
|
|
237
238
|
P = await posts(page, '/api/report-dashboards/layout');
|
|
238
239
|
ok('Reset POSTs reports: null', P.length === 1 && P[0].body.reports === null && P[0].body.key === 'portfolio', JSON.stringify(P));
|
|
239
|
-
await page.click(
|
|
240
|
+
await page.click(ACT + ' [data-rp3-edit]');
|
|
240
241
|
// a board layout is kept per key
|
|
241
242
|
await page.click('[data-rp3-scope="board"]'); await loaded(page, 5);
|
|
242
|
-
await page.click(
|
|
243
|
-
await page.click(
|
|
243
|
+
await page.click(ACT + ' [data-rp3-edit]'); await page.click(D + ' [data-rp3-rm="synergy"]'); await until(async () => (await order(page)).length === 4);
|
|
244
|
+
await page.click(ACT + ' [data-rp3-edit]');
|
|
244
245
|
P = await posts(page, '/api/report-dashboards/layout');
|
|
245
246
|
ok('a board layout is POSTed under board:<id>', P.length === 2 && P[1].body.key === 'board:shop', JSON.stringify(P.map((q) => q.body.key)));
|
|
246
247
|
await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
|
|
@@ -249,7 +250,7 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
249
250
|
/* ── share ── */
|
|
250
251
|
ok('the head says the portfolio is scheduled, draft only (the demo seeds a monthly schedule)', /Scheduled: Monthly, 1st at 08:00, draft only/.test(await page.textContent(D + ' [data-rp3-schedline]')));
|
|
251
252
|
await spy(page);
|
|
252
|
-
await page.click(
|
|
253
|
+
await page.click(ACT + ' [data-rp3-share]');
|
|
253
254
|
const sheet = await until(() => page.$('[data-rp3-sheet] [data-rp3-frame="Headline"]'));
|
|
254
255
|
ok('Share opens a sheet with the report frame', !!sheet);
|
|
255
256
|
const frames = await page.$$eval('[data-rp3-sheet] [data-rp3-frame] h2', (a) => a.map((h) => h.textContent.replace(/\d+$/, '')));
|
|
@@ -336,10 +337,10 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
336
337
|
const lefts = await pp.$$eval(D + ' [data-rp3-w]', (a) => [...new Set(a.map((w) => Math.round(w.getBoundingClientRect().left)))]);
|
|
337
338
|
ok('400px: the widgets stack in one column', lefts.length === 1, JSON.stringify(lefts));
|
|
338
339
|
await pp.click('[data-rp3-scope="initiative"]'); await loaded(pp, 4);
|
|
339
|
-
await pp.click(
|
|
340
|
+
await pp.click(ACT + ' [data-rp3-edit]');
|
|
340
341
|
ok('400px: Initiative scope in edit mode has no horizontal scroll', await noScroll(pp));
|
|
341
|
-
await pp.click(
|
|
342
|
-
await pp.click(
|
|
342
|
+
await pp.click(ACT + ' [data-rp3-edit]');
|
|
343
|
+
await pp.click(ACT + ' [data-rp3-share]'); await until(() => pp.$('[data-rp3-sheet] [data-rp3-frame]'));
|
|
343
344
|
ok('400px: the share sheet has no horizontal scroll', await noScroll(pp));
|
|
344
345
|
await pp.keyboard.press('Escape');
|
|
345
346
|
await pp.click('[data-rp3-tab="lib"]'); await pp.click('[data-rp3-pane="lib"] [data-home="delivery"]');
|
|
@@ -35,7 +35,7 @@ The keys work too. Press `+` to zoom in and `-` to zoom out. Press the left or t
|
|
|
35
35
|
|
|
36
36
|
## Layers
|
|
37
37
|
|
|
38
|
-
The chips under the zoom bar are the layers. Each chip shows the number of items in the range. Press a chip to hide its items, and press it again to show them.
|
|
38
|
+
The chips under the zoom bar are the layers. On a screen wider than 600 pixels they are folded: press **Layers** in the zoom bar to show them, and press it again to fold them. They stay shown until you fold them or reload the page. Each chip shows the number of items in the range. Press a chip to hide its items, and press it again to show them.
|
|
39
39
|
|
|
40
40
|
| Chip | What it shows |
|
|
41
41
|
|---|---|
|
|
@@ -11,7 +11,7 @@ Press the **Sessions** button in the console3 header to open the panel. It is th
|
|
|
11
11
|
|
|
12
12
|
## The dock
|
|
13
13
|
|
|
14
|
-
The dock is
|
|
14
|
+
The dock is folded when you first open the console. To open it, press Ctrl J (⌘J on a Mac), or use **Open the dock** in the **More** menu. The browser keeps your choice: when you leave the dock open, it opens on the Briefing next time. On the other tabs it is folded away, and no rail of buttons is left at the right edge.
|
|
15
15
|
|
|
16
16
|
## Start a session
|
|
17
17
|
|
|
@@ -44,7 +44,7 @@ The record is kept in your own runtime folder on this machine, never in a reposi
|
|
|
44
44
|
|
|
45
45
|
## Where you see it
|
|
46
46
|
|
|
47
|
-
Each page shows a short line
|
|
47
|
+
Each page shows a short line that says when each source was last read, for example **Initiatives read 08:02 · Calendar read 07:15 · Jira status read 06:31**. On a screen wider than 600 pixels, the line is folded into one dot next to the date. The dot is amber and says how many sources are not read, for example **2 sources not read**, or it says **Sources read**. Press the dot to show the full line, and press it again to fold it. The line never says that the data is up to date, because the agents read these sources on a schedule and not live.
|
|
48
48
|
|
|
49
49
|
- A source older than its limit is amber.
|
|
50
50
|
- A source that was never read says **not read yet**, in amber.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Initiatives on the console: Portfolio, boards and workflows"
|
|
4
4
|
description: "How to use the Initiatives tab of the console page (/): the Portfolio of boards, a board and its nine tabs, an initiative with its own workflow and stage records, and the Build and deliver setting."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Initiatives on the console: Portfolio, boards and workflows
|
|
@@ -24,6 +24,8 @@ Under the rows are a timeline of all boards (board › initiative › epic) and
|
|
|
24
24
|
|
|
25
25
|
The tabs are **Summary**, **Initiatives**, **Triage**, **Backlog**, **Board**, **Timeline**, **Releases**, **Docs** and **Sprints**. A board with Build and deliver off has no Releases and no Sprints tab.
|
|
26
26
|
|
|
27
|
+
On a screen wider than 600 pixels, the breadcrumb, the board status, the tabs and the Board settings gear are on one row. **Backlog**, **Releases**, **Docs** and **Sprints** are behind **More**. Press **More** to show them. When one of them is the open tab, it stays shown.
|
|
28
|
+
|
|
27
29
|
- **Summary** shows the initiatives at a glance and one bar per category. At a glance lists up to six open initiatives. Initiatives with work, and Now initiatives that are not on track, come first. The others follow by end date. Press a bar to open the Initiatives tab with that category.
|
|
28
30
|
- **Initiatives** lists the initiatives of the board, 25 on each page. Use the saved views (Now and next, Mine, At risk, No owner, All), the filters and the sort. Press **Move** on a row to move the initiative to a stage of its own workflow.
|
|
29
31
|
- **Triage** lists the new asks. Add an ask, then open it to see its draft. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). On a board, **New initiative** lists the anchors that `planning/okrs/` declares. Pick one from the list. Press **Ask for more** to ask a question, or **Decline** and give a reason.
|
|
@@ -92,7 +92,9 @@ async function handle(req, res, url, ctx) {
|
|
|
92
92
|
const w = wsOf(ctx, url.searchParams.get('ws'));
|
|
93
93
|
if (!w) return send(bad(404, 'no such workspace'));
|
|
94
94
|
const file = ledgerOf(ctx, w);
|
|
95
|
-
|
|
95
|
+
// the same clock the scheduler uses: PMOS_SCAN_NOW (server.js scanClock) fixes it, so a suite's scan is "today" on any date
|
|
96
|
+
const fixed = Date.parse(process.env.PMOS_SCAN_NOW || '');
|
|
97
|
+
const now = Number.isFinite(fixed) ? fixed : Date.now();
|
|
96
98
|
if (sub === 'today') return send({ status: 200, body: { ok: true, ws: w.id, ledger: !!file, scan: todayAnswer(ctx, w, file, now) } });
|
|
97
99
|
const board = text(url.searchParams.get('board')) || w.id;
|
|
98
100
|
if (!S.ID_RE.test(board)) return send(bad(400, 'board must be a board id'));
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.174.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.174.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* pm/c3-quiet.css — quieter console3 pages (c3-quiet-pages, PM-approved 2026-10-12). Linked once by c3-quiet.js.
|
|
2
|
+
* At most two rows sit above a page's content: row one is the page tabs with the date, the freshness dot and the tools on the
|
|
3
|
+
* right; row two is the page's own context (breadcrumb, status, sub-tabs, actions). Teaching sentences, empty cards and extra
|
|
4
|
+
* chips are not drawn; decks show on Briefing only. Everything is scoped under .c3 and uses tokens only. */
|
|
5
|
+
/* Phone widths keep today's layout: the phone gets its own companion design (Wawan, 2026-10-12), so every rule here is for 601px and up. */
|
|
6
|
+
@media (min-width:601px){
|
|
7
|
+
/* 1. Header: one row. Title and date left, a short tool set right. */
|
|
8
|
+
.c3 .c3top{display:flex !important;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:nowrap}
|
|
9
|
+
.c3 .c3top > div:first-child{min-width:0}
|
|
10
|
+
.c3 .c3top .tools{order:2;margin-left:auto;flex-wrap:nowrap}
|
|
11
|
+
.c3 .c3top .fr-fresh{display:none !important} /* the long freshness line */
|
|
12
|
+
.c3 .c3pg-daylight{display:none !important} /* sunrise and sunset text */
|
|
13
|
+
.c3 .c3top [data-c3pg="calendar"]{display:none !important} /* same as the Calendar tab */
|
|
14
|
+
.c3 .c3top .ib:not([aria-pressed="true"]),.c3 .c3top .psess{border-color:transparent !important;background:transparent !important;box-shadow:none !important}
|
|
15
|
+
.c3 .c3pg-tick{display:none !important} /* the pop-up "Just in" toast: the bell keeps it */
|
|
16
|
+
.qt-fresh{margin-left:4px}
|
|
17
|
+
.qt-fresh .qt-fresh__dot{width:8px;height:8px;border-radius:50%;background:var(--warning);display:inline-block}
|
|
18
|
+
.qt-fresh.ok .qt-fresh__dot{background:var(--muted-foreground)}
|
|
19
|
+
.c3 .c3top .fr-fresh.qt-open{display:flex !important;margin-top:6px}
|
|
20
|
+
/* 2. Tab counts: plain grey numbers, not blue badges */
|
|
21
|
+
.c3 .c3tabs .bdg{background:transparent !important;color:var(--muted-foreground) !important;font-weight:400;padding:0 0 0 2px;min-width:0;box-shadow:none}
|
|
22
|
+
/* 3. Teaching sentences and hints: gone from every page */
|
|
23
|
+
.c3 .ini-note:not(:has([data-ibb="clear"],[data-wk="clear"])),.c3 .ini-lhm .dim,.c3 .cal3-note,.c3 [data-pm-chips],.c3 [data-pm-lens-head],
|
|
24
|
+
.c3 [data-c3pg="foot"],.c3 .pulse-foot .c3s2,.c3 .st-lead,.c3 [data-c3pg="kept-out"],.c3 [data-needs-summary]{display:none !important}
|
|
25
|
+
/* 4. Rows: title plus at most two chips; the rest lives in the row's panel */
|
|
26
|
+
.c3 .nrow .nb > .chips > :nth-child(n+3){display:none !important}
|
|
27
|
+
.c3 [data-c3pg="today"] .nrow .chips [data-prep],.c3 [data-c3pg="today"] .nrow .chips [data-before]{display:none !important}
|
|
28
|
+
.c3 [data-c3pg="today"] .nrow .nb > .chips > [data-people]{display:inline !important}
|
|
29
|
+
/* 5. Decks: only Briefing shows the big cards */
|
|
30
|
+
.c3 .cal3 [data-c3pg="plan"],.c3 [data-rp3-deck]{display:none !important}
|
|
31
|
+
.c3 .stance [data-shell="listen"]{display:none !important}
|
|
32
|
+
.c3 .fc .cfh .kp:nth-child(n+2){display:none !important}
|
|
33
|
+
/* 7. Initiatives */
|
|
34
|
+
.c3 [data-ini-view="portfolio"] .ini-meta{display:none !important}
|
|
35
|
+
.c3 [data-ini-view="portfolio"] .bcol.r > section:first-child{display:none !important} /* repeats each board's own bar */
|
|
36
|
+
.c3 [data-ini="boards"] .nrow .nb > .chips > :nth-child(n+3){display:inline-flex !important}
|
|
37
|
+
.c3 [data-ini="boards"] .nrow .nb > .chips:has(.ini-sw){display:none !important} /* Discover 0 Plan 25 ... legend */
|
|
38
|
+
.c3 [data-ini="boards"] .nrow .chips .tag{display:none !important}
|
|
39
|
+
.c3 .bcols.qt-one{display:block !important}
|
|
40
|
+
.c3 .bcols.qt-one > .bcol.r{display:none !important}
|
|
41
|
+
.c3 .bcols.qt-one > .bcol.l{width:auto;max-width:none}
|
|
42
|
+
.c3 [data-ini="setup"]{display:none !important} /* Build and deliver line: lives in Board settings */
|
|
43
|
+
.c3 [data-ini-view="board"] > .calbody > .lh{display:none !important} /* board name repeats the breadcrumb */
|
|
44
|
+
.c3 [data-ini="board-tabs"] .bdg{display:none !important}
|
|
45
|
+
.c3 [data-ini="board-tabs"] .qt-more-tab{display:none}
|
|
46
|
+
.c3 [data-ini="board-tabs"].qt-show .qt-more-tab{display:inline-flex}
|
|
47
|
+
.c3 .qt-status{color:var(--muted-foreground);font-size:var(--fs-2);margin-left:8px}
|
|
48
|
+
.c3 .qt-status.warn{color:var(--warning)}
|
|
49
|
+
.c3 [data-ini="glance"] .chip2.mono{display:none !important}
|
|
50
|
+
.c3 .qt-fold > summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px;color:var(--muted-foreground)}
|
|
51
|
+
.c3 .qt-fold > summary::-webkit-details-marker{display:none}
|
|
52
|
+
.c3 .qt-hide{display:none !important}
|
|
53
|
+
/* 8. Calendar: layers behind one button */
|
|
54
|
+
.c3 [data-c3-pane="cal"] [data-cal3="chips"]{display:none !important}
|
|
55
|
+
.c3 [data-c3-pane="cal"] [data-cal3="chips"].qt-open{display:flex !important}
|
|
56
|
+
.c3 [data-c3-pane="cal"] [data-cal3="lanes"]{display:none !important}
|
|
57
|
+
/* 9. Work */
|
|
58
|
+
.c3 .wkfc-why,.c3 .wkfr .nb > .chips:nth-of-type(2){display:none !important}
|
|
59
|
+
@media (hover: hover){ /* with a pointer, a focus card's steps and buttons show on pointing at or tabbing into it; a touch screen always shows them */
|
|
60
|
+
.c3 .wkfr:not(:hover):not(:focus-within) .wkfr-b{display:none !important}
|
|
61
|
+
}
|
|
62
|
+
.c3 .wkfr .nb > .chips:has(> .chip2){display:none !important}
|
|
63
|
+
.c3 [data-pm-lenses] .bdg{background:transparent !important;color:var(--muted-foreground) !important}
|
|
64
|
+
/* 10. Reports */
|
|
65
|
+
.c3 .rp3-w > .chips{display:none !important}
|
|
66
|
+
@media (hover: hover){ /* with a pointer, About this report shows on pointing at or tabbing into the report */
|
|
67
|
+
.c3 .rp3-w:not(:hover):not(:focus-within) .rp3-about{opacity:0}
|
|
68
|
+
}
|
|
69
|
+
/* 11. Settings */
|
|
70
|
+
.c3 .st-nav .sh2 .bdg{display:none !important}
|
|
71
|
+
|
|
72
|
+
/* Phone: title and tools on one row, tools icon-only, date line below */
|
|
73
|
+
/* ===== v2: two rows above content ===== */
|
|
74
|
+
/* Row 1: the page tabs carry the page name; date, freshness dot and tools sit on the same row */
|
|
75
|
+
.c3 .c3main{position:relative}
|
|
76
|
+
.c3 .c3top h1{display:none !important}
|
|
77
|
+
.c3 .c3top{position:absolute;top:0;right:0;left:auto;align-items:center !important;gap:10px;z-index:3;padding:0 !important;margin:0 !important;min-height:44px;background:transparent}
|
|
78
|
+
.c3 .c3top > div:first-child{display:flex;align-items:center;gap:8px;order:1}
|
|
79
|
+
.c3 .c3top .sub{font-size:var(--fs-1);margin:0}
|
|
80
|
+
.c3 .c3top .fr-fresh.qt-open{position:absolute;top:44px;right:0;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:8px 10px;flex-wrap:wrap;max-width:560px;box-shadow:0 6px 24px rgb(0 0 0 / .08)}
|
|
81
|
+
.c3 nav.c3tabs{margin-top:0 !important;min-height:44px}
|
|
82
|
+
/* Row 2: one context bar per page */
|
|
83
|
+
.c3 .qt-ctx{display:flex !important;align-items:center;gap:10px;flex-wrap:wrap}
|
|
84
|
+
.c3 .qt-ctx [data-ini="board-tabs"]{margin-left:auto}
|
|
85
|
+
.c3 [data-ini-view="board"] > .calbody > .calbar:has([data-ini="board-tabs"]){display:none !important}
|
|
86
|
+
.c3 [data-ini-view="portfolio"] .calbody > .lh{display:none !important}
|
|
87
|
+
.c3 .stance[data-bf="lens-bar"]{display:none !important}
|
|
88
|
+
.c3 .rp3-head:not(:has(.rp3-pick)){display:none !important} /* Portfolio: the tab row says it all */
|
|
89
|
+
.c3 .rp3-head > .ki,.c3 .rp3-head [data-rp3-title],.c3 .rp3-head .rp3-min0 > .dim{display:none !important} /* a board or an initiative: only its chooser */
|
|
90
|
+
.c3 .c3add{display:flex;align-items:center;gap:10px}
|
|
91
|
+
.c3 .c3add .form{flex:1 1 auto}
|
|
92
|
+
.c3 .wk > .calbar.qt-moved{display:none !important}
|
|
93
|
+
.c3 .qt-ctx{justify-content:flex-start !important;flex-direction:row !important}
|
|
94
|
+
.c3 .qt-ctx > [data-ini="crumbs"]{order:1}
|
|
95
|
+
.c3 .qt-ctx > [data-ini="board-tabs"]{order:2;margin-left:12px !important}
|
|
96
|
+
.c3 .qt-ctx > .chips2:not([data-ini="crumbs"]){order:3;margin-left:auto}
|
|
97
|
+
.c3 [data-ini-view="board"] > .calbody{padding-top:4px}
|
|
98
|
+
.c3.qt-stack .c3top{position:static;justify-content:flex-end !important;min-height:0}
|
|
99
|
+
.c3.qt-stack nav.c3tabs{padding-right:0}
|
|
100
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* pm/c3-quiet.js — quieter console3 pages (c3-quiet-pages, PM-approved 2026-10-12: the mockup's
|
|
2
|
+
* version 2, linked from the initiative record).
|
|
3
|
+
*
|
|
4
|
+
* mountQuiet(root) links c3-quiet.css once and keeps the page in its quiet layout as the views draw:
|
|
5
|
+
* - the freshness line folds into one dot next to the date ("4 sources not read"); pressing the dot shows the full line;
|
|
6
|
+
* - a board: its status joins the breadcrumb, the board tabs join that row, Backlog, Releases, Docs and Sprints go
|
|
7
|
+
* behind More, Dashboard and the all-zero Work items card are not shown;
|
|
8
|
+
* - the portfolio: the headline joins the breadcrumb, an empty triage card is not shown, the timeline folds;
|
|
9
|
+
* - empty chips ("No owner", "0 epics", ...) are not drawn;
|
|
10
|
+
* - Calendar: the layer chips sit behind one Layers button, the Detail card shows once a meeting is picked;
|
|
11
|
+
* - Work: the lenses sit beside Add work; Reports: Share and Edit dashboard join the tab row.
|
|
12
|
+
* Desktop only: below 601px the page keeps today's layout.
|
|
13
|
+
* The views draw their own markup; this module only moves and hides it, so every press still goes to the view's own handler.
|
|
14
|
+
* It runs again, debounced, whenever the views redraw (one MutationObserver on the root). */
|
|
15
|
+
|
|
16
|
+
import { button } from './primitives.js';
|
|
17
|
+
|
|
18
|
+
const EMPTY_CHIP = /^(No owner|No dates yet|0 epics|0%|No release planned|Build and deliver)$/;
|
|
19
|
+
const MORE_TABS = /^(backlog|releases|docs|sprints)$/;
|
|
20
|
+
let layersOpen = false; // the Calendar's Layers button, for this page load
|
|
21
|
+
const BAD_FRESH = '.fr-fresh__state--never,.fr-fresh__state--stale,.fr-fresh__state--failed';
|
|
22
|
+
|
|
23
|
+
/** Link c3-quiet.css once, from this module's own folder (the way c3-scan.js links its stylesheet). */
|
|
24
|
+
export function ensureQuietCss() {
|
|
25
|
+
if (typeof document === 'undefined' || document.querySelector('link[data-c3-quiet]')) return;
|
|
26
|
+
const l = document.createElement('link');
|
|
27
|
+
l.rel = 'stylesheet'; l.href = new URL('./c3-quiet.css', import.meta.url).href; l.setAttribute('data-c3-quiet', '');
|
|
28
|
+
document.head.appendChild(l);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const mk = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
32
|
+
const warnWords = (t) => /attention|off track|at risk|late/i.test(t);
|
|
33
|
+
|
|
34
|
+
/* Header: the freshness dot. The Sessions button stays: it is the one way to the running sessions. */
|
|
35
|
+
function header(root) {
|
|
36
|
+
const top = root.querySelector('.c3top');
|
|
37
|
+
if (!top) return;
|
|
38
|
+
const fr = top.querySelector('.fr-fresh');
|
|
39
|
+
const date = top.querySelector('[data-c3pg="date"]');
|
|
40
|
+
if (fr && date) {
|
|
41
|
+
let dot = top.querySelector('.qt-fresh');
|
|
42
|
+
if (!dot) {
|
|
43
|
+
const press = () => { const on = fr.classList.toggle('qt-open'); dot.setAttribute('aria-expanded', String(on)); };
|
|
44
|
+
dot = button({ label: '', variant: 'ghost', size: 'sm', onClick: press, attrs: { class: 'qt-fresh', 'data-c3pg': 'fresh-dot', 'aria-expanded': 'false' } });
|
|
45
|
+
dot.prepend(mk('span', 'qt-fresh__dot'));
|
|
46
|
+
date.after(dot);
|
|
47
|
+
}
|
|
48
|
+
const bad = fr.querySelectorAll(BAD_FRESH).length;
|
|
49
|
+
const words = bad ? bad + (bad === 1 ? ' source' : ' sources') + ' not read' : 'Sources read';
|
|
50
|
+
dot.classList.toggle('ok', !bad);
|
|
51
|
+
if (dot.title !== words) { dot.title = words; dot.setAttribute('aria-label', words + '. Show when each source was read'); }
|
|
52
|
+
}
|
|
53
|
+
// The tools sit on the tab row when both fit; when they do not (a narrow window, the demo banner), they take their own row.
|
|
54
|
+
const nav = root.querySelector('nav.c3tabs');
|
|
55
|
+
const w = Math.ceil(top.getBoundingClientRect().width);
|
|
56
|
+
if (nav && w) {
|
|
57
|
+
const tabs = [...nav.querySelectorAll('[role="tab"]')];
|
|
58
|
+
const need = tabs.length ? tabs[tabs.length - 1].getBoundingClientRect().right - nav.getBoundingClientRect().left : 0;
|
|
59
|
+
root.classList.toggle('qt-stack', need + w + 32 > (nav.parentElement || nav).getBoundingClientRect().width);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* A board: one context row, five tabs and More, a gear for Board settings. */
|
|
64
|
+
function board(v) {
|
|
65
|
+
const crumbs = v.querySelector('[data-ini="crumbs"]');
|
|
66
|
+
const st = v.querySelector('.ini-lhm .dim');
|
|
67
|
+
if (crumbs && st) {
|
|
68
|
+
let s = crumbs.querySelector('.qt-status');
|
|
69
|
+
const t = st.textContent.split('.')[0];
|
|
70
|
+
if (!s) { s = mk('span', 'qt-status'); crumbs.appendChild(s); }
|
|
71
|
+
if (s.textContent !== t) { s.textContent = t; s.classList.toggle('warn', warnWords(t)); }
|
|
72
|
+
}
|
|
73
|
+
// Dashboard lives in Reports, which already holds the board dashboard.
|
|
74
|
+
for (const b of v.querySelectorAll(':scope > .calbar button')) if (b.textContent.trim() === 'Dashboard') b.classList.add('qt-hide');
|
|
75
|
+
const tabs = v.querySelector('[data-ini="board-tabs"]');
|
|
76
|
+
if (tabs) {
|
|
77
|
+
for (const t of tabs.querySelectorAll('[data-ini-tab]')) if (MORE_TABS.test(t.dataset.iniTab)) t.classList.add('qt-more-tab');
|
|
78
|
+
if (!tabs.querySelector('.qt-more')) {
|
|
79
|
+
const m = button({ label: 'More', variant: 'ghost', size: 'sm', attrs: { class: 'qt-more', 'aria-expanded': 'false' },
|
|
80
|
+
onClick: () => { const on = tabs.classList.toggle('qt-show'); m.setAttribute('aria-expanded', String(on)); } });
|
|
81
|
+
tabs.appendChild(m);
|
|
82
|
+
}
|
|
83
|
+
// a tab behind More that is the open one keeps the row shown, so the pressed tab never disappears
|
|
84
|
+
if (tabs.querySelector('.qt-more-tab[aria-pressed="true"]')) tabs.classList.add('qt-show');
|
|
85
|
+
const row = crumbs && crumbs.parentElement;
|
|
86
|
+
if (row && tabs.parentElement !== row) { row.classList.add('qt-ctx'); row.insertBefore(tabs, row.querySelector(':scope > .chips2:not([data-ini="crumbs"])')); }
|
|
87
|
+
}
|
|
88
|
+
for (const s of v.querySelectorAll('.bcol.r > section')) {
|
|
89
|
+
if (/Work items/i.test(s.textContent) && (s.textContent.match(/\b\d+\b/g) || []).every((x) => x === '0')) s.classList.add('qt-hide');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* The portfolio: the headline beside the breadcrumb, no empty triage card, the timeline folded. */
|
|
94
|
+
function portfolio(v) {
|
|
95
|
+
const c = v.querySelector('[data-ini="crumbs"]'), h = v.querySelector('.ini-lhm b');
|
|
96
|
+
if (c && h) {
|
|
97
|
+
let s = c.querySelector('.qt-status');
|
|
98
|
+
if (!s) { s = mk('span', 'qt-status'); c.appendChild(s); }
|
|
99
|
+
if (s.textContent !== h.textContent) { s.textContent = h.textContent; s.classList.toggle('warn', warnWords(h.textContent)); }
|
|
100
|
+
}
|
|
101
|
+
for (const s of v.querySelectorAll('.bcol.r section')) s.classList.toggle('qt-hide', /No new asks/.test(s.textContent));
|
|
102
|
+
const tl = [...v.querySelectorAll('section.card.sect')].find((s) => /Timeline across boards/.test((s.querySelector('h2') || {}).textContent || ''));
|
|
103
|
+
if (tl && !tl.closest('.qt-fold')) {
|
|
104
|
+
const d = mk('details', 'qt-fold card'); d.setAttribute('data-c3pg', 'pf-timeline-fold');
|
|
105
|
+
d.appendChild(mk('summary', null, 'Timeline across boards'));
|
|
106
|
+
tl.before(d); d.appendChild(tl);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Phone widths keep today's layout (the phone gets its own companion design), so nothing moves below 601px. */
|
|
111
|
+
const wide = () => typeof matchMedia !== 'function' || matchMedia('(min-width: 601px)').matches;
|
|
112
|
+
|
|
113
|
+
function apply(root) {
|
|
114
|
+
if (!wide()) return;
|
|
115
|
+
header(root);
|
|
116
|
+
for (const v of root.querySelectorAll('[data-ini-view="board"]')) board(v);
|
|
117
|
+
for (const v of root.querySelectorAll('[data-ini-view="portfolio"]')) portfolio(v);
|
|
118
|
+
// Two columns become one when the right column has nothing left to show.
|
|
119
|
+
for (const c of root.querySelectorAll('[data-ini-view] .bcols')) {
|
|
120
|
+
const r = c.querySelector(':scope > .bcol.r');
|
|
121
|
+
if (r) c.classList.toggle('qt-one', ![...r.children].some((x) => getComputedStyle(x).display !== 'none'));
|
|
122
|
+
}
|
|
123
|
+
for (const c of root.querySelectorAll('.nrow .chips > .chip2')) if (EMPTY_CHIP.test(c.textContent.trim())) c.classList.add('qt-hide');
|
|
124
|
+
for (const c of root.querySelectorAll('.chips2, .chips')) if (/^Open the Board\s*Open the Timeline$/.test(c.textContent.trim())) c.classList.add('qt-hide');
|
|
125
|
+
// Settings: the "This computer" tag under each field and the Welcome note are not drawn.
|
|
126
|
+
for (const t of root.querySelectorAll('.c3set .tag')) if (t.textContent.trim() === 'This computer') t.classList.add('qt-hide');
|
|
127
|
+
for (const f of root.querySelectorAll('.c3set .fold')) if (/^The Welcome page asks/.test(f.textContent.trim())) f.classList.add('qt-hide');
|
|
128
|
+
// Calendar: one Layers button for the layer chips. Open or folded holds across the calendar's redraws (a chip press redraws it).
|
|
129
|
+
for (const c of root.querySelectorAll('[data-c3-pane="cal"] [data-cal3="page"]')) {
|
|
130
|
+
const lens = c.querySelector(':scope > .calbar .lens'), chips = c.querySelector('[data-cal3="chips"]');
|
|
131
|
+
if (!lens || !chips) continue;
|
|
132
|
+
let b = lens.querySelector('.qt-layers');
|
|
133
|
+
if (!b) {
|
|
134
|
+
b = button({ label: 'Layers', variant: 'ghost', size: 'sm', attrs: { class: 'qt-layers', 'data-c3pg': 'cal-layers' }, onClick: () => { layersOpen = !layersOpen; apply(root); } });
|
|
135
|
+
lens.appendChild(b);
|
|
136
|
+
}
|
|
137
|
+
b.setAttribute('aria-expanded', String(layersOpen));
|
|
138
|
+
chips.classList.toggle('qt-open', layersOpen);
|
|
139
|
+
}
|
|
140
|
+
// Calendar: the Detail card shows once a meeting or a marker is picked (not its "Select a meeting" note).
|
|
141
|
+
for (const d of root.querySelectorAll('[data-cal3="detail"]')) d.classList.toggle('qt-hide', /^Select a meeting to see/.test((d.querySelector('.cal3-note') || {}).textContent || '') && !d.querySelector('button, a'));
|
|
142
|
+
// Work: the lenses beside Add work.
|
|
143
|
+
for (const w of root.querySelectorAll('.wk')) {
|
|
144
|
+
const add = w.querySelector(':scope > .c3add'), bar = w.querySelector(':scope > .calbar'), l = bar && bar.querySelector('[data-pm-lenses]');
|
|
145
|
+
if (add && l && l.parentElement === bar) { for (const o of add.querySelectorAll('[data-pm-lenses]')) o.remove(); add.appendChild(l); bar.classList.add('qt-moved'); }
|
|
146
|
+
}
|
|
147
|
+
// Reports: Share and Edit dashboard on the tab row.
|
|
148
|
+
for (const r of root.querySelectorAll('[data-rp3]')) {
|
|
149
|
+
const scope = r.querySelector(':scope > .calbar .rp3-scope'), acts = r.querySelector('.rp3-head > .chips2');
|
|
150
|
+
if (scope && acts) { for (const o of scope.querySelectorAll('.qt-acts')) o.remove(); acts.classList.add('qt-acts'); scope.appendChild(acts); }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Keep `root` (the console3 root) in the quiet layout. Safe to call once; later calls do nothing. */
|
|
155
|
+
export function mountQuiet(root) {
|
|
156
|
+
if (!root || root.dataset.quiet) return;
|
|
157
|
+
root.dataset.quiet = '1';
|
|
158
|
+
ensureQuietCss();
|
|
159
|
+
let t = null;
|
|
160
|
+
const run = () => { t = null; try { apply(root); } catch { /* a view mid-draw; the next mutation runs it again */ } };
|
|
161
|
+
run();
|
|
162
|
+
new MutationObserver(() => { if (!t) t = setTimeout(run, 50); }).observe(root, { subtree: true, childList: true });
|
|
163
|
+
if (typeof matchMedia === 'function') matchMedia('(min-width: 601px)').addEventListener('change', () => { if (!t) t = setTimeout(run, 50); });
|
|
164
|
+
}
|
|
@@ -49,6 +49,7 @@ import { registry, filterPalette, rowStep } from './briefing-model.js';
|
|
|
49
49
|
import { freshnessLine } from './freshness-line.js';
|
|
50
50
|
import { mountInitiatives } from './c3-initiatives.js';
|
|
51
51
|
import { mountSettings, gearIcon, applyLook } from './c3-settings.js';
|
|
52
|
+
import { mountQuiet } from './c3-quiet.js';
|
|
52
53
|
|
|
53
54
|
/* In demo mode each tab's deck asks for its own cards (the mockup draws a different deck per tab); real mode sends no scope. Reports always sends its own (repTab). */
|
|
54
55
|
const sc = (name) => (DEMO ? name : undefined);
|
|
@@ -63,6 +64,9 @@ const THEME_KEY = 'pmos.c3.theme';
|
|
|
63
64
|
const q = new URLSearchParams(location.search);
|
|
64
65
|
const store = { get(k) { try { return localStorage.getItem(k); } catch { return null; } }, set(k, v) { try { localStorage.setItem(k, v); } catch { /* no storage */ } } };
|
|
65
66
|
const DEMO = q.get('demo') === '1'; // demo mode: c3-demo.js answers the page's /api reads with the mockup's sample content
|
|
67
|
+
/* The dock's open or folded choice, kept on this device. No choice yet: folded. */
|
|
68
|
+
const dockPref = () => store.get('pmos.c3.dock') === 'open';
|
|
69
|
+
const keepDock = () => store.set('pmos.c3.dock', S.dockOpen ? 'open' : 'folded');
|
|
66
70
|
const S = { demo: DEMO, ws: q.get('ws') || '', workspaces: [], tab: 'brief', theme: 'light', panes: {}, mounted: {}, init: 'lifecycle', initRows: null, dockOpen: true, wide: true };
|
|
67
71
|
|
|
68
72
|
/* A view reads a route with no ws and the server answers for its home workspace. This page names one, so a GET to /api/ with no ws gets it. */
|
|
@@ -136,7 +140,7 @@ function toggleMore() {
|
|
|
136
140
|
item('phone', 'Phone preview', () => togglePreview(), !!S.preview, { 'data-a': 'phone' }),
|
|
137
141
|
item(S.theme === 'dark' ? 'sun' : 'moon', S.theme === 'dark' ? 'Light theme' : 'Dark theme', () => setTheme(S.theme === 'dark' ? 'light' : 'dark')),
|
|
138
142
|
((b) => { b.firstChild.replaceWith(gearIcon('sm')); return b; })(item('info', 'Settings', () => showTab('settings', true), false, { 'data-a': 'settings' })),
|
|
139
|
-
item('term', S.dockOpen ? 'Fold the dock' : 'Open the dock', () => { S.dockOpen = !S.dockOpen; syncDock(); }, false, { 'data-a': 'dockt' }, 'Ctrl J'),
|
|
143
|
+
item('term', S.dockOpen ? 'Fold the dock' : 'Open the dock', () => { S.dockOpen = !S.dockOpen; keepDock(); syncDock(); }, false, { 'data-a': 'dockt' }, 'Ctrl J'),
|
|
140
144
|
el('a', { class: 'c3pg-mi', role: 'menuitem', href: '/classic.html' }, c3ic('term', 'sm'), el('span', null, 'Open the classic app')));
|
|
141
145
|
S.root.append(S.menu); // on the page root with position:fixed, so no ancestor's overflow, transform or stacking order can hide it
|
|
142
146
|
S.placeMenu = () => { const r = S.moreBtn.getBoundingClientRect(); const w = S.menu.offsetWidth || 220; S.menu.style.top = Math.round(r.bottom + 6) + 'px'; S.menu.style.left = Math.round(Math.max(8, Math.min(r.right - w, innerWidth - w - 8))) + 'px'; S.menu.style.maxHeight = Math.max(160, innerHeight - r.bottom - 14) + 'px'; };
|
|
@@ -389,7 +393,7 @@ const sheetOpen = () => !!document.querySelector('[data-shell="sheet"],[data-she
|
|
|
389
393
|
function onKey(e) {
|
|
390
394
|
const ae = document.activeElement;
|
|
391
395
|
if ((e.metaKey || e.ctrlKey) && !e.altKey) {
|
|
392
|
-
if (e.key === 'j' || e.key === 'J') { e.preventDefault(); e.stopPropagation(); if (!e.repeat) { S.dockOpen = !S.dockOpen; syncDock(); } } // stopPropagation: the dock has its own Ctrl J handler on the bubble phase, and two toggles cancel out
|
|
396
|
+
if (e.key === 'j' || e.key === 'J') { e.preventDefault(); e.stopPropagation(); if (!e.repeat) { S.dockOpen = !S.dockOpen; keepDock(); syncDock(); } } // stopPropagation: the dock has its own Ctrl J handler on the bubble phase, and two toggles cancel out
|
|
393
397
|
else if (e.key === 'k' || e.key === 'K') { e.preventDefault(); if (S.pal) closePalette(true); else openPalette(); }
|
|
394
398
|
return;
|
|
395
399
|
}
|
|
@@ -419,7 +423,7 @@ function onKey(e) {
|
|
|
419
423
|
/* ── the dock: the mockup's repository, branch and session tree (c3-dock.js) ── */
|
|
420
424
|
function dock() {
|
|
421
425
|
S.dock = createDock({
|
|
422
|
-
setOpen: (on) => { S.dockOpen = !!on; syncDock(); },
|
|
426
|
+
setOpen: (on) => { S.dockOpen = !!on; keepDock(); syncDock(); },
|
|
423
427
|
onWs: (id) => { const u = new URL(location.href); u.searchParams.set('ws', id); location.assign(u.toString()); },
|
|
424
428
|
/* the needs-you badge on the agent button: runs that ask, hold a permission or need the terminal (not the running ones) */
|
|
425
429
|
onCount: (live, need) => { S.termBdg.textContent = String(need); S.termBdg.hidden = need === 0; import('./c3-terminal-icons.js').then((m) => { const t = m.TIPS.agentHeader(need); S.termBtn.title = t; S.termBtn.setAttribute('aria-label', t); }); }, // the badge counts runs that need you, never running ones
|
|
@@ -544,7 +548,7 @@ function showTab(id, push) {
|
|
|
544
548
|
else S.settings.refresh();
|
|
545
549
|
} else if (!S.mounted[id]) { S.mounted[id] = true; BUILD[id](S.panes[id]); }
|
|
546
550
|
S.root.dataset.tab = id;
|
|
547
|
-
S.dockOpen = S.wide && id === 'brief'; syncDock();
|
|
551
|
+
S.dockOpen = S.wide && id === 'brief' && dockPref(); syncDock(); // the dock starts folded (c3-quiet-pages); Ctrl J or the More menu opens it, and the choice is kept
|
|
548
552
|
S.pageView.classList.remove('view'); void S.pageView.offsetWidth; S.pageView.classList.add('view');
|
|
549
553
|
moveInd();
|
|
550
554
|
if (push) S.userNav = true;
|
|
@@ -602,6 +606,7 @@ async function boot() {
|
|
|
602
606
|
root.replaceChildren(S.grid);
|
|
603
607
|
mountDaypart(root, root.querySelector('.c3top'));
|
|
604
608
|
mountAmbient(root); // the ambient motion canvas, once, behind the grid
|
|
609
|
+
mountQuiet(root); // the quiet layout: two rows above the content, no teaching sentences (c3-quiet-pages)
|
|
605
610
|
|
|
606
611
|
S.dock.setWorkspaces(S.workspaces, S.ws);
|
|
607
612
|
mountBell(S.bellHost, { go: (_, t) => goTarget(t) });
|
|
@@ -611,7 +616,7 @@ async function boot() {
|
|
|
611
616
|
mountTicker(S.tickHost, { onOpen: () => openBellList() });
|
|
612
617
|
import('./shell-recording.js').then((m) => m.mountRecording(S.tools)).catch(() => { /* the recorder is not available on this page */ });
|
|
613
618
|
S.narrow = matchMedia('(max-width:980px)');
|
|
614
|
-
S.narrow.addEventListener('change', () => { S.wide = !S.narrow.matches; S.dockOpen = S.wide && S.tab === 'brief'; syncDock(); });
|
|
619
|
+
S.narrow.addEventListener('change', () => { S.wide = !S.narrow.matches; S.dockOpen = S.wide && S.tab === 'brief' && dockPref(); syncDock(); });
|
|
615
620
|
S.wide = !S.narrow.matches;
|
|
616
621
|
/* The saved settings set the start page (look.start) when the address names no tab, and turn the time-of-day tint and strip off or on. */
|
|
617
622
|
let start = q.get('tab');
|
|
@@ -144,7 +144,8 @@
|
|
|
144
144
|
"c3-driver-test.js",
|
|
145
145
|
"c3-scan-test.js",
|
|
146
146
|
"c3-sessions-panel-test.js",
|
|
147
|
-
"c3-updates-test.js"
|
|
147
|
+
"c3-updates-test.js",
|
|
148
|
+
"c3-quiet-test.js"
|
|
148
149
|
],
|
|
149
150
|
"ui": [
|
|
150
151
|
"ui-test.js"
|
|
@@ -285,7 +286,8 @@
|
|
|
285
286
|
"settings-store.test.js": 1,
|
|
286
287
|
"c3-settings-test.js": 25,
|
|
287
288
|
"c3-sessions-panel-test.js": 20,
|
|
288
|
-
"c3-updates-test.js": 20
|
|
289
|
+
"c3-updates-test.js": 20,
|
|
290
|
+
"c3-quiet-test.js": 30
|
|
289
291
|
},
|
|
290
292
|
"_comment_seconds": "Measured wall time of each suite run alone (4-CPU sandbox, 2026-10-05). Only the ORDER uses it: the slowest suites start first, and --shard deals suites to the least-loaded shard so the shards finish together. A wrong or missing number never skips a suite; a missing one counts as 10."
|
|
291
293
|
}
|