create-pmos 0.493.0 → 0.495.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 +99 -27
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/briefing-premium-adddoc-test.js +2 -1
- package/workspace/briefing-premium-board-test.js +5 -4
- package/workspace/briefing-premium-initiative-test.js +4 -2
- package/workspace/changelog.json +98 -0
- package/workspace/guide/briefing-premium.md +4 -1
- package/workspace/guide/claude-proposes.md +2 -2
- package/workspace/guide/data-freshness.md +3 -1
- package/workspace/guide/initiatives-screen.md +3 -1
- package/workspace/guide/report-slides.md +14 -1
- package/workspace/guide/reports.md +2 -0
- package/workspace/guide/workflows.md +15 -3
- package/workspace/guide/your-impact.md +6 -7
- package/workspace/ini-workflow-test.js +2 -1
- package/workspace/lib/board-trail.js +30 -3
- package/workspace/lib/engine/workflows.js +19 -6
- package/workspace/lib/git-shape.js +21 -0
- package/workspace/lib/grill-runs.js +21 -6
- package/workspace/lib/grill.js +1 -1
- package/workspace/lib/impact.js +29 -16
- package/workspace/lib/initiative-cli.js +15 -1
- package/workspace/lib/open-mission.js +14 -0
- package/workspace/lib/page-warm.js +68 -0
- package/workspace/lib/report-design.js +6 -1
- package/workspace/lib/report-export.js +150 -9
- package/workspace/lib/report-pyramid.js +535 -0
- package/workspace/lib/report-structure.js +5 -1
- package/workspace/lib/report-templates.js +111 -20
- package/workspace/lib/routes-boards.js +36 -3
- package/workspace/lib/routes-brief-page.js +21 -3
- package/workspace/lib/routes-grill.js +10 -4
- package/workspace/lib/routes-impact.js +12 -4
- package/workspace/lib/routes-initiatives.js +1 -0
- package/workspace/lib/routes-programs.js +6 -4
- package/workspace/lib/routes-report-design.js +15 -0
- package/workspace/lib/routes-report-flow.js +115 -19
- package/workspace/lib/routes-report-setups.js +4 -2
- package/workspace/lib/routes-report-structure.js +3 -0
- package/workspace/lib/routes-workflows.js +6 -1
- package/workspace/lib/wf-bridge.js +5 -3
- package/workspace/lib/workflow-cli.js +2 -2
- package/workspace/lib/workflows-store.js +15 -1
- package/workspace/metric-drafts-ui-test.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-board-actions-test.js +2 -1
- package/workspace/premium-chat-report-tab-test.js +5 -1
- package/workspace/premium-vw-backlog-test.js +3 -2
- package/workspace/premium-vw-item-test.js +3 -2
- package/workspace/premium-vw-jira-test.js +3 -2
- package/workspace/program-panel-test.js +2 -1
- package/workspace/public/pm/cozy-home.css +1 -0
- package/workspace/public/pm/cozy-vw-rp.css +12 -0
- package/workspace/public/pm/fetch-share.js +38 -0
- package/workspace/public/pm/home-app.js +24 -4
- package/workspace/public/pm/home-chat.js +53 -11
- package/workspace/public/pm/home-impact.css +23 -13
- package/workspace/public/pm/home-impact.js +49 -32
- package/workspace/public/pm/home-ini.css +2 -0
- package/workspace/public/pm/ini-view.css +55 -0
- package/workspace/public/pm/ini-view.js +7 -4
- package/workspace/public/pm/premium-board.js +1 -0
- package/workspace/public/pm/premium-insights.js +1 -0
- package/workspace/public/pm/premium-report-read.js +25 -6
- package/workspace/public/pm/premium-report-tab.js +69 -14
- package/workspace/public/pm/premium-shell.js +57 -6
- package/workspace/public/pm/premium.css +1 -0
- package/workspace/public/pm/report-flow.js +93 -10
- package/workspace/public/pm/wf-page.css +8 -0
- package/workspace/public/pm/wf-page.js +33 -5
- package/workspace/server.js +24 -3
- package/workspace/skills/report-template.skill +10 -0
- package/workspace/split-merge-ui-test.js +3 -2
- package/workspace/stage-card-test.js +2 -1
- package/workspace/task-press-test.js +2 -1
- package/workspace/test-lists/page-open-speed.json +5 -0
- package/workspace/test-lists/report-pyramid.json +9 -0
- package/workspace/tk-tasks-test.js +2 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The models are in home-data.js and home-list.js (pure). This file reads the routes, draws, and wires the presses. Every write is the PM's
|
|
16
16
|
* press (pmPost). Nothing here opens the old dialog or links to the console. */
|
|
17
|
+
import './fetch-share.js'; // first: identical GETs in flight are asked once (page-open-speed)
|
|
17
18
|
import { mountNav } from './cozy-nav.js';
|
|
18
19
|
import { mountSetupChecklist } from './setup-checklist.js';
|
|
19
20
|
import { mountOnboard } from './cozy-onboard.js';
|
|
@@ -222,7 +223,8 @@ async function load() {
|
|
|
222
223
|
// an initiative of another product opened before its board was read (a link, a quick press): read it again from its own board
|
|
223
224
|
if (S.ini && !S.IM && otherIni(S.ini)) goIni(S.ini, { page: VW.isOpen() });
|
|
224
225
|
}
|
|
225
|
-
/* ── Home's one line (canvas FcPhone, Home: "only when something changed"): the newest grill this week that left findings, from GET /api/grill
|
|
226
|
+
/* ── Home's one line (canvas FcPhone, Home: "only when something changed"): the newest grill this week that left findings, from GET /api/grill,
|
|
227
|
+
* and how many other initiatives were grilled with it ("I grilled Inline login and 5 more this week: 21 findings.").
|
|
226
228
|
* "Checkout v2 will miss its date. I grilled it overnight: 3 findings." ✦ opens them on the initiative. Nothing to say is silence. ── */
|
|
227
229
|
async function loadGrillNews() {
|
|
228
230
|
const b0 = (S.D.board && S.D.board.board) || {};
|
|
@@ -236,6 +238,7 @@ function grillNews() {
|
|
|
236
238
|
const row = H.boardRows(S.D).find((r) => r.id === n.ini), title = (row && row.title) || n.ini;
|
|
237
239
|
const t = n.turn && GW.turnWord(n.turn.to), when = GW.whenWord(n.at), f = n.n + (n.n === 1 ? ' finding' : ' findings');
|
|
238
240
|
const text = t ? (n.turn.scope === 'goal' ? 'A goal ' + title + ' moves ' + t + '. I grilled it ' + when + ': ' + f + '.' : title + ' ' + t + (t === 'will miss' ? ' its date' : '') + '. I grilled it ' + when + ': ' + f + '.')
|
|
241
|
+
: n.more > 0 ? 'I grilled ' + title + ' and ' + n.more + ' more this week: ' + n.total + ' findings.'
|
|
239
242
|
: 'I grilled ' + title + ' ' + when + (GW.whyWord(n) ? ', ' + GW.whyWord(n) : '') + ': ' + f + '.';
|
|
240
243
|
return { id: n.ini, title, text, n: n.n };
|
|
241
244
|
}
|
|
@@ -840,7 +843,9 @@ function sessionTarget(s) {
|
|
|
840
843
|
// a program's or a product's own chat says so in its scope, never "Loose" (batch 22 item 17)
|
|
841
844
|
const pm = /^chat:program:([^:]+)/.exec(key || ''), pr = pm ? (S.D.programs || []).find((p) => p && p.id === pm[1]) : null;
|
|
842
845
|
const prod = !ini && !pr && /^product(:|$)/.test(key || '') ? H.productName(S.D) : '';
|
|
843
|
-
|
|
846
|
+
// a New workflow chat (Settings › Workflows) says so, never "Loose" (batch 24 item 4); its draft stays on the Workflows list
|
|
847
|
+
const wfNew = !ini && !pr && !prod && /^pop:workflow:/.test(key || '');
|
|
848
|
+
const home = pr ? { text: pr.title, icon: TD.P.layers } : prod ? { text: prod, icon: I.home } : wfNew ? { text: 'New workflow', icon: I.chat } : null;
|
|
844
849
|
const t = { title: H.nameOf(s), ses: s.id, sub: [role.name, ini ? iniTitle(ini) : home ? home.text : 'Loose'].join(' · '), ini, role, scope: ini ? scopeOf(ini) : home || { text: 'Loose', icon: I.chat, loose: true }, rich: rich(), to: rich() ? role : null, also: alsoOf(ini) };
|
|
845
850
|
if (key && /^(decision|blocked|waited|src|ev|work|board|init|doc|ins|chat|pop):/.test(key)) { t.key = key; t.sid = undefined; } else t.sid = s.id;
|
|
846
851
|
if (!t.key) t.sid = s.id;
|
|
@@ -1052,12 +1057,12 @@ function homeTarget() {
|
|
|
1052
1057
|
empty: 'Ask anything about your work. Claude reads your repositories and works in a real Claude Code session.', also: alsoOf(null),
|
|
1053
1058
|
intro: S.impact ? S.impact.intro : undefined };
|
|
1054
1059
|
}
|
|
1055
|
-
/* Your impact (batch 16 item 7):
|
|
1060
|
+
/* Your impact (batch 16 item 7; batch 23 item 4, IcA): while last week's check is open, Home's own chat opens on Claude's thank-you card.
|
|
1056
1061
|
* Off in Settings, or nothing to check: no card. Read after the list is drawn, so it never holds Home up. */
|
|
1057
1062
|
async function loadImpact() {
|
|
1058
1063
|
const c = await IMP.readCheck(pmGet).catch(() => null);
|
|
1059
1064
|
const was = !!S.impact;
|
|
1060
|
-
S.impact = c ? { c, intro: IMP.checkIntro(c, { esc, pmPost
|
|
1065
|
+
S.impact = c ? { c, intro: IMP.checkIntro(c, { esc, pmPost }) } : null;
|
|
1061
1066
|
const cur = chat.current && chat.current();
|
|
1062
1067
|
if ((was || S.impact) && cur && cur.key === 'chat:main') { chat.open(homeTarget()); }
|
|
1063
1068
|
if (was || S.impact) drawList();
|
|
@@ -1583,6 +1588,19 @@ function menCtx() {
|
|
|
1583
1588
|
drawList(); chat.focus();
|
|
1584
1589
|
} };
|
|
1585
1590
|
}
|
|
1591
|
+
/* A conversation about one item that names its own context (a report's chat icon, report-flow.js chatOf): Home's chat opens on it, the Viewer
|
|
1592
|
+
* stays beside it (on a phone the chat takes the page). Another page hands it over through sessionStorage with ?about=<its key>. */
|
|
1593
|
+
function chatAbout(t) {
|
|
1594
|
+
S.phoneChat = isPhone(); if (isPhone()) VW.close();
|
|
1595
|
+
openTarget({ ...homeTarget(), key: t.key, title: t.title, scope: { text: 'Report · ' + t.title, icon: I.chat }, intro: t.intro ? { role: CLAUDE, text: t.intro } : undefined,
|
|
1596
|
+
placeholder: t.placeholder || 'Message Claude', context: t.context });
|
|
1597
|
+
drawList(); chat.focus();
|
|
1598
|
+
}
|
|
1599
|
+
addEventListener('pmos-chat-about', (e) => { const t = e.detail; if (!t || !/^pop:[A-Za-z0-9][A-Za-z0-9._:-]{0,100}$/.test(String(t.key)) || !t.context) return; e.preventDefault(); chatAbout(t); });
|
|
1600
|
+
function aboutHanded(key) {
|
|
1601
|
+
let t = null; try { t = JSON.parse(sessionStorage.getItem('pmos-chat-about') || 'null'); sessionStorage.removeItem('pmos-chat-about'); } catch { return null; }
|
|
1602
|
+
return t && t.key === key && t.context ? t : null;
|
|
1603
|
+
}
|
|
1586
1604
|
function recLive() {
|
|
1587
1605
|
if (!REC) return null;
|
|
1588
1606
|
const h = REC.holding && REC.holding();
|
|
@@ -1949,6 +1967,7 @@ load().then(() => {
|
|
|
1949
1967
|
const say = (P.get('say') || P.get('q') || '').slice(0, 500);
|
|
1950
1968
|
const asked = !ask ? '' : /^board:/.test(ask) ? ask : /^(insights|verdict)$/.test(ask) ? 'ins:' + ask : 'init:' + ask;
|
|
1951
1969
|
const about = KEY.test(P.get('about') || '') ? P.get('about') : KEY.test(asked) ? asked : null;
|
|
1970
|
+
const handed = about && /^pop:/.test(about) ? aboutHanded(about) : null;
|
|
1952
1971
|
if (isPhone() && ((open && !blockOpen) || sid || about)) { S.phoneChat = true; drawList(); } // the conversation the address names, not the list
|
|
1953
1972
|
if (open && !blockOpen) {
|
|
1954
1973
|
// ?item=pt:<program>:<key> (the program panel's link to a program task) names the row by its id; Home lists no task, so the chat opens
|
|
@@ -1960,6 +1979,7 @@ load().then(() => {
|
|
|
1960
1979
|
else if (about && /^init:/.test(about) && iniRowOf(P.get('vid') || about.slice(5))) { const id = iniRowOf(P.get('vid')) ? P.get('vid') : about.slice(5); goIni(id).then(() => { if (P.get('vk') && S.ini === id && !isPhone()) iniOverview(); }); }
|
|
1961
1980
|
// the Board's program panel Discuss in chat names board:program:<id>: the program's own conversation, the one its tasks open (bug 2026-10-24)
|
|
1962
1981
|
else if (about && /^board:program:[A-Za-z0-9][A-Za-z0-9._-]{0,100}$/.test(about)) openTarget(programTarget(about.slice(14)));
|
|
1982
|
+
else if (handed) chatAbout(handed); // a report's chat icon on another page
|
|
1963
1983
|
else if (about) { openTarget({ ...homeTarget(), key: about, title: (P.get('label') || '').slice(0, 80) || about }); discussed(about, P.get('vid'), P.get('label')); }
|
|
1964
1984
|
else if (P.get('view') === 'metrics') openMetrics(P.get('ws') || ''); // ?view=metrics: an old link lands on Intelligence › Delivery trends
|
|
1965
1985
|
else if (S.ini) goIni(S.ini);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* started, POST /ask and /press) or a session with no chat key (GET /api/sessions/conversation?id=, POST /api/session/say and /keys).
|
|
16
16
|
* A loose session the host can place (host.suggestLink(target, firstText) → { id, title }) shows one chip: Link it (host.linkTo) or × (host.dismissLink).
|
|
17
17
|
* Every write is the PM's press. mountChat(el, host) returns { open(target), refresh(), focus(), send(text) }.
|
|
18
|
-
* target: { key?, sid?, title, scope: { text, icon, loose }, role: { name, initial }, intro?: { role, text, at, pills: [{ label, primary, go }] },
|
|
18
|
+
* target: { key?, sid?, title, scope: { text, icon, loose }, role: { name, initial }, intro?: { role, text, at, mood? (Adler as the avatar while the log is empty), pills: [{ label, primary, go }] },
|
|
19
19
|
* ini?, rich, also?: { text, go } }
|
|
20
20
|
* host: { esc, toast, pmGet, pmPost, roles(), seen(key), markSeen(key), onScope(btn), onLink(btn), onTo(btn), workItem(text, target),
|
|
21
21
|
* viewerClosed(), reopenViewer(), head(target), touch(sid) (the PM opened, typed in or answered that session: the Live pill) } */
|
|
@@ -58,7 +58,7 @@ export const adlerAvHtml = (mood) => `<span class="hm-av hm-adler" aria-hidden="
|
|
|
58
58
|
|
|
59
59
|
export function mountChat(el, host) {
|
|
60
60
|
const esc = host.esc;
|
|
61
|
-
const C = { t: null, th: null, conv: null, timer: null, show: 12, plOpen: false, plAll: false, open: new Set(), seq: 0, sending: false, files: [], seenAt: null, lastKey: null, picked: {}, auto: { key: null, ctx: new Set(), rep: null, goal: null }, slashAt: 0 };
|
|
61
|
+
const C = { t: null, th: null, conv: null, timer: null, show: 12, plOpen: false, plAll: false, open: new Set(), seq: 0, sending: false, files: [], seenAt: null, lastKey: null, picked: {}, pend: [], auto: { key: null, ctx: new Set(), rep: null, goal: null }, slashAt: 0 };
|
|
62
62
|
el.innerHTML = `<button type="button" class="hm-reopen" id="hmReopen" aria-label="Open the Viewer" title="Open the Viewer" hidden>${PANE}</button>`
|
|
63
63
|
+ '<div class="hm-chd" id="hmCh" hidden></div><div class="hm-sug" id="hmSug" hidden></div><div class="hm-plw" id="hmPl" hidden></div><div class="hm-log" id="hmLog" role="log" aria-live="polite"></div><div class="hm-foot" id="hmFoot"></div>';
|
|
64
64
|
const log = el.querySelector('#hmLog'), foot = el.querySelector('#hmFoot'), reopen = el.querySelector('#hmReopen'), head = el.querySelector('#hmCh'), sug = el.querySelector('#hmSug'), pl = el.querySelector('#hmPl');
|
|
@@ -138,11 +138,12 @@ export function mountChat(el, host) {
|
|
|
138
138
|
if (a.kind === 'reach' && R.stop && R.stop.reach && it.mi === R.lastMsg) return '';
|
|
139
139
|
return kindCardHtml(a, { live, role: R.role.name });
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
/** alone: nothing else in the log yet, so an intro with a mood shows Adler as Claude's avatar (one Adler: a reply takes him over). */
|
|
142
|
+
function introHtml(o, alone = true) {
|
|
142
143
|
if (!o) return '';
|
|
143
144
|
const r = o.role || { name: 'Claude', initial: 'C' };
|
|
144
145
|
const card = o.card && typeof o.card.html === 'function' ? o.card.html() : '';
|
|
145
|
-
return `<div class="hm-a">${avHtml(r)}<div class="hm-ab"><p class="hm-who"><b>${esc(r.name)}</b> <span class="hm-say">${esc(o.text)}</span></p>${card ? `<div class="hm-icard">${card}</div>` : ''}</div></div>`
|
|
146
|
+
return `<div class="hm-a">${o.mood && alone && isClaude(r) ? adlerAvHtml(o.mood) : avHtml(r)}<div class="hm-ab"><p class="hm-who"><b>${esc(r.name)}</b> <span class="hm-say">${esc(o.text)}</span></p>${card ? `<div class="hm-icard">${card}</div>` : ''}</div></div>`
|
|
146
147
|
+ (o.pills && o.pills.length ? `<div class="hm-pills">${o.pills.map((p, i) => `<button type="button" class="${p.primary ? 'pri' : ''}" data-hm-pill="${i}"${p.disabled ? ' disabled' : ''}>${esc(p.label)}</button>`).join('')}</div>` : '');
|
|
147
148
|
}
|
|
148
149
|
/** Claude's question card (AskUserQuestion), answerable until the PM writes again. */
|
|
@@ -174,6 +175,32 @@ export function mountChat(el, host) {
|
|
|
174
175
|
return q.filter((x) => !read(x)).map((x) => `<div class="hm-q"><div class="hm-qb">${userHtml(x.text)}</div><span class="hm-qn">${esc(role().name)} reads it after this tool call, in seconds<button type="button" class="hm-qx" data-hm-qdel="${esc(x.at)}" aria-label="Take it back" title="Take it back">${SVG(XX, 13)}</button></span></div>`).join('');
|
|
175
176
|
}
|
|
176
177
|
const sid = () => (C.t && C.t.sid) || (C.th && C.th.session) || null;
|
|
178
|
+
/* A sent message shows at once (batch 24 item 2: "enter the prompt is so long to appear on the chat"). Its bubble draws the moment the PM
|
|
179
|
+
* presses Enter, says "Sending…" until PMOS takes it, then "You · HH:MM". It leaves when the transcript holds the PM's turn (the same words,
|
|
180
|
+
* or any PM turn from that moment on), and after 3 minutes at most, so a bubble never stays as a ghost. A message that was not sent leaves at
|
|
181
|
+
* once and its words go back in the box. */
|
|
182
|
+
const PEND_MAX = 180000;
|
|
183
|
+
function settlePend() {
|
|
184
|
+
if (!C.pend.length) return;
|
|
185
|
+
const key = (t) => splitFiles(shownUser(t)).text.replace(/\s+/g, ' ').trim();
|
|
186
|
+
const users = ((C.conv && C.conv.messages) || []).filter((m) => m.role === 'user');
|
|
187
|
+
const lastUser = users.reduce((n, m) => Math.max(n, Date.parse(m.at) || 0), 0), now = Date.now();
|
|
188
|
+
// a message for a working session is queued by the host and drawn as "reads it after this tool call": the bubble gives way to it
|
|
189
|
+
const queued = new Set((host.queued ? host.queued(sid()) : []).map((x) => x.text));
|
|
190
|
+
C.pend = C.pend.filter((p) => {
|
|
191
|
+
if (!p.sent) return true;
|
|
192
|
+
if (queued.has(p.text)) return false;
|
|
193
|
+
const t0 = Date.parse(p.at);
|
|
194
|
+
if (now - t0 > PEND_MAX) return false;
|
|
195
|
+
if (lastUser && lastUser >= t0 - 2000) return false;
|
|
196
|
+
return !users.slice(p.n).some((m) => key(m.text) === key(p.text));
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function pendHtml() {
|
|
200
|
+
settlePend();
|
|
201
|
+
return C.pend.map((p) => `<div class="hm-u hm-pend${p.sent ? '' : ' sending'}"><div class="hm-bub">${filesHtml(p.files)}${p.text ? userHtml(p.text) : ''}</div>`
|
|
202
|
+
+ `<span class="hm-you" role="status">${p.sent ? 'You · ' + esc(hm(p.at)) : 'Sending…'}</span></div>`).join('');
|
|
203
|
+
}
|
|
177
204
|
// A loose chat that reads like one initiative gets one quiet chip at its top: "Looks like <initiative> · Link it ×" (XvALinkSuggest).
|
|
178
205
|
function drawSug() {
|
|
179
206
|
const t = C.t, g = t && t.scope && t.scope.loose && t.sid && host.suggestLink ? host.suggestLink(t, firstUser()) : null;
|
|
@@ -204,7 +231,7 @@ export function mountChat(el, host) {
|
|
|
204
231
|
const goal = C.th && C.th.goal;
|
|
205
232
|
const parts = items.map((it, i) => msgHtml(it, i, R));
|
|
206
233
|
placeDivider(parts, items, goal);
|
|
207
|
-
let html = (t.intro && !(t.intro.onlyEmpty && items.length) ? introHtml(t.intro) : '') + parts.join('') + askHtml(R) + (R.stop && R.lastAgent < 0 ? dividerHtml(R.stop) : '') + queuedHtml();
|
|
234
|
+
let html = (t.intro && !(t.intro.onlyEmpty && items.length) ? introHtml(t.intro, !items.length) : '') + parts.join('') + askHtml(R) + (R.stop && R.lastAgent < 0 ? dividerHtml(R.stop) : '') + pendHtml() + queuedHtml();
|
|
208
235
|
html += GU.choiceHtml(); // /goal while a goal works here: In a new chat, or Here instead
|
|
209
236
|
if (C.th && C.th.working) html += `<p class="hm-note" role="status">The agent is answering. You can write again when it has.</p>`;
|
|
210
237
|
if (C.conv && C.conv.state === 'ended') html += '<p class="hm-note">This session has ended. Your conversation is kept here, and your next message picks it up.</p>';
|
|
@@ -417,20 +444,31 @@ export function mountChat(el, host) {
|
|
|
417
444
|
async function send(text) {
|
|
418
445
|
const t = C.t; text = String(text || '').trim();
|
|
419
446
|
if (!t || (!text && !C.files.length) || C.sending) return false;
|
|
447
|
+
if (t.key && C.files.some((f) => f.state === 'saving')) { host.toast('A file is still being saved. Send again in a moment.'); return false; }
|
|
420
448
|
C.sending = true;
|
|
421
449
|
let r;
|
|
422
450
|
const was = running();
|
|
451
|
+
// the bubble first, then the server (batch 24 item 2): the files leave the box with the words, and come back if the send fails
|
|
452
|
+
const sentFiles = C.files;
|
|
453
|
+
const p = { text, files: sentFiles.map((f) => f.path || f.saved).filter(Boolean), at: new Date().toISOString(), sent: false, n: ((C.conv && C.conv.messages) || []).filter((m) => m.role === 'user').length };
|
|
454
|
+
C.pend.push(p); C.files = []; C.jump = true; draw();
|
|
423
455
|
if (t.key) {
|
|
424
|
-
|
|
425
|
-
const files = C.files.map((f) => f.saved);
|
|
456
|
+
const files = sentFiles.map((f) => f.saved);
|
|
426
457
|
r = await host.pmPost('/api/briefing-premium/ask' + (t.ws ? '?ws=' + encodeURIComponent(t.ws) : ''), files.length ? { key: t.key, text, files, ...(t.context ? { context: t.context } : {}) } : { key: t.key, text, ...(t.context ? { context: t.context } : {}) }).catch(() => null);
|
|
427
458
|
} else r = await host.pmPost('/api/session/say', { id: t.sid, text }).catch(() => null);
|
|
428
459
|
C.sending = false;
|
|
429
|
-
if (!r || r.ok !== true) {
|
|
460
|
+
if (!r || r.ok !== true) {
|
|
461
|
+
C.pend = C.pend.filter((x) => x !== p);
|
|
462
|
+
if (C.t === t && !C.files.length) C.files = sentFiles;
|
|
463
|
+
if (C.t === t) draw();
|
|
464
|
+
host.toast((r && r.error) || 'The message was not sent.'); return false;
|
|
465
|
+
}
|
|
430
466
|
const s = sid() || r.session;
|
|
431
467
|
if (host.touch) host.touch(s);
|
|
432
468
|
if (was && s && host.queue) host.queue(s, text);
|
|
433
|
-
|
|
469
|
+
p.sent = true;
|
|
470
|
+
sentFiles.forEach((f) => { if (f.url) URL.revokeObjectURL(f.url); }); C.jump = true;
|
|
471
|
+
if (C.t === t) draw();
|
|
434
472
|
if (t.onSent) t.onSent(r);
|
|
435
473
|
read();
|
|
436
474
|
return true;
|
|
@@ -526,7 +564,11 @@ export function mountChat(el, host) {
|
|
|
526
564
|
drawFoot({ role: role() });
|
|
527
565
|
}
|
|
528
566
|
};
|
|
529
|
-
el.addEventListener('submit', (e) => { e.preventDefault(); const ta = foot.querySelector('textarea'); const v = ta.value; if (GU.submit(v)) { ta.value = ''; grow(ta); return; }
|
|
567
|
+
el.addEventListener('submit', (e) => { e.preventDefault(); const ta = foot.querySelector('textarea'); const v = ta.value; if (GU.submit(v)) { ta.value = ''; grow(ta); return; }
|
|
568
|
+
// the box empties at once and the bubble shows (batch 24 item 2); a send that fails puts the words back, unless the PM typed again
|
|
569
|
+
if (!v.trim() && !C.files.length) return;
|
|
570
|
+
ta.value = ''; grow(ta);
|
|
571
|
+
send(v).then((ok) => { const box = foot.querySelector('textarea'); if (!ok && box && !box.value) { box.value = v; grow(box); } }); });
|
|
530
572
|
const shutAdd = (focus) => { if (!C.addOpen) return; C.addOpen = false; drawFoot({ role: role() }); if (focus) { const a = foot.querySelector('#hmAdd'); if (a) a.focus(); } };
|
|
531
573
|
document.addEventListener('click', (e) => { if (C.addOpen && e.target.isConnected && !e.target.closest('.hm-addw')) shutAdd(false); }, true);
|
|
532
574
|
el.addEventListener('keydown', (e) => {
|
|
@@ -622,7 +664,7 @@ export function mountChat(el, host) {
|
|
|
622
664
|
if (notesM) notesM.then((M) => setTimeout(() => M.syncNotes(), 0)); // the Notes open in the Viewer belonged to the conversation before
|
|
623
665
|
const same = C.t && t && C.t.key === t.key && C.t.sid === t.sid;
|
|
624
666
|
C.t = t;
|
|
625
|
-
if (!same) { C.addOpen = false; C.th = null; C.conv = null; C.show = 12; C.open = new Set(); C.files = []; C.marked = null; C.touched = null; C.picked = {}; C.plOpen = false; C.plAll = false; C.seenAt = t.key ? host.seen(t.key) : null; put(log, introHtml(t.intro)); put(pl, ''); pl.hidden = true; C.jump = true; peek(null); }
|
|
667
|
+
if (!same) { C.addOpen = false; C.th = null; C.conv = null; C.show = 12; C.open = new Set(); C.files = []; C.marked = null; C.touched = null; C.picked = {}; C.pend = []; C.plOpen = false; C.plAll = false; C.seenAt = t.key ? host.seen(t.key) : null; put(log, introHtml(t.intro)); put(pl, ''); pl.hidden = true; C.jump = true; peek(null); }
|
|
626
668
|
draw(); read();
|
|
627
669
|
},
|
|
628
670
|
refresh() { if (C.t) draw(); },
|
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
/* pm/home-impact.css — the weekly check of Your impact in Home's chat (
|
|
2
|
-
.im-card { clear: both; margin-top:
|
|
3
|
-
.im-list { list-style: none; margin: 0; padding: 0; }
|
|
4
|
-
.im-
|
|
5
|
-
.im-
|
|
6
|
-
.im-row.im-off .im-t { color: var(--cz-ink3); text-decoration: line-through; }
|
|
7
|
-
.im-k { width: 36px; height: 36px; border-radius: 10px; display: inline-flex; align-items: center; justify-content: center; flex: none; background: var(--cz-well); color: var(--cz-ink2); }
|
|
8
|
-
.im-k.im-thanks { background: var(--cz-bluebg); color: var(--cz-blueink); }
|
|
1
|
+
/* pm/home-impact.css — the weekly check of Your impact in Home's chat (batch 16 item 7; batch 23 item 4, canvas IcA). pm/home-impact.js. */
|
|
2
|
+
.im-card { clear: both; margin-top: 10px; max-width: 600px; } /* under Claude's line, never beside it (the line floats on a desk) */
|
|
3
|
+
.im-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
4
|
+
.im-q { position: relative; display: flex; align-items: flex-start; gap: 10px; padding: 12px 8px 12px 16px; border-radius: 18px; background: var(--cz-bluebg); }
|
|
5
|
+
.im-k { flex: none; display: inline-flex; margin-top: 2px; color: var(--cz-blueink); }
|
|
9
6
|
.im-tx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
10
|
-
.im-t { font-size: var(--fs-4); overflow-wrap: anywhere; }
|
|
11
|
-
.im-
|
|
7
|
+
.im-t { font-size: var(--fs-4); font-weight: 700; line-height: 1.35; color: var(--cz-ink); overflow-wrap: anywhere; }
|
|
8
|
+
.im-w { font-size: var(--fs-3); color: var(--cz-ink2); overflow-wrap: anywhere; }
|
|
9
|
+
.im-src { display: inline-flex; align-items: center; gap: 4px; margin-top: 2px; font-size: var(--fs-2); color: var(--cz-ink3); text-decoration: none; }
|
|
12
10
|
.im-src:hover { text-decoration: underline; }
|
|
13
11
|
.im-src svg, .im-lock svg { flex: none; }
|
|
14
|
-
.im-
|
|
15
|
-
.im-
|
|
16
|
-
|
|
12
|
+
.im-mw { position: relative; flex: none; }
|
|
13
|
+
.im-more { width: 28px; height: 28px; color: var(--cz-ink2); }
|
|
14
|
+
.ut-ib.im-more svg { stroke-width: 3.2; }
|
|
15
|
+
.im-pop { position: absolute; right: 0; top: 32px; z-index: 5; padding: 4px; border-radius: 14px; background: var(--cz-card); box-shadow: 0 14px 34px -14px rgba(42, 46, 53, .4), 0 0 0 1px var(--cz-hair); }
|
|
16
|
+
.im-pop button { display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px; border: 0; border-radius: 10px; background: none; color: var(--cz-ink); font: inherit; font-size: var(--fs-3); font-weight: 600; white-space: nowrap; cursor: pointer; }
|
|
17
|
+
.im-pop button:hover, .im-pop button:focus-visible { background: var(--cz-well); }
|
|
18
|
+
.im-pop svg { width: 15px; height: 15px; }
|
|
19
|
+
.im-q.im-gone { align-items: center; padding: 8px 8px 8px 16px; background: var(--cz-well); color: var(--cz-ink3); font-size: var(--fs-3); }
|
|
20
|
+
.im-gt { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
21
|
+
.im-undo { flex: none; height: 32px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: var(--cz-blueink); font: inherit; font-size: var(--fs-3); font-weight: 700; cursor: pointer; }
|
|
22
|
+
.im-undo:hover { background: var(--cz-card); }
|
|
23
|
+
.im-lock { display: flex; align-items: center; gap: 6px; margin: 10px 0 0; font-size: var(--fs-2); color: var(--cz-ink3); }
|
|
24
|
+
.im-msg { margin: 4px 0 0; font-size: var(--fs-2); color: var(--cz-ink3); }
|
|
25
|
+
.im-msg:empty { display: none; }
|
|
26
|
+
@media (prefers-reduced-motion: no-preference) { .im-q { transition: background-color .2s ease; } }
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
/* pm/home-impact.js — the weekly check of "Your impact" in Home's chat (feedback batch 16 item 7
|
|
2
|
-
* After a week ends, Claude
|
|
3
|
-
*
|
|
4
|
-
*
|
|
1
|
+
/* pm/home-impact.js — the weekly check of "Your impact" in Home's chat (feedback batch 16 item 7; rebuilt batch 23 item 4, canvas IcA).
|
|
2
|
+
* After a week ends, Claude shows that week's items as thank-you bubbles: the quote in bold, who and what, the source link. They count by
|
|
3
|
+
* default: nothing needs a ✓. One quiet ⋯ per row holds "Not right, drop it"; a dropped row says so with Undo. The lock line is a quiet
|
|
4
|
+
* footer. No number is shown. The card is Home's own chat intro for the whole week after (lib/impact.js: when it ends, rows nobody dropped
|
|
5
|
+
* are real); the first time it is drawn, the page says so (looked), and the row under Mentions goes.
|
|
5
6
|
*
|
|
6
7
|
* readCheck(pmGet) → the GET /api/impact/check body when a check is due, else null
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* partsOf(item) → { quote, what }: the bold line (the words in quotes, else the item's sentence) and the who/what line
|
|
9
|
+
* checkIntro(c, { esc, pmPost }) → the intro of Home's chat: { role, mood, text, card: { html, press } }
|
|
10
|
+
* rowHtml(c, { esc, sel }) → the one row under Mentions until the card is seen: a tap opens Home's chat on it (on a phone too). No count.
|
|
11
|
+
* Writes: POST /api/impact/verdict { id, real: false }, /undo { id }, /looked { week }. Nothing leaves PMOS. */
|
|
10
12
|
|
|
11
13
|
const SV = (d, w = 18) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
12
14
|
const P = {
|
|
@@ -14,7 +16,7 @@ const P = {
|
|
|
14
16
|
unblock: 'M7 11V8a5 5 0 0 1 9.6-2M5 11h14v9H5z',
|
|
15
17
|
used: 'M12 3l8 9-8 9-8-9z',
|
|
16
18
|
kr: 'M4 17l6-6 4 4 6-7M15 8h5v5',
|
|
17
|
-
no: 'M6 6l12 12M18 6L6 18', yes: 'M5 12l5 5 9-10',
|
|
19
|
+
no: 'M6 6l12 12M18 6L6 18', yes: 'M5 12l5 5 9-10', dots: 'M5 12h.01M12 12h.01M19 12h.01',
|
|
18
20
|
lock: 'M7 11V8a5 5 0 0 1 10 0v3M5 11h14v9H5z', open: 'M14 4h6v6M20 4l-9 9M18 14v6H4V6h6',
|
|
19
21
|
};
|
|
20
22
|
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
@@ -26,47 +28,62 @@ export async function readCheck(pmGet) {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function rowHtml(c, { esc, sel }) {
|
|
29
|
-
if (!c) return '';
|
|
31
|
+
if (!c || c.looked) return '';
|
|
30
32
|
return `<div class="hv-card hr-card hr-men-card"><div class="hm-r hv-r hr-men${sel ? ' sel' : ''}" data-id="impact">`
|
|
31
33
|
+ `<button type="button" class="hm-rm" data-hr-impact${sel ? ' aria-current="true"' : ''} aria-label="Your impact: ${esc(c.label.toLowerCase())} is ready to check">`
|
|
32
34
|
+ `<span class="hv-mk hr-at" aria-hidden="true">${SV(P.yes, 16)}</span><span class="hm-rt"><b>Your impact</b><small class="hr-sub">${esc(c.label)} is ready to check</small></span>`
|
|
33
35
|
+ '<span class="hv-xc" aria-hidden="true"></span></button></div></div>';
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const QUOTE = /[“"]([^”"]{2,})[”"]/;
|
|
39
|
+
/** The bold line is the thanks in the person's own words when the item quotes them; the line under it says who and what. */
|
|
40
|
+
export function partsOf(x) {
|
|
41
|
+
const t = String((x && x.text) || ''), who = String((x && x.who) || ''), m = QUOTE.exec(t);
|
|
42
|
+
if (!m) return { quote: t, what: who };
|
|
43
|
+
const rest = t.replace(m[0], ' ').replace(/^[\s:—–-]+|[\s:—–-]+$/g, '').replace(/\s+/g, ' ').trim();
|
|
44
|
+
return { quote: `“${m[1].trim()}”`, what: rest && rest !== who ? rest : who };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function checkIntro(c, { esc, pmPost }) {
|
|
48
|
+
const st = { c, msg: '', menu: null, told: !!c.looked };
|
|
39
49
|
const row = (x) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
const { quote, what } = partsOf(x);
|
|
51
|
+
const src = `<a class="im-src" href="${esc(x.source.url)}" target="_blank" rel="noopener noreferrer">${esc(x.source.label)} · ${esc(dayName(x.at))} ${SV(P.open, 11)}</a>`;
|
|
52
|
+
if (x.real === false) {
|
|
53
|
+
return `<li class="im-q im-gone"><span class="im-gt">Dropped, not counted: ${esc(quote)}</span>`
|
|
54
|
+
+ `<button type="button" class="im-undo" data-im-undo="${esc(x.id)}">Undo</button></li>`;
|
|
55
|
+
}
|
|
56
|
+
const open = st.menu === x.id;
|
|
57
|
+
return `<li class="im-q"><span class="im-k im-${esc(x.kind)}" aria-hidden="true">${SV(P[x.kind] || P.used)}</span>`
|
|
58
|
+
+ `<span class="im-tx"><span class="im-t">${esc(quote)}</span>${what ? `<span class="im-w">${esc(what)}</span>` : ''}${src}</span>`
|
|
59
|
+
+ `<span class="im-mw"><button type="button" class="ut-ib im-more" data-im-more="${esc(x.id)}" aria-haspopup="menu" aria-expanded="${open}" aria-label="More" title="More">${SV(P.dots, 16)}</button>`
|
|
60
|
+
+ (open ? `<span class="im-pop" role="menu"><button type="button" role="menuitem" data-im-drop="${esc(x.id)}">${SV(P.no, 15)} Not right, drop it</button></span>` : '')
|
|
61
|
+
+ '</span></li>';
|
|
46
62
|
};
|
|
47
63
|
const html = () => {
|
|
48
|
-
|
|
49
|
-
return `<div class="
|
|
64
|
+
if (!st.told) { st.told = true; st.c.looked = true; Promise.resolve(pmPost('/api/impact/looked', { week: st.c.week })).catch(() => {}); }
|
|
65
|
+
return `<div class="im-card" role="group" aria-label="Your impact, ${esc(st.c.label.toLowerCase())}">`
|
|
50
66
|
+ `<ul class="im-list">${st.c.items.map(row).join('')}</ul>`
|
|
51
|
-
+ `<
|
|
52
|
-
+
|
|
53
|
-
+ `</div><p class="ut-small ut-mut" role="status">${esc(st.msg)}</p></div>`;
|
|
67
|
+
+ `<p class="im-lock">${SV(P.lock, 13)} Only you see this. Nothing leaves PMOS.</p>`
|
|
68
|
+
+ `<p class="im-msg" role="status">${esc(st.msg)}</p></div>`;
|
|
54
69
|
};
|
|
55
70
|
const press = async (b) => {
|
|
56
71
|
const d = b.dataset;
|
|
72
|
+
if (d.imMore) { st.menu = st.menu === d.imMore ? null : d.imMore; return true; }
|
|
57
73
|
let r = null;
|
|
58
|
-
if (d.
|
|
74
|
+
if (d.imDrop) r = await pmPost('/api/impact/verdict', { id: d.imDrop, real: false });
|
|
59
75
|
else if (d.imUndo) r = await pmPost('/api/impact/undo', { id: d.imUndo });
|
|
60
|
-
else if ('imAll' in d) r = await pmPost('/api/impact/verdict-all', { week: st.c.week });
|
|
61
76
|
else return false;
|
|
77
|
+
st.menu = null;
|
|
62
78
|
if (!r || !r.ok) { st.msg = (r && r.error) || 'Not saved.'; return true; }
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
st.msg = left().length ? '' : 'Thank you. This week is checked.';
|
|
67
|
-
if (!left().length && done) done();
|
|
79
|
+
const x = st.c.items.find((y) => y.id === (d.imDrop || d.imUndo));
|
|
80
|
+
if (x) x.real = d.imDrop ? false : null;
|
|
81
|
+
st.msg = '';
|
|
68
82
|
return true;
|
|
69
83
|
};
|
|
70
|
-
const
|
|
71
|
-
|
|
84
|
+
const thanks = c.items.some((x) => x.kind === 'thanks');
|
|
85
|
+
const when = c.label === 'Last week' ? 'last week' : c.label.replace(/^Week of/, 'the week of');
|
|
86
|
+
const short = c.gate === 'short' ? ' Too many of these were not right, so I keep your sprout and your log hidden for now.' : '';
|
|
87
|
+
return { role: { key: 'claude', name: 'Claude', initial: 'C' }, mood: 'handled',
|
|
88
|
+
text: `${thanks ? 'People thanked you' : 'Your work moved things'} ${when}. I read these from your sources and counted them in your impact.${short}`, card: { html, press } };
|
|
72
89
|
}
|
|
@@ -126,6 +126,8 @@ button.hi-pill { cursor: pointer; }
|
|
|
126
126
|
/* ── the initiative's page in the Viewer (canvas HbCDesk, HbCNew, HbCPhone): the stage card, three tasks, the documents that need a look ── */
|
|
127
127
|
.hi-page { display: flex; flex-direction: column; gap: 14px; }
|
|
128
128
|
.hi-page .hm-pcard { margin: 0; }
|
|
129
|
+
/* a phone (batch 23 item 5, canvas PmA): the Viewer's own 16 px is the page's one inset */
|
|
130
|
+
@media (max-width: 760px) { .hi-v.hi-page { padding-left: 0; padding-right: 0; } }
|
|
129
131
|
.hi-pg { display: flex; flex-direction: column; gap: 2px; }
|
|
130
132
|
.hi-pgh { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 4px; }
|
|
131
133
|
.hi-pgh b { font-size: var(--fs-3); font-weight: 800; color: var(--cz-ink); }
|
|
@@ -1060,6 +1060,61 @@ button.iv-mt.iv-wf { padding: 0 !important; margin: 0 !important; }
|
|
|
1060
1060
|
.shut-mc .iv-tel { min-height: 0; }
|
|
1061
1061
|
.ab-sum .nm { display: none; }
|
|
1062
1062
|
}
|
|
1063
|
+
/* ── a phone (batch 23 item 5, canvas PmA, agreed 2026-11-02): one 16 px inset and no beige wrapper; a shorter mission; the way to finish and About
|
|
1064
|
+
* as one white card of 48 px rows (the next stage with its round →, which opens the Move sheet; the people and the program; About); 36 px pills in
|
|
1065
|
+
* the Tasks header; task rows 12 px in, and a stage's late and open counts on its dates line. A desktop is unchanged. ── */
|
|
1066
|
+
.iv-gx-t .tx.ph, .iv-ws .n-in { display: none; }
|
|
1067
|
+
@media (max-width: 760px) {
|
|
1068
|
+
.iv-mc { background: transparent; padding: 0; border-radius: 0; gap: 0; }
|
|
1069
|
+
.iv-mc-row { display: contents; }
|
|
1070
|
+
.iv-mc > .iv-mc-top { margin-bottom: 12px; border: 1px solid var(--cz-hair); }
|
|
1071
|
+
.iv-mc .iv-tel { min-height: 128px; border-radius: 22px; }
|
|
1072
|
+
.iv-mc:not(.shut-mc) .iv-tel { margin-bottom: 12px; }
|
|
1073
|
+
.iv-mc .iv-tel-ct { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 0 12px; padding: 12px 56px 12px 16px; }
|
|
1074
|
+
.iv-mc .iv-tel-t { display: contents; }
|
|
1075
|
+
.iv-mc .iv-tel-ct .iv-shn { flex-basis: 100%; min-height: 36px; }
|
|
1076
|
+
.iv-tel .iv-dtc { min-height: 24px; font-size: var(--fs-2); }
|
|
1077
|
+
.iv-tel .iv-setd { min-height: 36px; }
|
|
1078
|
+
.iv-ro { gap: 16px; padding-top: 0; }
|
|
1079
|
+
.iv-ro > div { flex-direction: row; align-items: baseline; gap: 4px; }
|
|
1080
|
+
.iv-ro small { font-size: var(--fs-2); font-weight: 700; letter-spacing: 0; }
|
|
1081
|
+
.iv-ro b { font-size: var(--fs-3); }
|
|
1082
|
+
.iv-ro b i { font-size: var(--fs-2); }
|
|
1083
|
+
/* the readouts on the dates line say where today is; the art's own Today label would sit on them */
|
|
1084
|
+
.iv-tel .mi-tx.now { display: none; }
|
|
1085
|
+
/* the one card: the way to finish on top, About under it */
|
|
1086
|
+
.iv-gx, .iv-ab { border: 1px solid var(--cz-hair); }
|
|
1087
|
+
.iv-gx { border-radius: 22px 22px 0 0; border-bottom: 0; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "t a" "l l" "f f"; }
|
|
1088
|
+
.iv-ab { border-radius: 0 0 22px 22px; }
|
|
1089
|
+
.iv-gx-t { padding: 4px 0 4px 10px !important; min-height: 56px; gap: 6px; }
|
|
1090
|
+
.iv-gx-t .chev { width: 16px; }
|
|
1091
|
+
.iv-gx[data-ph] .iv-gx-t .tx.dk, .iv-gx[data-ph] .iv-gx-c { display: none; }
|
|
1092
|
+
.iv-gx-t .tx.ph { display: flex; }
|
|
1093
|
+
.iv-gx-t .tx b { font-size: var(--fs-3); overflow: hidden; text-overflow: ellipsis; }
|
|
1094
|
+
.iv-gx-c { grid-area: a; }
|
|
1095
|
+
.iv-gx-a { padding: 0 8px 0 4px; }
|
|
1096
|
+
.iv-gx[data-ph] .iv-gx-a .iv-scgo { flex: none; width: 40px; height: 40px; padding: 0 !important; justify-content: center !important; }
|
|
1097
|
+
.iv-gx[data-ph] .iv-gx-a .iv-scgn { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
1098
|
+
.iv-gx-ft { padding: 0 8px; min-height: 48px; display: flex; align-items: center; }
|
|
1099
|
+
.iv-gx .iv-meta { flex: 1; }
|
|
1100
|
+
.iv-gx .iv-prog { margin-left: auto !important; }
|
|
1101
|
+
.iv-ab-h { min-height: 48px; padding: 0 14px !important; }
|
|
1102
|
+
/* Tasks: 36 px pills (a 44 px press under each), rows 12 px in, the counts on the dates line */
|
|
1103
|
+
.hi-v .iv-th button { min-height: 36px; height: 36px; position: relative; }
|
|
1104
|
+
.hi-v .iv-th button::after { content: ""; position: absolute; left: 0; right: 0; top: -4px; bottom: -4px; }
|
|
1105
|
+
.iv-ws { padding-left: 12px !important; }
|
|
1106
|
+
.iv-ws.in { padding-left: 12px !important; }
|
|
1107
|
+
.iv-ws > .n { display: none; }
|
|
1108
|
+
.iv-ws .n-in { display: inline; }
|
|
1109
|
+
.iv-ws .n-in.w { color: var(--cz-risk); font-weight: 700; }
|
|
1110
|
+
.iv-ws.pl > .iv-cv { margin-left: 40px; }
|
|
1111
|
+
.iv-wbs > .iv-ws + .iv-ws::before, .iv-wbs > .iv-wl + .iv-ws::before, .iv-wl > .iv-ws + .iv-ws::before, .iv-wl > .iv-wl + .iv-ws::before, .iv-wbs > * + :is(.iv-wsw, .ig-dz)::before, .iv-wbs > :is(.iv-wsw, .ig-dz) + .iv-ws::before { left: 12px; }
|
|
1112
|
+
.iv-wl { padding: 0 12px 6px 12px; }
|
|
1113
|
+
.iv-wl > .iv-ws.in { margin-left: -12px; width: calc(100% + 24px); }
|
|
1114
|
+
.iv-tr.iv-ed { margin: 0 -12px; padding: 10px 12px 12px !important; width: calc(100% + 24px); }
|
|
1115
|
+
.iv-addin { margin: 0 -12px; padding: 10px 12px 4px; }
|
|
1116
|
+
.iv-addd { margin: 0 -12px; padding: 4px 12px 12px; }
|
|
1117
|
+
}
|
|
1063
1118
|
html.ad-still .iv-mc-top.adl, html.ad-still .iv-mc-top.adl .iv-ask .ad-svg { animation: none !important; }
|
|
1064
1119
|
@media (prefers-reduced-motion: reduce) { .iv-mc-top.adl, .iv-mc-top.adl .iv-ask .ad-svg { animation: none !important; } }
|
|
1065
1120
|
|
|
@@ -75,11 +75,11 @@ const SHOWN = new Map(); // id → the view last drawn, for the Kanban's stage
|
|
|
75
75
|
const MCS = new Map(); // id → the PM's own changes to { mc, gx, ab } (true = open), kept in the browser
|
|
76
76
|
const MCK = 'pmos.iv.mc.';
|
|
77
77
|
const store = () => { try { return typeof localStorage !== 'undefined' ? localStorage : null; } catch { return null; } };
|
|
78
|
-
/** The three folds of one initiative: the PM's own, else the defaults (the way to finish is open on a wide screen, About is folded; on a phone both are folded; About opens by itself while a success measure waits on a ✓). */
|
|
78
|
+
/** The three folds of one initiative: the PM's own, else the defaults (the telemetry card starts hidden in its banner; the way to finish is open on a wide screen, About is folded; on a phone both are folded; About opens by itself while a success measure waits on a ✓). */
|
|
79
79
|
export function mcState(id, o = {}) {
|
|
80
80
|
let mine = MCS.get(id);
|
|
81
81
|
if (!mine) { mine = {}; try { const s = store(), j = s && JSON.parse(s.getItem(MCK + id) || 'null'); if (j && typeof j === 'object') for (const k of ['mc', 'gx', 'ab']) if (typeof j[k] === 'boolean') mine[k] = j[k]; } catch { mine = {}; } MCS.set(id, mine); }
|
|
82
|
-
const dflt = { mc:
|
|
82
|
+
const dflt = { mc: false, gx: !phone(), ab: !!o.proposal }; // the telemetry starts hidden in its banner (Wawan, batch 24 item 1)
|
|
83
83
|
return { mc: 'mc' in mine ? mine.mc : dflt.mc, gx: 'gx' in mine ? mine.gx : dflt.gx, ab: 'ab' in mine ? mine.ab : dflt.ab };
|
|
84
84
|
}
|
|
85
85
|
export function mcToggle(id, key, o) {
|
|
@@ -165,7 +165,10 @@ function gatesHtml(v, o, pri, open) {
|
|
|
165
165
|
const rows = G.open.length ? `<div class="iv-gx-l" role="list">${G.open.slice(0, 3).map(row).join('')}${G.open.length > 3 ? `<p class="iv-gx-dn">${G.open.length - 3} more in Tasks</p>` : ''}${G.done ? `<p class="iv-gx-dn">${plural(G.done, 'milestone', 'milestones')} done</p>` : ''}</div>`
|
|
166
166
|
: G.done ? `<div class="iv-gx-l"><p class="iv-gx-dn">${plural(G.done, 'milestone', 'milestones')} done</p></div>` : '';
|
|
167
167
|
const title = st.line && st.line.finished ? 'This stage is done' : 'To finish this stage';
|
|
168
|
-
|
|
168
|
+
// a phone (batch 23 item 5, canvas PmA) reads the row as the next stage and the count, with the round → beside it; the text above is the desktop's
|
|
169
|
+
const cnt = st.nTotal ? st.doneOf || st.nDone + ' of ' + st.nTotal + ' done' : '';
|
|
170
|
+
const ph = st.next ? `<span class="tx ph"><b>${esc('Next: ' + (st.next.ms ? '◆ ' : '') + st.next.name)}</b>${cnt ? `<span class="s">${esc(cnt)}</span>` : ''}</span>` : '';
|
|
171
|
+
return `<section class="iv-gx"${ph ? ' data-ph' : ''} aria-label="${esc(title)}"><button type="button" class="iv-gx-t" data-iv-mc="gx" aria-expanded="${!!open}"><span class="chev" aria-hidden="true">${SVG(CHEV, 16, 2)}</span><span class="tx dk"><b>${title}</b>${sub ? `<span class="s">${esc(sub)}</span>` : ''}</span>${ph}</button>`
|
|
169
172
|
+ `${st.nTotal ? `<span class="iv-gx-c">${pipsOf(st)}<small>${esc(st.doneOf || st.nDone + ' of ' + st.nTotal + ' done')}</small></span>` : ''}<div class="iv-gx-a">${nextHtml(st, o, pri)}</div>${open ? rows : ''}<div class="iv-gx-ft">${metaHtml(s, o)}</div></section>`;
|
|
170
173
|
}
|
|
171
174
|
function ctrlHtml(v, o = {}) {
|
|
@@ -305,7 +308,7 @@ function sectionHtml(v, sec, o, inner) {
|
|
|
305
308
|
const name = sec.label || sec.name;
|
|
306
309
|
const plus = st && op && has ? ED.plusHtml(sec, v.id || v.key || '', o) : '';
|
|
307
310
|
const row = `<button type="button" class="iv-ws${sec.kind === 'past' || sec.kind === 'later' ? ' mut' : ''}${inner ? ' in' : ''}${plus ? ' pl' : ''}"${st ? ` data-iv-sec="${sk}"` : ''} ${has ? `data-iv-fold="${esc(key)}" aria-expanded="${op}"` : 'disabled'}>${markOf(sec)}`
|
|
308
|
-
+ `<span class="w"><b>${esc(name)}</b>${sec.sub ? `<small class="${sec.subWarn ? 'w' : ''}">${esc(sec.sub)}</small>` : ''}</span>${sec.epicKey ? `<span class="ig-key">${esc(sec.epicKey)}</span>` : ''}${sec.count ? `<span class="n${sec.warn ? ' w' : ''}">${esc(sec.count)}</span>` : ''}${has ? chev(op) : ''}</button>`;
|
|
311
|
+
+ `<span class="w"><b>${esc(name)}</b>${sec.sub || sec.count ? `<small class="${sec.subWarn ? 'w' : ''}">${esc(sec.sub || '')}${sec.count ? `<span class="n-in${sec.warn ? ' w' : ''}">${sec.sub ? ' · ' : ''}${esc(sec.count)}</span>` : ''}</small>` : ''}</span>${sec.epicKey ? `<span class="ig-key">${esc(sec.epicKey)}</span>` : ''}${sec.count ? `<span class="n${sec.warn ? ' w' : ''}">${esc(sec.count)}</span>` : ''}${has ? chev(op) : ''}</button>`;
|
|
309
312
|
// a group's own presses sit beside its fold: ⋯ on the PM's own group, the send icon on an epic (batch 11 item 20)
|
|
310
313
|
const tools = G.toolsHtml(sec) + plus, head = tools ? `<div class="iv-wsw">${row}${tools}</div>` : row;
|
|
311
314
|
// the PM's own groups and Not in a group take a dragged task
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Workflows (D99, workflows-per-initiative: WfABoard WfABoardOne WfATrack WfAElse WfAPhone; pm/bd-wf.js): every initiative is on one workflow. The
|
|
18
18
|
* chip row picks one (its own stages become the columns and the track's header) or All (the three categories, each row its own stages).
|
|
19
19
|
* Where no record holds a value for an initiative, the element says so: nothing is invented. */
|
|
20
|
+
import './fetch-share.js'; // first: identical GETs in flight are asked once (page-open-speed)
|
|
20
21
|
import { pmPost } from './http.js';
|
|
21
22
|
import { followDoc } from './doc-follow.js';
|
|
22
23
|
import { $, $$, esc, store, chatAbout, setPageGaps } from './premium-shell.js';
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* IsPhone the goal detail full screen with Readings, Moved by and Dashboards; a report setup as a sheet.
|
|
29
29
|
* window.czViewer is feature-detected, with this page's side panel as the fallback. A report file opens in the report reader
|
|
30
30
|
* (pm/premium-report-read.js), the report panel in the Viewer card. The page has no #dlg. */
|
|
31
|
+
import './fetch-share.js'; // first: identical GETs in flight are asked once (page-open-speed)
|
|
31
32
|
import { $, $$, esc, toast, wireRail, setPageGaps } from './premium-shell.js';
|
|
32
33
|
import { pmPost, pmGet } from './http.js';
|
|
33
34
|
import * as GW from './grill-words.js'; // Grill and Forecast screens (canvas FcIntel): the goal that will miss, and what Claude's grill found
|