create-pmos 0.124.0 → 0.125.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/scripts/nine-stages.js +81 -0
- package/payload/scripts/workflow-state.js +2 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-one-home-test.js +131 -0
- package/workspace/fixtures/initiatives-api/list.json +171 -3
- package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
- package/workspace/fixtures/initiatives-api/sheet.json +57 -1
- package/workspace/fixtures/one-home-baseline.json +83 -0
- package/workspace/guide/layout.md +2 -0
- package/workspace/lib/engine/nine-stages.js +81 -0
- package/workspace/lib/engine/workflow-state.js +2 -0
- package/workspace/lib/facts-async.js +51 -0
- package/workspace/lib/item-steps.js +52 -0
- package/workspace/lib/jira-status.js +8 -3
- package/workspace/lib/jobs-to-be-done.js +52 -0
- package/workspace/lib/outbox-store.js +87 -0
- package/workspace/lib/pm-items.js +115 -0
- package/workspace/lib/route-modules.js +21 -0
- package/workspace/lib/routes-briefing.js +100 -0
- package/workspace/lib/routes-c3-init.js +53 -0
- package/workspace/lib/routes-c3-life.js +119 -0
- package/workspace/lib/routes-c3-strategy.js +169 -0
- package/workspace/lib/routes-c3-timeline.js +191 -0
- package/workspace/lib/routes-calendar-import.js +60 -0
- package/workspace/lib/routes-calendar-views.js +61 -0
- package/workspace/lib/routes-doc-health.js +79 -0
- package/workspace/lib/routes-doc-sheet.js +169 -0
- package/workspace/lib/routes-forecast.js +85 -0
- package/workspace/lib/routes-hooks.js +121 -0
- package/workspace/lib/routes-initiatives.js +3 -0
- package/workspace/lib/routes-intake.js +24 -0
- package/workspace/lib/routes-outbox.js +97 -0
- package/workspace/lib/routes-pm-items.js +216 -0
- package/workspace/lib/routes-records.js +61 -0
- package/workspace/lib/routes-releases.js +129 -0
- package/workspace/lib/routes-report-facts.js +84 -0
- package/workspace/lib/routes-report-families.js +32 -0
- package/workspace/lib/routes-roadmap-ops.js +141 -0
- package/workspace/lib/routes-runs.js +119 -0
- package/workspace/lib/routes-signals.js +50 -0
- package/workspace/lib/run-answers.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +26 -0
- package/workspace/public/pm/briefing-deck.js +415 -0
- package/workspace/public/pm/briefing-model.js +176 -0
- package/workspace/public/pm/c3-breakdown.js +240 -0
- package/workspace/public/pm/c3-brief.js +278 -0
- package/workspace/public/pm/c3-cal-strategy.js +528 -0
- package/workspace/public/pm/c3-case.js +173 -0
- package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
- package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
- package/workspace/public/pm/c3-demo-cal2.js +66 -0
- package/workspace/public/pm/c3-demo-cal3.js +173 -0
- package/workspace/public/pm/c3-demo-data.js +560 -0
- package/workspace/public/pm/c3-demo-init.js +373 -0
- package/workspace/public/pm/c3-demo-life.js +77 -0
- package/workspace/public/pm/c3-demo-misc2.js +46 -0
- package/workspace/public/pm/c3-demo-plan2.js +30 -0
- package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
- package/workspace/public/pm/c3-demo-rep2.js +92 -0
- package/workspace/public/pm/c3-demo-term.js +153 -0
- package/workspace/public/pm/c3-demo-time.js +49 -0
- package/workspace/public/pm/c3-demo-work-ix.js +80 -0
- package/workspace/public/pm/c3-demo-work2.js +13 -0
- package/workspace/public/pm/c3-demo.js +113 -0
- package/workspace/public/pm/c3-doc-sheet.js +314 -0
- package/workspace/public/pm/c3-dock.js +180 -0
- package/workspace/public/pm/c3-docs.js +290 -0
- package/workspace/public/pm/c3-expand.js +96 -0
- package/workspace/public/pm/c3-init-extras.js +170 -0
- package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
- package/workspace/public/pm/c3-misc-panels.js +89 -0
- package/workspace/public/pm/c3-plan-panels.js +163 -0
- package/workspace/public/pm/c3-plan.js +192 -0
- package/workspace/public/pm/c3-roadmap.js +249 -0
- package/workspace/public/pm/c3-sheet.js +107 -0
- package/workspace/public/pm/c3-shell.js +637 -0
- package/workspace/public/pm/c3-signals.js +196 -0
- package/workspace/public/pm/c3-strategy-panels.js +45 -0
- package/workspace/public/pm/c3-swipe.js +138 -0
- package/workspace/public/pm/c3-terminal-data.js +26 -0
- package/workspace/public/pm/c3-terminal-edge.js +91 -0
- package/workspace/public/pm/c3-terminal-icons.js +41 -0
- package/workspace/public/pm/c3-terminal-results.js +141 -0
- package/workspace/public/pm/c3-terminal.js +52 -0
- package/workspace/public/pm/c3-tickets.js +163 -0
- package/workspace/public/pm/c3-timeline.js +201 -0
- package/workspace/public/pm/c3-toast.js +16 -0
- package/workspace/public/pm/calendar-views.js +1015 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1480 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- package/workspace/public/pm/dest-reports.js +47 -1
- package/workspace/public/pm/dest-today.js +55 -1
- package/workspace/public/pm/doc-health-panel.js +110 -0
- package/workspace/public/pm/doc-rail.js +9 -3
- package/workspace/public/pm/lifecycle-lens-model.js +228 -0
- package/workspace/public/pm/lifecycle-lens.js +261 -0
- package/workspace/public/pm/new-initiative-sheet.js +116 -21
- package/workspace/public/pm/outbox-panel.js +125 -0
- package/workspace/public/pm/pm-work-view.js +919 -0
- package/workspace/public/pm/releases-view.js +248 -0
- package/workspace/public/pm/report-families-view.js +503 -0
- package/workspace/public/pm/run-card.js +244 -0
- package/workspace/public/pm/sessions-panel.js +149 -0
- package/workspace/public/pm/shell-bell.js +162 -0
- package/workspace/public/pm/shell-model.js +83 -0
- package/workspace/public/pm/shell-phone.js +124 -0
- package/workspace/public/pm/shell-readaloud.js +53 -0
- package/workspace/public/pm/shell-recording.js +110 -0
- package/workspace/public/pm/shell-ticker.js +90 -0
- package/workspace/public/pm/slide-confirm.js +163 -0
- package/workspace/public/pm/timeline-forecast-view.js +164 -0
- package/workspace/public/pm/today-day.js +8 -2
- package/workspace/public/pm/tokens.css +1 -0
- package/workspace/server.js +5 -2
- package/workspace/test-files.json +8 -0
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
/* pm/c3-shell.js — the console3 SHELL as its own page (/c3.html). The mockup (console3.html) is the UI: the header (title and date,
|
|
2
|
+
* the icon buttons, the bell), the tab bar (Briefing, Calendar, Work, Initiatives, Reports) and the three-column Briefing. This
|
|
3
|
+
* file draws only the frame and composes the real views into it, each through its own mount function:
|
|
4
|
+
* Briefing left mountBriefing (day-part bar and deck) + a "Needs you" list of the same cards as rows
|
|
5
|
+
* middle mountCalendarViews (day) + a portfolio pulse drawn from the Calendar's own gantt rows
|
|
6
|
+
* right the dock (c3-dock.js): the repository, branch and session tree, the selected session's detail, "Open the session" to /
|
|
7
|
+
* Every tab but Briefing also carries the mockup's "Plan: Decide what gates the day" lens row and deck (c3-plan.js planNode).
|
|
8
|
+
* Calendar mountCalendarViews Work mountPmWork
|
|
9
|
+
* Initiatives a switch among mountLifecycle, mountForecast and mountReleases Reports mountReportFamilies, mountOutbox
|
|
10
|
+
* The bell, the ticker, the banner and the recording group are the shell-*.js modules. The old app (/) is not touched.
|
|
11
|
+
* Real data is mostly empty: every view draws its own honest empty state, and nothing here is invented.
|
|
12
|
+
* The workspace id is ?ws= (default: the first of GET /api/workspaces). The views read some routes without a ws, so a GET to /api/
|
|
13
|
+
* that names none gets the page's ws added (see withWs). No dialog is used. Presses are the views' own. */
|
|
14
|
+
import { el } from './dom.js';
|
|
15
|
+
import { pmGet } from './http.js';
|
|
16
|
+
import { ensureC3Css, c3ic, c3Phone, mountPmWork } from './pm-work-view.js';
|
|
17
|
+
import { mountBriefing, showCard, flipBack } from './briefing-deck.js';
|
|
18
|
+
import { mountCalendarViews } from './calendar-views.js';
|
|
19
|
+
import { mountLifecycle } from './lifecycle-lens.js';
|
|
20
|
+
import { mountForecast } from './timeline-forecast-view.js';
|
|
21
|
+
import { mountReleases } from './releases-view.js';
|
|
22
|
+
import { mountReportFamilies } from './report-families-view.js';
|
|
23
|
+
import { mountOutbox } from './outbox-panel.js';
|
|
24
|
+
import { createDock } from './c3-dock.js';
|
|
25
|
+
import { belowLine, reviewSection, handledRow, trustCard, footLine, needRow, rowApi, BX, toast, setNeedsTitle, hooks as briefHooks } from './c3-brief.js';
|
|
26
|
+
import { stopCard } from './c3-misc-panels.js';
|
|
27
|
+
import { openSheet } from './c3-sheet.js';
|
|
28
|
+
import { planNode, setPlanScope, dueCard, crossLinks, newInitLine, portfolioCard, readQuarter, mockPulse } from './c3-plan.js';
|
|
29
|
+
import { initiativesViewCtx } from './registry.js';
|
|
30
|
+
import { mount as mountRoadmap } from './c3-roadmap.js';
|
|
31
|
+
import { mount as mountBreakdown } from './c3-breakdown.js';
|
|
32
|
+
import { ic } from './c3-terminal-icons.js';
|
|
33
|
+
import { mount as mountTickets, badge as ticketsBadge } from './c3-tickets.js';
|
|
34
|
+
import { mount as mountDocs, badge as docsBadge } from './c3-docs.js';
|
|
35
|
+
import { mount as mountSignals, badge as signalsBadge } from './c3-signals.js';
|
|
36
|
+
import { mountTicker } from './shell-ticker.js';
|
|
37
|
+
import { mountBell, openBellList } from './shell-bell.js';
|
|
38
|
+
import { onBriefing, loadBriefing } from './shell-ticker.js';
|
|
39
|
+
import { TAG_STYLE, cardView, registry, filterPalette, rowStep } from './briefing-model.js';
|
|
40
|
+
|
|
41
|
+
/* 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. */
|
|
42
|
+
const sc = (name) => (DEMO ? name : undefined);
|
|
43
|
+
const TABS = [['brief', 'Briefing', 'sun'], ['cal', 'Calendar', 'cal'], ['work', 'Work', 'todo'], ['init', 'Initiatives', 'init'], ['rep', 'Reports', 'report']];
|
|
44
|
+
/* The mockup's eight Initiatives views, in its order: Lifecycle, Roadmap, Breakdown, Timeline (= Forecast), Tickets, Releases, Documents, Signals. */
|
|
45
|
+
const INITS = [['lifecycle', 'Lifecycle', mountLifecycle, 'init'], ['roadmap', 'Roadmap', mountRoadmap, 'flag'], ['breakdown', 'Breakdown', mountBreakdown, 'tree'], ['forecast', 'Timeline', mountForecast, 'cal'],
|
|
46
|
+
['tickets', 'Tickets', mountTickets, 'check'], ['releases', 'Releases', mountReleases, 'out'], ['documents', 'Documents', mountDocs, 'doc'], ['signals', 'Signals', mountSignals, 'alert']];
|
|
47
|
+
/* The five views of this page's own (c3-roadmap.js ... c3-signals.js) draw the Plan lens in their own left column, as Lifecycle does. A view with a `badge` gets a count on its button. */
|
|
48
|
+
const OWN = new Set(['roadmap', 'breakdown', 'tickets', 'documents', 'signals']);
|
|
49
|
+
const BADGES = { tickets: ticketsBadge, documents: docsBadge, signals: signalsBadge };
|
|
50
|
+
const THEME_KEY = 'pmos.c3.theme';
|
|
51
|
+
const q = new URLSearchParams(location.search);
|
|
52
|
+
const store = { get(k) { try { return localStorage.getItem(k); } catch { return null; } }, set(k, v) { try { localStorage.setItem(k, v); } catch { /* no storage */ } } };
|
|
53
|
+
const DEMO = q.get('demo') === '1'; // demo mode: c3-demo.js answers the page's /api reads with the mockup's sample content
|
|
54
|
+
const S = { demo: DEMO, ws: q.get('ws') || '', workspaces: [], tab: 'brief', theme: 'light', panes: {}, mounted: {}, init: 'lifecycle', initRows: null, dockOpen: true, wide: true };
|
|
55
|
+
|
|
56
|
+
/* 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. */
|
|
57
|
+
function withWs() {
|
|
58
|
+
const f = window.fetch.bind(window);
|
|
59
|
+
window.fetch = (input, init) => {
|
|
60
|
+
try {
|
|
61
|
+
if (typeof input === 'string' && S.ws && input.startsWith('/api/') && !/[?&]ws=/.test(input) && (!init || !init.method || init.method === 'GET')) {
|
|
62
|
+
input += (input.includes('?') ? '&' : '?') + 'ws=' + encodeURIComponent(S.ws);
|
|
63
|
+
}
|
|
64
|
+
} catch { /* leave the request as it is */ }
|
|
65
|
+
return f(input, init);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function setTheme(t) {
|
|
70
|
+
S.theme = t === 'dark' ? 'dark' : 'light';
|
|
71
|
+
document.documentElement.setAttribute('data-pm-page-theme', S.theme);
|
|
72
|
+
S.root.setAttribute('data-pm-theme', S.theme);
|
|
73
|
+
store.set(THEME_KEY, S.theme);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ── header: the mockup's .top (title, date line) and .tools (terminal, recording, bell, search, more) ── */
|
|
77
|
+
const dateLine = () => {
|
|
78
|
+
const d = new Date();
|
|
79
|
+
const day = d.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }).replace(',', '');
|
|
80
|
+
return day + ' · as of ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') + (S.incident ? ' · incident' : '');
|
|
81
|
+
};
|
|
82
|
+
const ibtn = (label, icon, attrs) => el('button', { type: 'button', class: 'ib', title: label, 'aria-label': label, ...(attrs || {}) }, c3ic(icon));
|
|
83
|
+
const iLink = (label, icon, href) => el('a', { class: 'ib', href, title: label, 'aria-label': label }, c3ic(icon));
|
|
84
|
+
|
|
85
|
+
function header() {
|
|
86
|
+
S.ttl = el('h1', { 'data-c3pg': 'title' });
|
|
87
|
+
S.sub = el('div', { class: 'sub', 'data-c3pg': 'date' }, dateLine());
|
|
88
|
+
S.termBdg = el('span', { class: 'bdg', hidden: true, 'data-c3pg': 'term-count' }, '0');
|
|
89
|
+
/* the AGENT button (the pulse glyph, as the mockup's Sessions pill): the Terminal page, the full agent surfaces, with the needs-you badge. The dock's own button is the TERMINAL glyph (c3-terminal-icons.js); Ctrl J and the More menu fold the dock. */
|
|
90
|
+
S.termBtn = el('button', { type: 'button', class: 'psess', title: 'Sessions: the agent surfaces, results to accept and questions for you', 'aria-label': 'Sessions: the agent surfaces, results to accept and questions for you', 'aria-haspopup': 'dialog', 'data-c3pg': 'agent-button',
|
|
91
|
+
style: 'display:flex;gap:6px;align-items:center', onclick: () => { import('./c3-terminal.js').then((m) => m.openTerminalPage()); } }, ic('pulse', 'sm'), 'Sessions', S.termBdg); // the mockup's header button (agent-surfaces.html #act)
|
|
92
|
+
S.tools = el('div', { class: 'tools', 'data-c3pg': 'tools' });
|
|
93
|
+
if (S.demoMod) S.tools.append(S.demoMod.demoPill());
|
|
94
|
+
S.tools.append(S.termBtn);
|
|
95
|
+
S.bellHost = el('span', { class: 'c3pg-bell' });
|
|
96
|
+
S.tools.append(S.bellHost);
|
|
97
|
+
S.tools.append(ibtn('Search (Ctrl K)', 'search', { 'data-c3pg': 'search', 'aria-haspopup': 'dialog', onclick: () => openPalette() }));
|
|
98
|
+
S.moreBtn = ibtn('More', 'more', { 'aria-haspopup': 'menu', 'aria-expanded': 'false', 'data-c3pg': 'more', onclick: toggleMore });
|
|
99
|
+
S.tools.append(el('span', { class: 'c3pg-menuwrap' }, S.moreBtn));
|
|
100
|
+
return el('div', { class: 'c3top' }, el('div', null, S.ttl, S.sub), S.tools);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* the More menu (console3.html menuPop, line 1940): why each element is here, incident day, agent playbooks, phone preview; and ours: the theme and a way to
|
|
104
|
+
* the main app. Escape and a press outside close it. A switch shows a check while it is on. */
|
|
105
|
+
function toggleMore() {
|
|
106
|
+
if (S.menu) { closeMore(); return; }
|
|
107
|
+
const item = (icon, label, fn, on, attrs, kbd) => el('button', { type: 'button', class: 'c3pg-mi', role: 'menuitem', ...(attrs || {}), onclick: () => { if (attrs && attrs.disabled) return; closeMore(); fn(); } },
|
|
108
|
+
c3ic(icon, 'sm'), el('span', null, label), kbd ? el('kbd', { class: 'c3pg-kbd' }, kbd) : null, on ? el('span', { class: 'st ok', style: 'margin-left:auto' }, c3ic('check', 'sm')) : null);
|
|
109
|
+
S.menu = el('div', { class: 'c3pg-menu menu', role: 'menu', 'aria-label': 'More' },
|
|
110
|
+
item('info', 'Why each element is here', () => toggleWhy(), S.why, { 'data-a': 'whyt' }),
|
|
111
|
+
item('alert', 'Incident day', () => toggleIncident(), S.incident, S.demo ? { 'data-a': 'inc' } : { 'data-a': 'inc', disabled: true, title: 'No incident feed is connected, so this page shows no incident.' }),
|
|
112
|
+
item('spark', 'Agent playbooks', () => openPlaybooks(), false, { 'data-a': 'pbopen' }),
|
|
113
|
+
item('phone', 'Phone preview', () => togglePreview(), !!S.preview, { 'data-a': 'phone' }),
|
|
114
|
+
item(S.theme === 'dark' ? 'sun' : 'moon', S.theme === 'dark' ? 'Light theme' : 'Dark theme', () => setTheme(S.theme === 'dark' ? 'light' : 'dark')),
|
|
115
|
+
item('term', S.dockOpen ? 'Fold the dock' : 'Open the dock', () => { S.dockOpen = !S.dockOpen; syncDock(); }, false, { 'data-a': 'dockt' }, 'Ctrl J'),
|
|
116
|
+
el('a', { class: 'c3pg-mi', role: 'menuitem', href: '/' }, c3ic('term', 'sm'), el('span', null, 'Open the main app')));
|
|
117
|
+
S.root.append(S.menu); // on the page root with position:fixed, so no ancestor's overflow, transform or stacking order can hide it
|
|
118
|
+
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'; };
|
|
119
|
+
S.placeMenu(); addEventListener('resize', S.placeMenu); addEventListener('scroll', S.placeMenu, true);
|
|
120
|
+
S.moreBtn.setAttribute('aria-expanded', 'true');
|
|
121
|
+
S.menuOff = (e) => { if (e.type === 'keydown' ? e.key === 'Escape' : !S.menu.contains(e.target) && !S.moreBtn.contains(e.target)) { closeMore(); if (e.type === 'keydown') S.moreBtn.focus(); } };
|
|
122
|
+
document.addEventListener('keydown', S.menuOff); document.addEventListener('pointerdown', S.menuOff);
|
|
123
|
+
const first = S.menu.querySelector('.c3pg-mi:not([disabled])'); if (first) first.focus();
|
|
124
|
+
}
|
|
125
|
+
function closeMore() {
|
|
126
|
+
if (!S.menu) return;
|
|
127
|
+
document.removeEventListener('keydown', S.menuOff); document.removeEventListener('pointerdown', S.menuOff);
|
|
128
|
+
removeEventListener('resize', S.placeMenu); removeEventListener('scroll', S.placeMenu, true);
|
|
129
|
+
S.menu.remove(); S.menu = null; S.moreBtn.setAttribute('aria-expanded', 'false');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* "Why each element is here" (console3.html whyPanel, line 1458, WHYS 1301): every region of the Briefing gets a number, and a panel steps through the reasons. */
|
|
133
|
+
const WHYS = [
|
|
134
|
+
[1, 'Signals', 'Each card is tagged by what it asks of you. Critical: you lose something if it waits. Just in: new since you last looked. Opportunity: upside that closes. Small win: bank it in one gesture. Big win: tell others. Two questions sort them: what happens if I wait, and how long does this stay true. Tap or swipe a card up and it turns over to a short case file; swipe down to close it.'],
|
|
135
|
+
[2, 'Cause to effect', 'Three nodes show why the decision is urgent, so you do not have to remember it. The moving line shows pressure flowing from your decision to the date.'],
|
|
136
|
+
[3, 'Needs you', 'Exceptions only: what the agents could not or should not decide. Ranked and capped at five. Tap a row to open a case file: what changed, the options, the agent\'s recommendation and what it could not verify.'],
|
|
137
|
+
[4, 'Time left', 'A bar for items with a clock. "Due 10:30" and "due Friday" read the same in text, so the bar shows how much time is left.'],
|
|
138
|
+
[5, 'Review', 'Drafts wait here. Open one to see the draft, the claims it makes and where each claim came from. Approve is a slide, not a tap, so it takes intent. The agent may recommend holding a draft.'],
|
|
139
|
+
[6, 'Handled for you', 'Automation should leave a receipt, not a to-do. Collapsed to one line. Open a line to see the rule that allowed it, the evidence and how to undo.'],
|
|
140
|
+
[7, 'Time lens', 'One lens with five zoom levels, from today to five years. Each level opens with what matters and how it is progressing. The more senior the role, the wider the scan. Use the zoom icons, the + and - keys, or pinch.'],
|
|
141
|
+
[8, 'Portfolio pulse', '"Am I on track?" comes after "what do I do". One bar answers at a glance; rows say why; the small timeline shows promised against forecast.'],
|
|
142
|
+
[9, 'Data trust', 'The agent has already drafted the fixes for mismatches, so this is status only. Folded to one line until you want the evidence and the rule.'],
|
|
143
|
+
[10, 'Gestures', 'Drag a row right to finish it, left for later. The two glyphs at the edge show both; the bright one is what the agent suggests. At rest the page has almost no buttons. Keys: e, n, a, r, j, k.'],
|
|
144
|
+
[11, 'Depth', 'Elevation means priority. Raised layers are things to act on. Flat layers are things to know. Glass floats above the page because it stays while you scroll.'],
|
|
145
|
+
[12, 'Two columns', 'The left column is what you do. The right column is what you need to know. On a phone they merge, with the lens right after the cards.'],
|
|
146
|
+
[13, 'Part of the day', 'The page reads differently as the day goes: Plan, Unblock, Close, Reflect. It follows the clock, and the icons let you preview another part. The same signals are re-ranked, not replaced.'],
|
|
147
|
+
[14, 'Phone: one screen', 'No scrolling on the phone. A running line and the bell carry what just arrived. The four tiles open popups for the lists, so the page itself stays still. Audio is optional: the speaker reads the top three cards aloud for when your hands are busy.'],
|
|
148
|
+
];
|
|
149
|
+
const WHY_REGIONS = { 1: '[data-c3pg="deck-host"]', 13: '.stance', 3: '[data-c3pg="needs"]', 5: '[data-c3pg="review-host"] > *', 6: '[data-c3pg="handled-host"] > *', 7: '[data-c3pg="cal-host"]', 8: '[data-c3pg="pulse"]', 9: '[data-c3pg="trust-host"] > *' };
|
|
150
|
+
function whyMark() {
|
|
151
|
+
S.panes.brief.querySelectorAll('[data-n]').forEach((n) => n.removeAttribute('data-n'));
|
|
152
|
+
if (!S.why) return;
|
|
153
|
+
for (const [n, sel] of Object.entries(WHY_REGIONS)) { const t = S.panes.brief.querySelector(sel); if (t) t.setAttribute('data-n', n); }
|
|
154
|
+
S.panes.brief.querySelectorAll('.wy').forEach((n) => n.classList.remove('wy'));
|
|
155
|
+
const cur = S.panes.brief.querySelector('[data-n="' + S.whyN + '"]'); if (cur) cur.classList.add('wy');
|
|
156
|
+
}
|
|
157
|
+
function whyPanel() {
|
|
158
|
+
if (S.whyWrap) { S.whyWrap.remove(); S.whyWrap = null; }
|
|
159
|
+
if (!S.why) return;
|
|
160
|
+
const w = WHYS[S.whyN - 1];
|
|
161
|
+
const step = (d) => { S.whyN = ((S.whyN - 1 + d + WHYS.length) % WHYS.length) + 1; whyPanel(); whyMark(); };
|
|
162
|
+
const aside = el('aside', { class: 'whyp glass', 'aria-live': 'polite', 'data-c3pg': 'why' },
|
|
163
|
+
el('div', { class: 'chips2', style: 'justify-content:space-between' }, el('span', { class: 'chips2' }, el('b', { class: 'n' }, String(w[0])), el('b', { style: 'font-size:var(--fs-3)' }, w[1])),
|
|
164
|
+
el('span', { class: 'chips2' }, ibtn('Previous reason', 'prev', { class: 'ib sm', onclick: () => step(-1) }), el('span', { class: 'mono dim', style: 'font-size:var(--fs-1)' }, w[0] + ' of ' + WHYS.length),
|
|
165
|
+
ibtn('Next reason', 'next', { class: 'ib sm', onclick: () => step(1) }), ibtn('Close', 'x', { class: 'ib sm', onclick: () => toggleWhy() }))),
|
|
166
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-3);line-height:1.45' }, w[2]));
|
|
167
|
+
S.whyWrap = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'why' }, aside);
|
|
168
|
+
document.body.appendChild(S.whyWrap);
|
|
169
|
+
}
|
|
170
|
+
function toggleWhy() {
|
|
171
|
+
S.why = !S.why; S.whyN = 1;
|
|
172
|
+
if (S.why) showTab('brief', true);
|
|
173
|
+
S.panes.brief.classList.toggle('why-on', S.why);
|
|
174
|
+
whyPanel(); whyMark();
|
|
175
|
+
}
|
|
176
|
+
function toggleIncident() {
|
|
177
|
+
S.incident = !S.incident;
|
|
178
|
+
S.sub.textContent = dateLine();
|
|
179
|
+
if (S.refreshNeeds) S.refreshNeeds();
|
|
180
|
+
}
|
|
181
|
+
function togglePreview() {
|
|
182
|
+
S.preview = !S.preview;
|
|
183
|
+
const old = document.querySelector('[data-shell="preview"]'); if (old) old.remove();
|
|
184
|
+
if (!S.preview) return;
|
|
185
|
+
const close = () => { S.preview = false; const o = document.querySelector('[data-shell="preview"]'); if (o) o.remove(); S.moreBtn.focus(); };
|
|
186
|
+
const frame = el('iframe', { title: 'Phone preview, 390 by 844', src: location.href, style: 'width:390px;height:min(844px,calc(100vh - 80px));border:1px solid var(--border);border-radius:28px;background:var(--background);box-shadow:var(--sh3)' });
|
|
187
|
+
const wrap = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'preview' }, el('div', { class: 'scrim', onclick: close }),
|
|
188
|
+
el('div', { class: 'c3pg-prev', role: 'dialog', 'aria-label': 'Phone preview' }, el('button', { type: 'button', class: 'ib sm', 'aria-label': 'Close the phone preview', title: 'Close (Esc)', onclick: close }, c3ic('x', 'sm')), frame));
|
|
189
|
+
wrap.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
|
|
190
|
+
document.body.appendChild(wrap);
|
|
191
|
+
wrap.querySelector('button').focus();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* the Agent playbooks sheet (console3.html playbookSheet, line 1762). The list comes from the briefing's sample content only; a real workspace defines none that this page can read,
|
|
195
|
+
* so it says so. Changing a mode or adding one has no route here, so those controls are drawn and disabled. */
|
|
196
|
+
function openPlaybooks() {
|
|
197
|
+
const pbs = registry('playbooks') || [];
|
|
198
|
+
const why = 'No route changes a playbook from this page.';
|
|
199
|
+
const MODES = [['shadow', 'eye', 'Shadow'], ['approve', 'check', 'Approve'], ['auto', 'bolt', 'Auto']];
|
|
200
|
+
const body = [el('div', { class: 'legend' }, el('span', null, c3ic('eye', 'sm'), ' shadow: proposes, nothing reaches your queue'), el('span', null, c3ic('check', 'sm'), ' approve: you press Keep'), el('span', null, c3ic('bolt', 'sm'), ' auto: acts, logs, you can undo')),
|
|
201
|
+
...(pbs.length ? pbs.map((b) => el('div', { class: 'card', style: 'gap:6px' },
|
|
202
|
+
el('div', { class: 'chips2', style: 'justify-content:space-between' }, el('b', null, b.n), b.out ? el('span', { class: 'chip2', title: 'An outward step stays with you' }, c3ic('lock', 'sm'), 'outward step is yours') : null),
|
|
203
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-3)' }, c3ic('clock', 'sm'), ' ' + b.trig), el('div', { style: 'font-size:var(--fs-3)' }, c3ic('spark', 'sm'), ' ' + b.first),
|
|
204
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, c3ic('shield', 'sm'), ' ' + b.gate),
|
|
205
|
+
el('div', { class: 'chips2', style: 'justify-content:space-between' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Mode' }, ...MODES.map((m) => el('button', { type: 'button', 'aria-pressed': String(b.mode === m[0]), disabled: true, title: why }, c3ic(m[1], 'sm'), m[2]))),
|
|
206
|
+
el('span', { class: 'dim mono', style: 'font-size:var(--fs-1)' }, b.asis + '% used as is · ' + b.runs + ' runs'))))
|
|
207
|
+
: [el('div', { class: 'fold' }, c3ic('info', 'sm'), 'No playbook is defined that this page can read.')]),
|
|
208
|
+
el('div', { class: 'card' }, el('h3', null, c3ic('plus'), 'Describe a new playbook'), el('div', { class: 'form' }, el('input', { type: 'text', placeholder: 'Name, for example: Prepare the quarterly review', 'aria-label': 'Name', disabled: true, title: why })),
|
|
209
|
+
el('div', { class: 'chips2' }, el('button', { type: 'button', class: 'ib pri', style: 'width:auto;padding:0 12px;gap:6px', disabled: true, title: why }, c3ic('plus', 'sm'), 'Add playbook'))),
|
|
210
|
+
el('div', { class: 'fold' }, c3ic('info', 'sm'), 'Auto needs 10 runs and 90% used as is. A step that sends anything outside stays on approve.')];
|
|
211
|
+
openSheet({ title: 'Agent playbooks', icon: 'spark', wide: true, scrim: false, body });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ── tabs: the mockup's nav.tabs with the sliding indicator ── */
|
|
215
|
+
function tabBar() {
|
|
216
|
+
S.tabBtns = {};
|
|
217
|
+
const nav = el('nav', { class: 'c3tabs', role: 'tablist', 'aria-label': 'Places' });
|
|
218
|
+
S.workBdg = el('span', { class: 'bdg', hidden: true, 'data-c3pg': 'work-count' }, '0');
|
|
219
|
+
for (const [id, label, icon] of TABS) {
|
|
220
|
+
const b = el('button', { type: 'button', role: 'tab', id: 'c3tab-' + id, 'aria-controls': 'c3pane-' + id, 'data-c3-tab': id, onclick: () => showTab(id, true) }, c3ic(icon), label, id === 'work' ? S.workBdg : null);
|
|
221
|
+
S.tabBtns[id] = b;
|
|
222
|
+
nav.append(b);
|
|
223
|
+
}
|
|
224
|
+
S.ind = el('span', { class: 'ind', 'aria-hidden': 'true' });
|
|
225
|
+
nav.append(S.ind);
|
|
226
|
+
nav.addEventListener('keydown', (e) => {
|
|
227
|
+
const ids = TABS.map((t) => t[0]); const i = ids.indexOf(S.tab);
|
|
228
|
+
const to = e.key === 'ArrowRight' ? ids[(i + 1) % ids.length] : e.key === 'ArrowLeft' ? ids[(i + ids.length - 1) % ids.length] : e.key === 'Home' ? ids[0] : e.key === 'End' ? ids[ids.length - 1] : null;
|
|
229
|
+
if (to) { e.preventDefault(); showTab(to, true); S.tabBtns[to].focus(); }
|
|
230
|
+
});
|
|
231
|
+
S.tabNav = nav;
|
|
232
|
+
return nav;
|
|
233
|
+
}
|
|
234
|
+
function moveInd() {
|
|
235
|
+
const b = S.tabBtns[S.tab]; if (!b || !S.ind) return;
|
|
236
|
+
S.ind.style.width = b.offsetWidth + 'px'; S.ind.style.transform = 'translateX(' + b.offsetLeft + 'px)';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ── the Briefing tab: three columns ── */
|
|
240
|
+
/* "Needs you": one row per need (c3-brief.js needRow). A press on a row opens its case file in place; a swipe, or the keys e and n, acts; j and k move. The rows
|
|
241
|
+
* are built again only when the briefing's needs changed, so a re-read does not close an open row or a running agent. On an incident day (console3.html line 1467)
|
|
242
|
+
* only the needs with a hard deadline stay. */
|
|
243
|
+
function needsList() {
|
|
244
|
+
const count = el('span', { class: 'bdg g' }, '0');
|
|
245
|
+
const quick = el('span', { class: 'chips2 dim mono', style: 'font-size:var(--fs-2)' });
|
|
246
|
+
const list = el('ul', { class: 'list', role: 'list' });
|
|
247
|
+
const extra = el('div');
|
|
248
|
+
const alertHost = el('div');
|
|
249
|
+
const box = el('section', { class: 'card sect', 'data-c3pg': 'needs', 'aria-label': 'Needs you' },
|
|
250
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('inbox', 'sm'), 'Needs you ', count)), alertHost, list, extra);
|
|
251
|
+
let sig = ''; let last = null;
|
|
252
|
+
const recount = () => {
|
|
253
|
+
const n = box.querySelectorAll('.nrow[data-needs-row]').length;
|
|
254
|
+
count.textContent = String(n);
|
|
255
|
+
if (!n && !list.querySelector('.fold')) list.replaceChildren(el('li', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing needs you.'));
|
|
256
|
+
};
|
|
257
|
+
const ctx = { changed: recount };
|
|
258
|
+
const fill = (b) => {
|
|
259
|
+
last = b;
|
|
260
|
+
const inc = !!S.incident;
|
|
261
|
+
const needs = b && Array.isArray(b.needs) && b.needs.length ? b.needs.filter((n) => !BX.gone[n.key] && (!inc || n.hard)) : null;
|
|
262
|
+
const key = needs ? needs.map((n) => n.key + n.headline) : ((b && b.cards) || []).map((c) => (c.key || '') + c.headline);
|
|
263
|
+
if (needs && S.pinNeed) { const at = needs.findIndex((n) => n.key === S.pinNeed); if (at > 0) needs.unshift(...needs.splice(at, 1)); } // a need the banner or the bell named rides to the top of the list: shown, not folded
|
|
264
|
+
const g = JSON.stringify([key, inc, !!(b && b.kept_out), S.pinNeed || '']);
|
|
265
|
+
if (g === sig) return; sig = g;
|
|
266
|
+
alertHost.replaceChildren(...(inc ? [el('div', { class: 'incident', role: 'status', 'data-c3pg': 'incident' }, c3ic('alert', 'lg'), el('div', null, el('b', null, 'Card payments failing since 07:40'), el('div', { class: 'dim', style: 'font-size:var(--fs-3)' }, '12% of checkouts. Sam on call. Only deadlines shown.')))] : []));
|
|
267
|
+
if (needs) {
|
|
268
|
+
if (!quick.isConnected) box.querySelector('.sh2').append(quick);
|
|
269
|
+
quick.textContent = ''; quick.append(c3ic('bolt', 'sm'), needs.filter((n) => n.quick).length + ' under 2 min');
|
|
270
|
+
list.replaceChildren(...needs.slice(0, 5).map((n, i) => needRow(n, i, ctx))); // the mockup shows five and folds the rest "below the line"
|
|
271
|
+
extra.replaceChildren(...(inc ? [] : belowLine(needs.slice(5), (n, i) => needRow(n, i + 5, ctx), b.kept_out)));
|
|
272
|
+
recount();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const cards = ((b && b.cards) || []).map(cardView);
|
|
276
|
+
quick.remove();
|
|
277
|
+
list.replaceChildren(...(cards.length ? cards.map((v, i) => {
|
|
278
|
+
const T = TAG_STYLE[v.tag] || TAG_STYLE['Just in'];
|
|
279
|
+
return needRow({ key: v.key || String(i), headline: v.headline, kind: v.tag, icon: T.icon, color: T.color, time: v.deadline && v.deadline.text ? v.deadline.text : null, src: v.tag, view: v }, i, ctx);
|
|
280
|
+
}) : [el('li', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing needs you.')]));
|
|
281
|
+
extra.replaceChildren();
|
|
282
|
+
count.textContent = String(cards.length);
|
|
283
|
+
};
|
|
284
|
+
S.refreshNeeds = () => { sig = ''; if (last) fill(last); };
|
|
285
|
+
S.needsFill = fill;
|
|
286
|
+
onBriefing((b) => { S.brief = b; fill(b); });
|
|
287
|
+
loadBriefing();
|
|
288
|
+
return box;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* portfolio pulse: the Calendar's own gantt rows (GET /api/calendar-views, quarter) as one status bar and a row per initiative */
|
|
292
|
+
const PST = { 'on time': ['ok', 'On time', 'var(--success)'], 'at risk': ['warn', 'At risk', 'var(--warning)'], delayed: ['bad', 'Delayed', 'var(--destructive)'], overdue: ['bad', 'Overdue', 'var(--destructive)'], 'no plan': ['', 'No plan', 'var(--muted-foreground)'] };
|
|
293
|
+
|
|
294
|
+
function pulse() {
|
|
295
|
+
const body = el('div', null, el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'Reading the initiatives.'));
|
|
296
|
+
const box = el('section', { class: 'card', 'data-c3pg': 'pulse', style: 'gap:8px' },
|
|
297
|
+
el('div', { class: 'chips2', style: 'justify-content:space-between' },
|
|
298
|
+
el('h3', null, c3ic('init', 'sm'), 'Portfolio pulse'), el('span', { class: 'mono dim', style: 'font-size:var(--fs-1)' }, 'promised vs forecast')), body);
|
|
299
|
+
const draw = (rows, err) => {
|
|
300
|
+
if (err) { body.replaceChildren(el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status' }, 'The initiatives could not be read: ' + err)); return; }
|
|
301
|
+
if (!rows.length) { body.replaceChildren(el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No initiative has a promised date yet, so there is nothing to compare with the forecast.')); return; }
|
|
302
|
+
if (rows.some((g) => g.pulse)) { body.replaceChildren(...mockPulse(rows, S.today, () => showTab('init', true))); return; } // demo: the mockup's pulse (its own colours, reasons and mini timelines)
|
|
303
|
+
const noPlan = rows.filter((g) => g.status === 'no plan').length;
|
|
304
|
+
rows = rows.filter((g) => g.status !== 'no plan');
|
|
305
|
+
if (!rows.length) { body.replaceChildren(el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No initiative has a promised date yet, so there is nothing to compare with the forecast.'), noPlan ? el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, noPlan + ' initiatives have no plan date.') : null); return; }
|
|
306
|
+
const by = {};
|
|
307
|
+
rows.forEach((g) => { const k = PST[g.status] ? g.status : 'no plan'; by[k] = (by[k] || 0) + 1; });
|
|
308
|
+
const bar = el('div', { class: 'pbs', role: 'group', 'aria-label': 'Status of initiatives' },
|
|
309
|
+
...Object.keys(by).map((k) => el('span', { style: 'flex:' + by[k] + ';height:10px;border-radius:5px;background:' + PST[k][2], title: by[k] + ' ' + PST[k][1].toLowerCase(), 'aria-label': by[k] + ' ' + PST[k][1].toLowerCase() })));
|
|
310
|
+
const end = (o) => (o && (o.end || o.start)) || null;
|
|
311
|
+
const rowEl = (g) => {
|
|
312
|
+
const k = PST[g.status] ? g.status : 'no plan'; const f = end(g.forecast); const p = g.promised || null;
|
|
313
|
+
const why = p ? 'Forecast ' + (f || 'not set') + ' vs promised ' + p : 'No promised date' + (f ? '; forecast ' + f : '');
|
|
314
|
+
return el('button', { type: 'button', class: 'prow', 'data-c3pg-prow': g.id, 'data-a': 'goinit', 'aria-label': 'Open the initiatives: ' + (g.title || g.id), onclick: () => showTab('init', true) },
|
|
315
|
+
el('span', { class: 'dotc ' + PST[k][0] }),
|
|
316
|
+
el('span', { style: 'min-width:0' }, el('b', { style: 'font-size:var(--fs-3)' }, g.title || g.id), el('div', { class: 'dim', style: 'font-size:var(--fs-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis' }, why)),
|
|
317
|
+
el('span', { class: 'mono', style: 'font-size:var(--fs-2);color:' + PST[k][2] }, PST[k][1] + (g.slip_days > 0 ? ' +' + g.slip_days + ' d' : '')));
|
|
318
|
+
};
|
|
319
|
+
body.replaceChildren(bar, el('div', null, ...rows.slice(0, 8).map(rowEl)), rows.length > 8 ? el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, '+' + (rows.length - 8) + ' more in the Calendar tab') : null, noPlan ? el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, noPlan + ' more have no plan date.') : null);
|
|
320
|
+
};
|
|
321
|
+
readQuarter().then((j) => {
|
|
322
|
+
if (!j || j.ok === false) draw([], (j && j.error) || 'unreadable'); else { S.today = j.today || ''; draw(j.gantt || []); }
|
|
323
|
+
}).catch((e) => draw([], String((e && e.message) || e)));
|
|
324
|
+
return box;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function briefTab(pane) {
|
|
328
|
+
const left = el('div', { class: 'bcol l' }); const mid = el('div', { class: 'bcol r' });
|
|
329
|
+
const deckHost = el('div', { 'data-c3pg': 'deck-host' }); const calHost = el('div', { class: 'c3pg-midcal', 'data-c3pg': 'cal-host' });
|
|
330
|
+
const rvHost = el('div', { 'data-c3pg': 'review-host' }); const hdHost = el('div', { 'data-c3pg': 'handled-host' }); const trHost = el('div', { 'data-c3pg': 'trust-host' }); const ftHost = el('div'); const stHost = el('div', { 'data-c3pg': 'stop-host' }); S.stopHost = stHost; S.needsBox = null;
|
|
331
|
+
S.deckHost = deckHost;
|
|
332
|
+
let sig = '';
|
|
333
|
+
onBriefing((b) => { // below "Needs you": only what the briefing carries (the demo's); the real route sends none of these. Built again only when it changed: an open draft stays open.
|
|
334
|
+
const g = JSON.stringify([b && b.review, b && b.handled, b && b.trust, b && b.foot, b && b.stop]);
|
|
335
|
+
if (g === sig) return; sig = g;
|
|
336
|
+
rvHost.replaceChildren(...(b && b.review ? [reviewSection(b.review)] : [])); hdHost.replaceChildren(...(b && b.handled ? [handledRow(b.handled)] : []));
|
|
337
|
+
stHost.replaceChildren(...[stopCard(b && b.stop)].filter(Boolean)); trHost.replaceChildren(...(b && b.trust ? [trustCard(b.trust)] : [])); ftHost.replaceChildren(...(b && b.foot ? [footLine(b.foot)] : []));
|
|
338
|
+
if (S.why) whyMark();
|
|
339
|
+
});
|
|
340
|
+
S.needsBox = needsList(); left.append(deckHost, S.needsBox, rvHost, hdHost, stHost);
|
|
341
|
+
mid.append(calHost, pulse(), trHost);
|
|
342
|
+
pane.append(el('div', { class: 'brief' }, el('div', { class: 'bcols' }, left, mid), ftHost));
|
|
343
|
+
mountBriefing(deckHost, { noTicker: true, onLens: lensChanged });
|
|
344
|
+
mountCalendarViews(calHost, { view: 'day', scope: sc('brief'), ribbon: true });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* The day-part switch moves the time lens as the mockup does (setTod, line 2209): Reflect shows the week, any other part goes back to today if Reflect had moved it. */
|
|
348
|
+
function ribbonGo(label) {
|
|
349
|
+
const b = [...S.panes.brief.querySelectorAll('[data-cv="ribbon"] .seg button')].find((x) => x.textContent.trim() === label);
|
|
350
|
+
if (b) b.click();
|
|
351
|
+
}
|
|
352
|
+
function lensChanged(l) {
|
|
353
|
+
const rf = l === 'Reflect'; if (S.stopHost) S.stopHost.hidden = !rf; setNeedsTitle(S.needsBox, rf); // Reflect: "Carries over" and "What I could stop" (the mockup's vBrief, line 1470)
|
|
354
|
+
const cur = S.panes && S.panes.brief ? S.panes.brief.querySelector('[data-cv="ribbon"] .seg [aria-pressed="true"]') : null;
|
|
355
|
+
const on = cur ? cur.textContent.trim() : '';
|
|
356
|
+
if (l === 'Reflect' && on === 'Today') { S.rAuto = true; ribbonGo('Week'); }
|
|
357
|
+
else if (l !== 'Reflect' && S.rAuto) { S.rAuto = false; ribbonGo('Today'); }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* ── global: where a notification goes, the person sheet, the palette and the keys ── */
|
|
361
|
+
/* A bell entry or the banner goes to its target (console3.html nopen, line 2530): the card to turn over, the row to open, or the sheet to show. */
|
|
362
|
+
function goTarget(t) {
|
|
363
|
+
showTab('brief', true);
|
|
364
|
+
if (!t) return;
|
|
365
|
+
if (t.card !== undefined && t.card !== null) { showCard(S.deckHost, t.card); return; }
|
|
366
|
+
if (t.need) {
|
|
367
|
+
let row = S.panes.brief.querySelector('[data-needs-row="' + String(t.need).replace(/"/g, '') + '"]');
|
|
368
|
+
if (row && row.closest('.xp:not(.open)') && S.refreshNeeds) { S.pinNeed = String(t.need); S.refreshNeeds(); row = S.panes.brief.querySelector('[data-needs-row="' + String(t.need).replace(/"/g, '') + '"]') || row; } // below the line: the row joins the visible rows, the fold stays shut
|
|
369
|
+
if (row) {
|
|
370
|
+
for (let f = row.closest('.xp'); f; f = f.parentElement && f.parentElement.closest('.xp')) { const fb = f.previousElementSibling; if (!f.classList.contains('open') && fb && fb.matches('button[aria-expanded]')) fb.click(); } // a row below the line sits in a closed fold: open the fold first, as the mockup does
|
|
371
|
+
const api = rowApi.get(row); if (api) api.open(); row.scrollIntoView({ block: 'center', behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' }); try { row.focus({ preventScroll: true }); } catch { /* gone */ } }
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (t.sheet === 'review' && S.brief && S.brief.review) openSheet({ title: 'Review', icon: 'spark', wide: true, body: reviewSection(S.brief.review) });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* the person sheet (console3.html sheet, line 1927) */
|
|
378
|
+
const personNotes = {};
|
|
379
|
+
function openPerson(k) {
|
|
380
|
+
const p = (registry('people') || {})[k]; if (!p) return;
|
|
381
|
+
const row = (x) => el('div', { class: 'chips2', style: 'justify-content:space-between' }, el('span', null, x.t, el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, x.due + ' · ' + x.init)), el('span', { class: 'st ' + (x.overdue ? 'bad' : 'warn') }, c3ic(x.overdue ? 'alert' : 'clock', 'sm')));
|
|
382
|
+
const h3 = (icon, t) => el('h3', { class: 'mono dim', style: 'font-size:var(--fs-1);margin:0' }, c3ic(icon, 'sm'), ' ' + t);
|
|
383
|
+
const note = el('textarea', { id: 'pn', 'aria-label': 'Private notes', style: 'width:100%;min-height:70px;margin-top:6px', placeholder: 'Only you write here', oninput: () => { personNotes[k] = note.value; } });
|
|
384
|
+
note.value = personNotes[k] || '';
|
|
385
|
+
const body = [el('div', { class: 'chips2 dim mono', style: 'font-size:var(--fs-2)' }, c3ic('clock', 'sm'), 'last spoke ' + p.last),
|
|
386
|
+
el('div', { class: 'chips2 dim mono', style: 'font-size:var(--fs-2)' }, c3ic('out', 'sm'), 'told: ' + p.told),
|
|
387
|
+
h3('out', 'YOU OWE'), ...((p.owe || []).length ? p.owe.map(row) : [el('span', { class: 'dim' }, 'Nothing')]),
|
|
388
|
+
h3('wait', 'YOU WAIT ON'), ...((p.theirs || []).length ? p.theirs.map(row) : [el('span', { class: 'dim' }, 'Nothing')]),
|
|
389
|
+
...((p.kept || []).length ? [h3('check', 'KEPT'), ...p.kept.map((x) => el('div', { class: 'dim', style: 'font-size:var(--fs-3)' }, x))] : []),
|
|
390
|
+
el('details', null, el('summary', { class: 'chips2 dim', style: 'cursor:pointer;font-size:var(--fs-3);min-height:44px' }, c3ic('lock', 'sm'), 'private notes'), note,
|
|
391
|
+
el('div', { class: 'dim mono', style: 'font-size:var(--fs-1)' }, 'local only · never read into a draft, summary or session'))];
|
|
392
|
+
openSheet({ title: p.n + ' · ' + p.role + ' · ' + p.tier, icon: 'user', scrim: false, body });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* the palette (console3.html palette, line 1944): one input, up to nine results across places, people, documents and sessions */
|
|
396
|
+
function paletteItems() {
|
|
397
|
+
const items = [{ label: 'Agent playbooks', kind: 'place', icon: 'spark', run: () => openPlaybooks() },
|
|
398
|
+
...TABS.map(([id, label, icon]) => ({ label, kind: 'place', icon, run: () => showTab(id, true) }))];
|
|
399
|
+
for (const k of Object.keys(registry('people') || {})) { const p = registry('people')[k]; items.push({ label: p.n + ' · ' + p.role, kind: 'person', icon: 'user', run: () => openPerson(k) }); }
|
|
400
|
+
for (const d of registry('docs') || []) items.push({ label: d.n, kind: 'document', icon: d.icon || 'doc', run: () => { showTab('init', true); const b = S.initSeg && S.initSeg.querySelector('[data-c3-init="documents"]'); if (b) b.click(); } });
|
|
401
|
+
for (const x of S.palSessions || []) items.push({ label: (x.branch || x.tree || '') ? ((x.branch || String(x.tree).split('/').pop()) + ' · ' + x.name) : x.name, kind: 'session', icon: 'branch', run: () => openSession(x) });
|
|
402
|
+
return items;
|
|
403
|
+
}
|
|
404
|
+
function openSession(x) {
|
|
405
|
+
showTab('brief', true); S.dockOpen = true; syncDock();
|
|
406
|
+
const b = [...(S.dockEl ? S.dockEl.querySelectorAll('.sr') : [])].find((n) => n.textContent.includes(x.name));
|
|
407
|
+
if (b) b.click(); else try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id: x.id } })); } catch { /* no event */ }
|
|
408
|
+
}
|
|
409
|
+
function openPalette() {
|
|
410
|
+
if (S.pal) return;
|
|
411
|
+
closeMore();
|
|
412
|
+
const opener = document.activeElement && document.activeElement !== document.body ? document.activeElement : S.root.querySelector('[data-c3pg="search"]');
|
|
413
|
+
const input = el('input', { type: 'text', id: 'pq', placeholder: 'Go to a place, person, document or session', 'aria-label': 'Search', autocomplete: 'off', role: 'combobox', 'aria-expanded': 'true', 'aria-controls': 'pq-list', 'aria-autocomplete': 'list' });
|
|
414
|
+
const ul = el('ul', { class: 'list', id: 'pq-list', role: 'listbox', 'aria-label': 'Results' });
|
|
415
|
+
let at = 0; let list = [];
|
|
416
|
+
const pick = (it) => { closePalette(false); it.run(); };
|
|
417
|
+
const draw = () => {
|
|
418
|
+
list = filterPalette(paletteItems(), input.value);
|
|
419
|
+
at = Math.max(0, Math.min(at, list.length - 1));
|
|
420
|
+
ul.replaceChildren(...(list.length ? list.map((it, i) => el('li', null, el('button', { type: 'button', role: 'option', 'aria-selected': String(i === at), 'data-kind': it.kind, onclick: () => pick(it) }, c3ic(it.icon), it.label))) : [el('li', { class: 'dim', style: 'padding:10px 14px' }, 'Nothing matches.')]));
|
|
421
|
+
};
|
|
422
|
+
input.addEventListener('input', () => { at = 0; draw(); });
|
|
423
|
+
input.addEventListener('keydown', (e) => {
|
|
424
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); at = Math.min(list.length - 1, at + 1); draw(); }
|
|
425
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); at = Math.max(0, at - 1); draw(); }
|
|
426
|
+
else if (e.key === 'Enter' && list[at]) { e.preventDefault(); pick(list[at]); }
|
|
427
|
+
});
|
|
428
|
+
const wrap = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'palette' },
|
|
429
|
+
el('div', { class: 'pal', onclick: (e) => { if (e.target === e.currentTarget) closePalette(true); } }, el('div', { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Search' }, input, ul)));
|
|
430
|
+
document.body.appendChild(wrap);
|
|
431
|
+
S.pal = { wrap, opener };
|
|
432
|
+
draw(); input.focus();
|
|
433
|
+
/* sessions are read once per opening: GET /api/sessions */
|
|
434
|
+
pmGet('/api/sessions').then((j) => { if (S.pal && S.pal.wrap === wrap && j && Array.isArray(j.sessions)) { S.palSessions = j.sessions.filter((x) => !x.automatic); draw(); } }).catch(() => { /* none */ });
|
|
435
|
+
}
|
|
436
|
+
function closePalette(back) {
|
|
437
|
+
if (!S.pal) return;
|
|
438
|
+
const { wrap, opener } = S.pal; S.pal = null; wrap.remove();
|
|
439
|
+
if (back && opener && opener.isConnected && opener.focus) { try { opener.focus({ preventScroll: true }); } catch { /* gone */ } }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* The keys the mockup's script binds (console3.html 2191 to 2203 and 2460 to 2468), on the Briefing: j and k move the row focus, e and n act on a need, a and r on a draft,
|
|
443
|
+
* + and - move the time lens, Esc turns the open card back; everywhere: Ctrl or Cmd with J folds the dock, with K opens the search. "/" opens the search too (the mockup
|
|
444
|
+
* does not bind it; the PM asked for it). One handler on the document, in the capture phase, so it runs before a card or a calendar sees the key. */
|
|
445
|
+
const typing = (n) => !!(n && n.matches && n.matches('input,textarea,select,[contenteditable="true"]'));
|
|
446
|
+
const sheetOpen = () => !!document.querySelector('[data-shell="sheet"],[data-shell="preview"]');
|
|
447
|
+
function onKey(e) {
|
|
448
|
+
const ae = document.activeElement;
|
|
449
|
+
if ((e.metaKey || e.ctrlKey) && !e.altKey) {
|
|
450
|
+
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
|
|
451
|
+
else if (e.key === 'k' || e.key === 'K') { e.preventDefault(); if (S.pal) closePalette(true); else openPalette(); }
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (e.altKey) return;
|
|
455
|
+
if (S.pal) { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closePalette(true); } return; }
|
|
456
|
+
if (e.key === 'Escape' && S.tab === 'brief' && !S.menu && !sheetOpen() && S.deckHost && flipBack(S.deckHost)) { e.stopPropagation(); return; }
|
|
457
|
+
if (typing(ae) || sheetOpen()) return;
|
|
458
|
+
if (e.key === '/' && !e.shiftKey) { e.preventDefault(); openPalette(); return; }
|
|
459
|
+
if (S.tab !== 'brief') return;
|
|
460
|
+
const rows = [...S.panes.brief.querySelectorAll('[data-row]')].filter((r) => r.offsetParent !== null && getComputedStyle(r).visibility !== 'hidden');
|
|
461
|
+
const cur = ae && ae.closest ? ae.closest('[data-row][data-id]') : null;
|
|
462
|
+
if (e.key === 'j' || e.key === 'k') {
|
|
463
|
+
e.preventDefault();
|
|
464
|
+
const ni = rowStep(rows.length, rows.indexOf(cur), e.key === 'j' ? 1 : -1);
|
|
465
|
+
if (rows[ni]) rows[ni].focus();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if ('+=-_'.includes(e.key) && e.key.length === 1) {
|
|
469
|
+
const b = S.panes.brief.querySelector('[data-cv="ribbon"] [aria-label^="Zoom ' + (e.key === '+' || e.key === '=' ? 'in' : 'out') + '"]');
|
|
470
|
+
e.stopPropagation(); if (b && !b.disabled) b.click();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (cur && e.key.length === 1 && 'enar'.includes(e.key)) {
|
|
474
|
+
e.stopPropagation(); e.preventDefault();
|
|
475
|
+
const api = rowApi.get(cur); const draft = cur.classList.contains('drow');
|
|
476
|
+
if (api && !draft && (e.key === 'e' || e.key === 'n')) (e.key === 'e' ? api.right : api.left)();
|
|
477
|
+
else if (api && draft && (e.key === 'a' || e.key === 'r')) (e.key === 'a' ? api.right : api.left)();
|
|
478
|
+
else toast(draft ? 'Drafts: a approves, r sends back' : 'Items: e done, n later');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* ── the dock: the mockup's repository, branch and session tree (c3-dock.js) ── */
|
|
483
|
+
function dock() {
|
|
484
|
+
S.dock = createDock({
|
|
485
|
+
setOpen: (on) => { S.dockOpen = !!on; syncDock(); },
|
|
486
|
+
onWs: (id) => { const u = new URL(location.href); u.searchParams.set('ws', id); location.assign(u.toString()); },
|
|
487
|
+
/* the needs-you badge on the agent button: runs that ask, hold a permission or need the terminal (not the running ones) */
|
|
488
|
+
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
|
|
489
|
+
});
|
|
490
|
+
S.dockEl = S.dock.side; S.rail = S.dock.rail;
|
|
491
|
+
window.addEventListener('pmos:open-session', () => location.assign('/'));
|
|
492
|
+
return [S.dock.side, S.dock.rail];
|
|
493
|
+
}
|
|
494
|
+
/* The mockup opens the dock on the Briefing and folds it to a rail on the other tabs (console3.html line 2017). */
|
|
495
|
+
function syncDock() {
|
|
496
|
+
S.dockEl.hidden = !S.dockOpen;
|
|
497
|
+
S.rail.hidden = S.dockOpen || !S.wide;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* ── the other tabs ── */
|
|
501
|
+
function initTab(pane) {
|
|
502
|
+
S.initSeg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Initiatives views', 'data-c3pg': 'init-switch', style: 'overflow-x:auto;max-width:100%' });
|
|
503
|
+
S.initHosts = {};
|
|
504
|
+
const ws = () => S.workspaces.find((w) => w.id === S.ws) || {};
|
|
505
|
+
const reload = () => { S.initRows = null; S.mounted = Object.fromEntries(Object.entries(S.mounted).filter(([k]) => !k.startsWith('init-'))); loadInitRows().then(() => showInit(S.init)); };
|
|
506
|
+
const add = newInitLine({ get repo() { return ws().root || null; }, ws: S.ws, get rows() { return (S.initRows && S.initRows.rows) || []; }, onAdded: reload });
|
|
507
|
+
const bar = el('div', { class: 'calbar' }, S.initSeg);
|
|
508
|
+
const body = el('div', { class: 'c3pg-initbody' });
|
|
509
|
+
for (const [id] of INITS) { S.initHosts[id] = el('div', { 'data-in-host': id, hidden: true }); body.append(S.initHosts[id]); }
|
|
510
|
+
pane.append(add, bar, body);
|
|
511
|
+
S.initNote = el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-c3pg': 'init-note' }, 'Reading the repository');
|
|
512
|
+
body.prepend(S.initNote);
|
|
513
|
+
drawInitSwitch();
|
|
514
|
+
loadInitRows().then(() => { showInit(S.init); loadBadges(); });
|
|
515
|
+
}
|
|
516
|
+
/* The counts on the sub-view buttons: each view's own `badge(ctx)` (a number, or null when it cannot say). */
|
|
517
|
+
function loadBadges() {
|
|
518
|
+
const ws = S.workspaces.find((w) => w.id === S.ws) || {};
|
|
519
|
+
const ctx = { repo: ws.root || null, ws: S.ws, rows: (S.initRows && S.initRows.rows) || [] };
|
|
520
|
+
S.badges = S.badges || {};
|
|
521
|
+
for (const [id, fn] of Object.entries(BADGES)) Promise.resolve().then(() => fn(ctx)).then((n) => { S.badges[id] = Number(n) || 0; drawInitSwitch(); }, () => { /* no count */ });
|
|
522
|
+
}
|
|
523
|
+
function drawInitSwitch() {
|
|
524
|
+
S.initSeg.replaceChildren(...INITS.map(([id, label, mount, icon]) => {
|
|
525
|
+
const n = S.badges && S.badges[id];
|
|
526
|
+
return el('button', { type: 'button', 'aria-pressed': String(S.init === id), 'data-c3-init': id, onclick: () => showInit(id) }, c3ic(icon, 'sm'), label, n ? [' ', el('span', { class: 'bdg', 'data-c3-init-badge': id }, String(n))] : null);
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
/* The roadmap read answers { pending } while the repository is still being read. Ask again until it has rows, and say so meanwhile. */
|
|
530
|
+
async function loadInitRows() {
|
|
531
|
+
for (let n = 0; n < 40; n++) {
|
|
532
|
+
const r = await pmGet('/api/initiatives?ws=' + encodeURIComponent(S.ws) + '&view=roadmap');
|
|
533
|
+
if (r && r.horizons) {
|
|
534
|
+
S.initRows = { rows: [...(r.horizons.now || []), ...(r.horizons.next || []), ...(r.horizons.later || []), ...(r.dormant || [])], sig: r.sig || null, strategy: r.strategy || null };
|
|
535
|
+
S.initNote.hidden = true;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (!(r && r.pending)) {
|
|
539
|
+
S.initRows = { rows: [], sig: null };
|
|
540
|
+
S.initNote.hidden = false;
|
|
541
|
+
S.initNote.textContent = 'The initiatives could not be read' + (r && r.error ? ': ' + r.error : '.');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
S.initNote.hidden = false; S.initNote.textContent = 'Reading the repository';
|
|
545
|
+
await new Promise((res) => setTimeout(res, 4000));
|
|
546
|
+
}
|
|
547
|
+
S.initRows = { rows: [], sig: null };
|
|
548
|
+
S.initNote.textContent = 'The repository is taking long to read. Reload in a moment for the initiatives.';
|
|
549
|
+
}
|
|
550
|
+
function showInit(id) {
|
|
551
|
+
S.init = id; drawInitSwitch();
|
|
552
|
+
const ws = S.workspaces.find((w) => w.id === S.ws) || {};
|
|
553
|
+
for (const [k, , mount] of INITS) {
|
|
554
|
+
if (!mount) continue;
|
|
555
|
+
S.initHosts[k].hidden = k !== id;
|
|
556
|
+
if (k === id && !S.mounted['init-' + k] && S.initRows) {
|
|
557
|
+
S.mounted['init-' + k] = true;
|
|
558
|
+
const ctx = { ...initiativesViewCtx({ repo: ws.root || null, ws: S.ws, rows: S.initRows.rows, sig: S.initRows.sig, openInitiative: () => location.assign('/'), setState: () => {} }), strategy: S.initRows.strategy, plan: planNode(sc(OWN.has(k) ? 'init-' + k : 'init')), links: crossLinks((t) => showTab(t, true)) };
|
|
559
|
+
if (k === 'lifecycle' || OWN.has(k)) mount(S.initHosts[k], Object.freeze(ctx));
|
|
560
|
+
else { const view = el('div'); S.initHosts[k].replaceChildren(ctx.plan, view); mount(view, Object.freeze({ ...ctx, plan: null })); }
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function repTab(pane) {
|
|
565
|
+
const fam = el('div', { 'data-c3pg': 'families-host' }); const out = el('div', { class: 'brief', 'data-c3pg': 'outbox-host', style: 'margin-top:var(--sp-4)' });
|
|
566
|
+
pane.append(fam, out);
|
|
567
|
+
pmGet('/api/report-families/facts?ws=' + encodeURIComponent(S.ws)).then((j) => {
|
|
568
|
+
mountReportFamilies(fam, { reportFacts: j && j.ok ? j.facts : {}, plan: planNode(sc('rep')), portfolio: portfolioCard() });
|
|
569
|
+
mountOutbox(out, {});
|
|
570
|
+
/* The mockup's Reports draw the outbox's drafts under Communications only (the Messages queue); the other families have no outbox. */
|
|
571
|
+
const sync = () => { out.hidden = !fam.querySelector('[data-family="communications"][aria-pressed="true"]'); };
|
|
572
|
+
sync(); new MutationObserver(sync).observe(fam, { subtree: true, attributes: true, attributeFilter: ['aria-pressed'] });
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const BUILD = { brief: briefTab, cal: (p) => mountCalendarViews(p, { view: 'day', plan: planNode(sc('cal')), due: dueCard(), scope: sc('cal') }), work: (p) => { const plan = planNode(sc('work') && 'work:triage'); mountPmWork(p, { plan, onLens: (l) => { if (DEMO) setPlanScope(plan, 'work:' + l); } }); }, init: initTab, rep: repTab };
|
|
577
|
+
|
|
578
|
+
function showTab(id, push) {
|
|
579
|
+
if (!BUILD[id]) id = 'brief';
|
|
580
|
+
S.tab = id;
|
|
581
|
+
for (const [t, label, icon] of TABS) {
|
|
582
|
+
const on = t === id;
|
|
583
|
+
S.tabBtns[t].setAttribute('aria-selected', String(on)); S.tabBtns[t].tabIndex = on ? 0 : -1;
|
|
584
|
+
S.panes[t].hidden = !on;
|
|
585
|
+
if (on) { S.ttl.replaceChildren(c3ic(icon, 'lg'), label); document.title = label + ' · PMOS console'; }
|
|
586
|
+
}
|
|
587
|
+
if (!S.mounted[id]) { S.mounted[id] = true; BUILD[id](S.panes[id]); }
|
|
588
|
+
S.root.dataset.tab = id;
|
|
589
|
+
S.dockOpen = S.wide && id === 'brief'; syncDock();
|
|
590
|
+
S.pageView.classList.remove('view'); void S.pageView.offsetWidth; S.pageView.classList.add('view');
|
|
591
|
+
moveInd();
|
|
592
|
+
if (push) { try { const u = new URL(location.href); u.searchParams.set('tab', id); history.replaceState(null, '', u); } catch { /* no history */ } }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function workCount() {
|
|
596
|
+
const j = await pmGet('/api/pm-items').catch(() => null);
|
|
597
|
+
if (!j || !j.ok || !j.lists) return;
|
|
598
|
+
const n = (j.lists.triage || []).length + (S.demo ? 0 : (j.lists.today || []).length); // the mockup's Work badge counts what is to triage
|
|
599
|
+
S.workBdg.textContent = String(n); S.workBdg.hidden = n === 0;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function boot() {
|
|
603
|
+
const root = document.getElementById('c3-root');
|
|
604
|
+
S.root = root;
|
|
605
|
+
if (DEMO) { const m = await import('./c3-demo.js'); m.installDemo(); S.demoMod = m; if (window.__c3demoReady) await window.__c3demoReady; if (!S.ws) S.ws = m.DEMO_WS; root.setAttribute('data-c3-demo', '1'); }
|
|
606
|
+
withWs();
|
|
607
|
+
ensureC3Css();
|
|
608
|
+
c3Phone(root);
|
|
609
|
+
const t = q.get('theme') || store.get(THEME_KEY) || 'light';
|
|
610
|
+
setTheme(t);
|
|
611
|
+
try { S.workspaces = (await pmGet('/api/workspaces')).workspaces || []; } catch { S.workspaces = []; }
|
|
612
|
+
if (!S.ws || !S.workspaces.some((w) => w.id === S.ws)) S.ws = (S.workspaces[0] && S.workspaces[0].id) || S.ws;
|
|
613
|
+
|
|
614
|
+
S.tickHost = el('div', { class: 'c3pg-tick', 'data-c3pg': 'ticker-host' });
|
|
615
|
+
S.pageView = el('div', { class: 'view' });
|
|
616
|
+
for (const [id] of TABS) { S.panes[id] = el('div', { class: 'c3pg-pane', role: 'tabpanel', id: 'c3pane-' + id, 'aria-labelledby': 'c3tab-' + id, hidden: true, 'data-c3-pane': id }); S.pageView.append(S.panes[id]); }
|
|
617
|
+
const main = el('div', { class: 'c3main' }, header(), S.tickHost, tabBar(), S.pageView);
|
|
618
|
+
S.grid = el('div', { class: 'c3grid' }, main, ...dock());
|
|
619
|
+
root.replaceChildren(S.grid);
|
|
620
|
+
|
|
621
|
+
S.dock.setWorkspaces(S.workspaces, S.ws);
|
|
622
|
+
mountBell(S.bellHost, { go: (_, t) => goTarget(t) });
|
|
623
|
+
briefHooks.person = openPerson;
|
|
624
|
+
briefHooks.go = (g) => { const t = { goinit: 'init', gorep: 'rep', godeliver: 'rep', gowork: 'work', gocal: 'cal' }[g.action]; if (t) showTab(t, true); };
|
|
625
|
+
document.addEventListener('keydown', onKey, true);
|
|
626
|
+
mountTicker(S.tickHost, { onOpen: () => openBellList() });
|
|
627
|
+
import('./shell-recording.js').then((m) => m.mountRecording(S.tools)).catch(() => { /* the recorder is not available on this page */ });
|
|
628
|
+
S.narrow = matchMedia('(max-width:980px)');
|
|
629
|
+
S.narrow.addEventListener('change', () => { S.wide = !S.narrow.matches; S.dockOpen = S.wide && S.tab === 'brief'; syncDock(); });
|
|
630
|
+
S.wide = !S.narrow.matches;
|
|
631
|
+
showTab(q.get('tab') || 'brief', false);
|
|
632
|
+
new ResizeObserver(moveInd).observe(S.tabNav);
|
|
633
|
+
workCount(); S.dock.start();
|
|
634
|
+
setInterval(() => { S.sub.textContent = dateLine(); }, 30000);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
boot().catch((e) => { const r = document.getElementById('c3-root'); if (r) r.textContent = 'The console could not start: ' + String((e && e.message) || e); });
|