create-pmos 0.495.0 → 0.497.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 +57 -33
- package/payload/AGENTS.md +2 -0
- 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/workflow-state.js +7 -19
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/adler-moments-test.js +3 -1
- package/workspace/board-programs-test.js +6 -6
- package/workspace/board-shared-ui-test.js +2 -2
- package/workspace/briefing-premium-adddoc-test.js +1 -48
- package/workspace/briefing-premium-board-test.js +18 -19
- package/workspace/changelog.json +56 -0
- package/workspace/guide/adler.md +1 -1
- package/workspace/guide/briefing-premium.md +1 -0
- package/workspace/guide/claude-proposes.md +6 -2
- package/workspace/guide/data-freshness.md +7 -1
- package/workspace/guide/home.md +1 -1
- package/workspace/guide/published-board.md +1 -1
- package/workspace/guide/workflows.md +4 -3
- package/workspace/lib/board-premium.js +17 -2
- package/workspace/lib/engine/workflow-state.js +7 -19
- package/workspace/lib/initiative-chat.js +4 -1
- package/workspace/lib/proposals.js +18 -4
- package/workspace/lib/routes-board-premium.js +2 -2
- package/workspace/lib/routes-proposals.js +14 -2
- package/workspace/lib/split-merge-suggest.js +37 -3
- package/workspace/lib/split-merge.js +10 -4
- package/workspace/lib/task-cli.js +5 -1
- package/workspace/lib/vendor/scripts/workflow-state.js +7 -19
- package/workspace/lib/workflow-cli.js +70 -15
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-board-actions-test.js +7 -9
- package/workspace/premium-core-doc-issue-test.js +9 -38
- package/workspace/public/pm/chat-turn-end.js +19 -0
- package/workspace/public/pm/cozy-board.css +17 -5
- package/workspace/public/pm/home-app.js +21 -5
- package/workspace/public/pm/home-chat.js +5 -0
- package/workspace/public/pm/home-ini.js +7 -0
- package/workspace/public/pm/home-today.js +11 -4
- package/workspace/public/pm/load-shapes.css +57 -0
- package/workspace/public/pm/load-shapes.js +57 -0
- package/workspace/public/pm/premium-board.js +82 -57
- package/workspace/public/pm/readonly.js +3 -3
- package/workspace/public/pm/split-merge.js +3 -2
- package/workspace/public/premium-board.html +7 -10
- package/workspace/public/premium.html +1 -0
- package/workspace/split-merge-ui-test.js +1 -1
- package/workspace/test-lists/chat-turn-end.json +9 -0
- package/workspace/test-lists/page-open-speed.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Loading shapes (initiative page-open-speed; canvas boards SpA, SpD, SpG, SpJ, SpM, agreed A 2026-11-02).
|
|
2
|
+
* Honest state: grey shapes say only "something goes here" (no names, no counts); a hollow ring says Home is checking; nothing moves;
|
|
3
|
+
* no bar and no percent. Every shape is static (principle 12: no motion on data). */
|
|
4
|
+
|
|
5
|
+
/* the hollow ring beside "Good morning" while Home checks (SpA) */
|
|
6
|
+
.ls-ringdot { display: inline-block; flex: none; width: 9px; height: 9px; margin-left: 6px; border-radius: 50%; border: 1.6px solid var(--cz-blue); box-sizing: border-box; }
|
|
7
|
+
|
|
8
|
+
/* grey shapes */
|
|
9
|
+
.ls-sk { display: block; height: 9px; border-radius: 6px; background: var(--cz-well2); }
|
|
10
|
+
.ls-dot { flex: none; width: 12px; height: 12px; border-radius: 50%; background: var(--cz-well2); }
|
|
11
|
+
.ls-box { flex: none; width: 12px; height: 12px; border-radius: 6px; background: var(--cz-well2); }
|
|
12
|
+
.ls-row { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 12px; box-sizing: border-box; }
|
|
13
|
+
.ls-row + .ls-row { border-top: 1px solid var(--cz-hair); }
|
|
14
|
+
.ls-head { display: flex; align-items: center; min-height: 32px; padding: 0 12px; }
|
|
15
|
+
.ls-group + .ls-group { margin-top: 14px; }
|
|
16
|
+
.ls-men { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 12px; margin-bottom: 12px; border-radius: 8px; background: var(--cz-card); box-sizing: border-box; }
|
|
17
|
+
|
|
18
|
+
/* the last view, drawn at once while the fresh read runs (SpA): it can be read, not acted on; a press that acts is dashed */
|
|
19
|
+
.ls-stale { pointer-events: none; }
|
|
20
|
+
.ls-stale .hm-cta, .ls-stale .hv-cta, .ls-stale .solid, .ls-stale [data-ls-was].solid { background: transparent !important; color: var(--cz-ink3) !important; border: 1px dashed var(--cz-ink3) !important; box-shadow: none !important; }
|
|
21
|
+
.ls-stale .hr-bd { visibility: hidden; }
|
|
22
|
+
|
|
23
|
+
/* the Viewer's column held from frame one (SpJ): a dashed empty panel, the same width the Viewer opens at, no text, no icon */
|
|
24
|
+
.ls-vw { flex: none; border: 1.5px dashed var(--cz-hair); border-radius: 8px; box-sizing: border-box; width: min(500px, max(380px, calc(100vw - 808px))); }
|
|
25
|
+
@media (max-width: 760px) { .ls-vw { display: none; } }
|
|
26
|
+
|
|
27
|
+
/* the Board's skeleton (SpD): the real toolbar and tabs are the page's own; the body is grey rows shaped like programs and initiatives */
|
|
28
|
+
.ls-bd { display: flex; flex-direction: column; }
|
|
29
|
+
.ls-bd .ls-bh { display: grid; grid-template-columns: minmax(160px, 260px) 1fr minmax(120px, 220px); gap: 16px; align-items: center; min-height: 36px; padding: 0 14px; border-radius: 8px; background: var(--cz-well); }
|
|
30
|
+
.ls-bd .ls-stg { display: flex; justify-content: space-between; gap: 6px; }
|
|
31
|
+
.ls-bd .ls-stg .ls-sk { width: 28px; height: 7px; }
|
|
32
|
+
.ls-bd .ls-br { display: grid; grid-template-columns: minmax(160px, 260px) 1fr minmax(120px, 220px); gap: 16px; align-items: center; min-height: 46px; padding: 0 14px; }
|
|
33
|
+
.ls-bd .ls-br.pg { min-height: 40px; margin-top: 8px; border-radius: 8px; background: var(--cz-well); }
|
|
34
|
+
.ls-bd .ls-trk { position: relative; display: flex; align-items: center; justify-content: space-between; height: 12px; }
|
|
35
|
+
.ls-bd .ls-trk::before { content: ""; position: absolute; left: 4px; right: 4px; top: 5px; height: 2px; background: var(--cz-well2); }
|
|
36
|
+
.ls-bd .ls-trk i { position: relative; width: 7px; height: 7px; background: var(--cz-well2); }
|
|
37
|
+
.ls-bd .ls-trk i.ls-tdot { border-radius: 50%; }
|
|
38
|
+
.ls-bd .ls-trk i.on { width: 12px; height: 12px; }
|
|
39
|
+
.ls-slow { margin: 10px 14px 4px; font-size: var(--fs-2); font-weight: 700; color: var(--cz-ink3); }
|
|
40
|
+
@media (max-width: 760px) {
|
|
41
|
+
.ls-bd .ls-bh, .ls-bd .ls-br { grid-template-columns: 1fr; gap: 6px; }
|
|
42
|
+
.ls-bd .ls-bh .ls-stg, .ls-bd .ls-br .ls-trk, .ls-bd .ls-br > .ls-sk:last-child { display: none; }
|
|
43
|
+
}
|
|
44
|
+
.ls-stale > .hm-pg + .hm-pg { margin-top: 12px; }
|
|
45
|
+
.ls-stale > .hm-pg.loose { margin-top: 0; }
|
|
46
|
+
.ls-w30 { width: 30%; }
|
|
47
|
+
.ls-w34 { width: 34%; }
|
|
48
|
+
.ls-w38 { width: 38%; }
|
|
49
|
+
.ls-w40 { width: 40%; }
|
|
50
|
+
.ls-w55 { width: 55%; }
|
|
51
|
+
.ls-w60 { width: 60%; }
|
|
52
|
+
.ls-w70 { width: 70%; }
|
|
53
|
+
.ls-w75 { width: 75%; }
|
|
54
|
+
.ls-w48 { width: 48%; }
|
|
55
|
+
.ls-w54 { width: 54%; }
|
|
56
|
+
.ls-w62 { width: 62%; }
|
|
57
|
+
.ls-w78 { width: 78%; }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Loading shapes (initiative page-open-speed; canvas SpA, SpD, SpG, SpJ, SpM, agreed A 2026-11-02).
|
|
2
|
+
*
|
|
3
|
+
* ringHtml() the hollow ring beside "Good morning" while Home checks
|
|
4
|
+
* listShapes() Home's list as grey rows, before the first read and with no last view kept
|
|
5
|
+
* menShape() the Mentions row's grey shape, until Mentions is read
|
|
6
|
+
* keepLast(key, html) keep the list Home drew last in this browser (not over 300 KB), for the next open
|
|
7
|
+
* last(key) that list, or ''
|
|
8
|
+
* holdViewer(host, on) the Viewer's column as a dashed empty panel, so the chat never narrows when the Viewer opens
|
|
9
|
+
* slowLine(host, M, ms) 'Still reading the board.' once the wait runs past ms, while there is still no model
|
|
10
|
+
*
|
|
11
|
+
* Grey shapes carry no names and no counts, and nothing moves. The last view is drawn inert: it can be read, not acted on, until the
|
|
12
|
+
* fresh read is in. */
|
|
13
|
+
|
|
14
|
+
const store = {
|
|
15
|
+
get(k) { try { return localStorage.getItem(k); } catch { return null; } },
|
|
16
|
+
set(k, v) { try { if (v == null) localStorage.removeItem(k); else localStorage.setItem(k, v); } catch { /* a private window, or full */ } },
|
|
17
|
+
};
|
|
18
|
+
const LAST = 'pmos.home.last:';
|
|
19
|
+
const MAX = 300000;
|
|
20
|
+
|
|
21
|
+
export const ringHtml = () => '<span class="ls-ringdot" role="img" aria-label="Checking"></span>';
|
|
22
|
+
|
|
23
|
+
const W = [62, 78, 54, 70, 48];
|
|
24
|
+
const row = (w, box) => `<div class="ls-row">${box ? '<i class="ls-box"></i>' : '<i class="ls-dot"></i>'}<i class="ls-sk ls-w${w}"></i></div>`;
|
|
25
|
+
export function listShapes() {
|
|
26
|
+
return '<div class="ls-list" aria-hidden="true">'
|
|
27
|
+
+ `<div class="ls-group"><div class="ls-head"><i class="ls-sk ls-w38"></i></div><div class="hv-card">${W.map((w, i) => row(w, i > 0)).join('')}</div></div>`
|
|
28
|
+
+ `<div class="ls-group"><div class="ls-head"><i class="ls-sk ls-w30"></i></div><div class="hv-card">${W.slice(0, 3).map((w) => row(w, true)).join('')}</div></div>`
|
|
29
|
+
+ '</div>';
|
|
30
|
+
}
|
|
31
|
+
export const menShape = () => '<div class="ls-men" aria-hidden="true"><i class="ls-dot"></i><i class="ls-sk ls-w34"></i></div>';
|
|
32
|
+
|
|
33
|
+
const kept = new Map(); // what this page wrote last, so a redraw that changed nothing writes nothing
|
|
34
|
+
export function keepLast(key, html) { if (kept.get(key) === html) return; kept.set(key, html); store.set(LAST + key, html && html.length <= MAX ? html : null); }
|
|
35
|
+
/** The last view, drawn as a picture of itself: no button, link or field in it, so nothing in it can be pressed or found as a control
|
|
36
|
+
* until the fresh read is in (SpA: it can be read, not acted on). */
|
|
37
|
+
export function last(key) { return inert(store.get(LAST + key) || ''); }
|
|
38
|
+
/** html with its buttons, links and fields drawn as spans, and its press marks renamed: seen, not pressed, until the fresh read is in. */
|
|
39
|
+
export function inert(html) {
|
|
40
|
+
return String(html || '').replace(/<(\/?)(button|a|input|select|textarea)\b/gi, (m, end, tag) => '<' + end + 'span' + (end ? '' : ' data-ls-was="' + tag.toLowerCase() + '"'))
|
|
41
|
+
.replace(/\sdata-(hm|hr)-/g, ' data-ls-'); // and without the presses' own marks, so nothing reads it as the live list
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** on: a dashed empty panel in the host (once); off: the panel goes. */
|
|
45
|
+
export function holdViewer(host, on) {
|
|
46
|
+
if (!host) return;
|
|
47
|
+
const was = host.querySelector(':scope > .ls-vw');
|
|
48
|
+
if (on && !was) { const p = document.createElement('div'); p.className = 'ls-vw'; p.setAttribute('aria-hidden', 'true'); host.prepend(p); }
|
|
49
|
+
if (!on && was) was.remove();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The Board's quiet line past ms of waiting (SpD): it says only what is true, and never shows once the board is in (M() is set). */
|
|
53
|
+
export function slowLine(host, M, ms = 2000) {
|
|
54
|
+
const show = () => { if (M() || !host || !host.querySelector('.ls-bd') || host.querySelector('.ls-slow')) return; host.insertAdjacentHTML('afterbegin', '<p class="ls-slow" role="status">Still reading the board.</p>'); };
|
|
55
|
+
const left = ms - (typeof performance !== 'undefined' ? performance.now() : 0);
|
|
56
|
+
if (left <= 0) show(); else setTimeout(show, left);
|
|
57
|
+
}
|
|
@@ -55,7 +55,8 @@ import { statusOf as ivStatus } from './ini-view-model.js';
|
|
|
55
55
|
import { initiativeRename, taskRename } from './rename-press.js';
|
|
56
56
|
import { setSay } from './cozy-xv.js';
|
|
57
57
|
import { narrowForm } from './pn-narrow.js';
|
|
58
|
-
import * as BP2 from './bd-pane.js';
|
|
58
|
+
import * as BP2 from './bd-pane.js';
|
|
59
|
+
import * as LS from './load-shapes.js'; // the skeleton and the slow-case line while the board is read (page-open-speed, SpD) // the Board as the first panel: fit(), the rows that open beside, the open row's mark
|
|
59
60
|
|
|
60
61
|
const VIEW_KEY = 'pmos.board.view', PVIEW_KEY = 'pmos.board.pview', SPAN_KEY = 'pmos.board.span', GROUPING_KEY = 'pmos.board.grouping';
|
|
61
62
|
let M = null; // the model
|
|
@@ -68,6 +69,8 @@ let span = SPANS.some(([k]) => k === store.get(SPAN_KEY)) ? store.get(SPAN_KEY)
|
|
|
68
69
|
let tab = 'ini';
|
|
69
70
|
let SH_N = 0; let SH_FULL = false; let SH_LOADED = false; // the Shared tab's number, whether it has read the teams, whether the first count was asked
|
|
70
71
|
let readTimer = null;
|
|
72
|
+
const SKEL = $('#ibBody').innerHTML; // the grey rows the page opens with (premium-board.html), put back if a failure line replaced them
|
|
73
|
+
LS.slowLine($('#ibBody'), () => M);
|
|
71
74
|
let readTries = 0; // reads that answered "reading" in a row: the next ask waits longer each time
|
|
72
75
|
let foldOpen = false; // the Monitor and Evaluate rows under the table
|
|
73
76
|
let sprintSet = false; // "This sprint" starts on, once, when the board has a sprint
|
|
@@ -101,7 +104,7 @@ const I = {
|
|
|
101
104
|
flag: 'M5 21V4M5 4h11l-2 4 2 4H5', note: 'M4 5h16v11h-9l-5 4v-4H4z', spark: 'M12 3l1.8 4.7L18.5 9.5l-4.7 1.8L12 16l-1.8-4.7L5.5 9.5l4.7-1.8z',
|
|
102
105
|
doc: 'M6 3h9l4 4v14H6zM14 3v5h5', link: 'M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1', chev: 'M7 10l5 5 5-5', chevUp: 'M7 14l5-5 5 5',
|
|
103
106
|
move: 'M4 12h12M12 6l6 6-6 6M20 5v14', hold: 'M9 5v14M15 5v14', archive: 'M4 5h16v4H4zM6 9v10h12V9M10 13h4',
|
|
104
|
-
track: 'M3 5h18v4H3zM3 11h18v4H3zM3 17h12v3H3z', time: 'M4 6h9M8 12h11M6 18h7',
|
|
107
|
+
track: 'M3 5h18v4H3zM3 11h18v4H3zM3 17h12v3H3z', time: 'M4 6h9M8 12h11M6 18h7', info: 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18M12 11v5M12 8h.01',
|
|
105
108
|
};
|
|
106
109
|
const plural = (n, a, b) => (n === 1 ? a : b);
|
|
107
110
|
const ago = (iso, short) => { const t = Date.parse(iso || ''); if (!t) return ''; const m = Math.max(0, Math.round((Date.now() - t) / 60000)); return m < 1 ? 'just now' : m < 60 ? m + ' min' + (short ? '' : ' ago') : m < 60 * 36 ? Math.round(m / 60) + ' h' + (short ? '' : ' ago') : Math.round(m / 1440) + ' days' + (short ? '' : ' ago'); };
|
|
@@ -242,52 +245,77 @@ function drawHead() {
|
|
|
242
245
|
setPageGaps(((M.header && M.header.missing) || []).length);
|
|
243
246
|
}
|
|
244
247
|
let jiraReading = false; // a press is reading Jira again: the chip's Adler wears his glasses until the next read of the Board lands
|
|
245
|
-
const CHIP_F = [['late', 'late', 'm-warn',
|
|
248
|
+
const CHIP_F = [['late', 'late', 'm-warn', 'Late'], ['risk', 'risk', 'm-warn', 'At risk'], ['blocked', 'red', 'm-crit', 'Blocked'], ['idle', 'q', '', 'Idle 30+ days']];
|
|
246
249
|
/** The sprint filter (the "Sprint N only" chip and Filter's Sprint group) shows on the Sprint span only among the timeline's spans (TsSprintFilter);
|
|
247
250
|
* the stage track keeps its chip. */
|
|
248
251
|
const curView = () => (PHONE.matches ? pview : narrow() ? nview : view);
|
|
249
252
|
const sprintFilterOn = () => curView() !== 'time' || span === 'sprint';
|
|
253
|
+
/** What needs a look (batch 24 item 7, canvas BbB): new asks, late, at risk, blocked and idle are ONE pill of dots and numbers. A press opens the
|
|
254
|
+
* rows; a row opens what its chip opened (the asks, a metric's trend, whose "Show only" filters); a filter that is on turns off at once.
|
|
255
|
+
* A row that is zero is not listed, and the pill is not drawn when every row is zero (zero is silence). */
|
|
256
|
+
function needHtml(C, open) {
|
|
257
|
+
const rows = [];
|
|
258
|
+
if (C.asks) rows.push({ k: 'asks', dot: 'you m-info', n: C.asks, name: plural(C.asks, 'New ask', 'New asks'), say: C.asks + ' new ' + plural(C.asks, 'ask', 'asks') });
|
|
259
|
+
for (const [k, tone, mo, name] of CHIP_F) if (C[k]) rows.push({ k, dot: tone + (mo ? ' ' + mo : ''), n: C[k], name, say: C[k] + ' ' + name.toLowerCase() });
|
|
260
|
+
if (!rows.length) return '';
|
|
261
|
+
const on = rows.some((r) => F.chips.has(r.k));
|
|
262
|
+
const item = (r) => (r.k === 'asks'
|
|
263
|
+
? `<button type="button" class="bd-nr" id="chAsks" aria-haspopup="dialog"><i class="cz-dot ${r.dot}"></i>${esc(r.name)}<span class="k">${r.n}</span></button>`
|
|
264
|
+
: `<button type="button" class="bd-nr${F.chips.has(r.k) ? ' on' : ''}" data-f="${r.k}" aria-pressed="${F.chips.has(r.k)}"><i class="cz-dot ${r.dot}"></i>${esc(r.name)}<span class="k">${r.n}</span></button>`);
|
|
265
|
+
return `<span class="bd-grpw2 bd-needw" id="bdNeedw"><button type="button" class="bd-chip bd-need${on ? ' on' : ''}" id="chNeed" aria-haspopup="true" aria-expanded="${open}" aria-controls="bdNeedMenu" aria-label="${esc(rows.map((r) => r.say).join(', '))}. Show them" title="${esc(rows.map((r) => r.say).join(' · '))}">`
|
|
266
|
+
+ rows.map((r) => `<span><i class="cz-dot ${r.dot}"></i>${r.n}</span>`).join('') + SV(I.chev) + '</button>'
|
|
267
|
+
+ `<span class="bd-grpm bd-needm" id="bdNeedMenu" role="group" aria-label="Needs a look"${open ? '' : ' hidden'}><span class="bd-mlbl">Needs a look</span>${rows.map(item).join('')}</span></span>`;
|
|
268
|
+
}
|
|
269
|
+
const menuOpen = (id) => !!$('#' + id) && !$('#' + id).hidden;
|
|
250
270
|
function drawChips() {
|
|
251
271
|
const J = M.jira || {}, C = M.chips || {}, sp = J.sprint, rel = J.release, phone = small();
|
|
252
|
-
const jira = J.readAt ?
|
|
272
|
+
const jira = J.readAt ? 'Jira · read ' + ago(J.readAt) : 'Jira · not read';
|
|
253
273
|
const sprint = [sp ? sp.name : null, rel ? rel.name + ' ships ' + rel.at.replace(/^(\d+) (\w+)$/, '$2 $1') : null].filter(Boolean).join(' · ') || 'No sprint on record';
|
|
254
|
-
const asks = C.asks == null ? 'New asks · not read' : null;
|
|
255
|
-
const flt = (keys) => CHIP_F.filter(([k]) => keys.includes(k)).map(([k, tone, mo, word]) => (C[k] ? `<button type="button" class="bd-chip gh${F.chips.has(k) ? ' on' : ''}" data-f="${k}" aria-pressed="${F.chips.has(k)}"><i class="cz-dot ${tone} ${mo}"></i>${esc(word(C[k]))}</button>` : '')).join('');
|
|
256
274
|
const thisSprint = sp && (view === 'track' || view === 'time' || phone) && sprintFilterOn() ? `<button type="button" class="bd-chip${F.chips.has('sprint') ? ' on' : ''}" data-f="sprint" aria-pressed="${F.chips.has('sprint')}" title="${F.chips.has('sprint') ? 'Shows only the initiatives with work in ' + esc(sp.name) + '. Press to show all.' : 'Press to show only the initiatives with work in ' + esc(sp.name) + '.'}">${esc(sp.name)} only</button>` : ''; // it named no sprint and read like a heading (testing fixes)
|
|
257
|
-
// All products (PgBoard): one Jira
|
|
258
|
-
// AdM2:
|
|
259
|
-
// a press reads it again. Settings > Adler > Off keeps the refresh icon.
|
|
275
|
+
// All products (PgBoard): one Jira control with the oldest read of any product, and every product's new asks; the sprint stays per product
|
|
276
|
+
// AdM2: Adler's S face carries the read's mood: calm when Jira was read within a day and a half, honest when it was not, glasses on while
|
|
277
|
+
// a press reads it again. Settings > Adler > Off keeps the refresh icon. Batch 24 item 7 (BbB): the face alone, an amber pip when the read is
|
|
278
|
+
// old, the words on hover (and for a screen reader).
|
|
260
279
|
const jMood = jiraReading ? 'thinking' : J.readAt && Date.now() - Date.parse(J.readAt) < 36 * 3600000 ? 'handled' : 'honest';
|
|
261
|
-
const
|
|
262
|
-
const
|
|
280
|
+
const jSay = jira + '. ' + (ALL ? 'Press to read Jira again for every product' : 'Press to read Jira again');
|
|
281
|
+
const jiraChip = `<button type="button" class="bd-chip bd-jf" id="chJira" title="${esc(jSay)}" aria-label="${esc(jSay)}">${faceOr(jMood, 's', SV(I.refresh), { label: false })}${jMood === 'honest' ? '<i class="bd-pip" aria-hidden="true"></i>' : ''}<span class="vh">${esc(jira)}</span></button>`;
|
|
282
|
+
const need = needHtml(C, menuOpen('bdNeedMenu'));
|
|
263
283
|
if (phone) {
|
|
264
|
-
|
|
265
|
-
$('#bdChipset').innerHTML = thisSprint + jiraChip + askChip + flt(['late', 'risk', 'blocked', 'idle']) + `<button type="button" class="bd-chip" id="chDocs">Docs ${docs}</button><button type="button" class="bd-chip" id="chShared">Shared${SH_N ? ' ' + SH_N : ''}</button>`;
|
|
284
|
+
$('#bdChipset').innerHTML = thisSprint + jiraChip + need + `<button type="button" class="bd-chip" id="chShared">Shared${SH_N ? ' ' + SH_N : ''}</button>`;
|
|
266
285
|
return;
|
|
267
286
|
}
|
|
268
|
-
// the timeline's legend
|
|
269
|
-
// where a hold is drawn (feedback batch 9 item 8: zero is silence)
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
+
|
|
273
|
-
|
|
274
|
-
+
|
|
287
|
+
// the timeline's legend (PgTimeline) goes behind an info icon at the end of the row (BbB), with a program milestone only where a program is drawn
|
|
288
|
+
// and "On hold" only where a hold is drawn (feedback batch 9 item 8: zero is silence)
|
|
289
|
+
const lo = menuOpen('bdLgdMenu');
|
|
290
|
+
const lgd = view === 'time' && span !== 'sprint' ? `<span class="bd-grpw2 bd-lgdw"><button type="button" class="bd-ib bd-lgdb" id="bdLgdBtn" aria-haspopup="true" aria-expanded="${lo}" aria-controls="bdLgdMenu" aria-label="What the bars mean" title="What the bars mean">${SV(I.info)}</button>`
|
|
291
|
+
+ `<span class="bd-grpm bd-lgdm" id="bdLgdMenu"${lo ? '' : ' hidden'}><span class="bd-mlbl">What the bars mean</span>${legendHtml(!byProduct && !!groups(visible()), hasHold(visible()), { ...planKeys(visible()), ...gapKeys(visible()), range: visible().some((r) => fcRange(r)) })}</span></span>` : '';
|
|
292
|
+
$('#bdChipset').innerHTML = (ALL ? '' : `<button type="button" class="bd-chip" id="chSprint" aria-haspopup="dialog">${SV(I.cal)}<span>${esc(sprint)}</span></button>`)
|
|
293
|
+
+ thisSprint + '<span class="bd-sp"></span>' + need + lgd + jiraChip;
|
|
275
294
|
}
|
|
295
|
+
const closeMenu = (id, btn) => { const m = $('#' + id); if (m && !m.hidden) { m.hidden = true; const b = $('#' + btn); if (b) b.setAttribute('aria-expanded', 'false'); return true; } return false; };
|
|
296
|
+
const toggleMenu = (id, btn) => { const m = $('#' + id), o = m.hidden; m.hidden = !o; $('#' + btn).setAttribute('aria-expanded', String(o)); if (o) { const f = m.querySelector('button'); if (f) f.focus(); } };
|
|
276
297
|
$('#bdChipset').addEventListener('click', (e) => {
|
|
298
|
+
if (e.target.closest('#bdLgdBtn')) { closeMenu('bdNeedMenu', 'chNeed'); toggleMenu('bdLgdMenu', 'bdLgdBtn'); return; }
|
|
299
|
+
if (e.target.closest('#chNeed')) { closeMenu('bdLgdMenu', 'bdLgdBtn'); toggleMenu('bdNeedMenu', 'chNeed'); return; }
|
|
277
300
|
const f = e.target.closest('[data-f]');
|
|
278
301
|
if (f) {
|
|
279
|
-
const k = f.dataset.f;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (F.chips.has(k))
|
|
302
|
+
const k = f.dataset.f, inNeed = !!f.closest('#bdNeedMenu');
|
|
303
|
+
if (inNeed) closeMenu('bdNeedMenu', 'chNeed');
|
|
304
|
+
// a metric row opens its trend (BdPanel), whose "Show only" filters; a row that is on turns off at once
|
|
305
|
+
if (k !== 'sprint' && !F.chips.has(k)) { Acts.open.trend(k, inNeed ? $('#chNeed') : f); return; }
|
|
306
|
+
if (F.chips.has(k)) F.chips.delete(k); else F.chips.add(k); drawChips(); drawInit(); if (inNeed) { const b = $('#chNeed') || $('#bdFilter'); b.focus(); } return;
|
|
283
307
|
}
|
|
284
308
|
const b = e.target.closest('button'); if (!b) return;
|
|
285
309
|
if (b.id === 'chJira') refreshJira(b);
|
|
286
310
|
else if (b.id === 'chSprint') Acts.open.sprint(b);
|
|
287
|
-
else if (b.id === 'chAsks') Acts.open.asks(
|
|
288
|
-
else if (b.id === 'chDocs') setTab('docs');
|
|
311
|
+
else if (b.id === 'chAsks') { closeMenu('bdNeedMenu', 'chNeed'); Acts.open.asks($('#chNeed')); }
|
|
289
312
|
else if (b.id === 'chShared') setTab('shared');
|
|
290
313
|
});
|
|
314
|
+
$('#bdChipset').addEventListener('keydown', (e) => {
|
|
315
|
+
if (e.key !== 'Escape') return;
|
|
316
|
+
if (closeMenu('bdNeedMenu', 'chNeed')) { e.stopPropagation(); $('#chNeed').focus(); } else if (closeMenu('bdLgdMenu', 'bdLgdBtn')) { e.stopPropagation(); $('#bdLgdBtn').focus(); }
|
|
317
|
+
});
|
|
318
|
+
document.addEventListener('click', (e) => { if (!e.target.closest('#bdNeedw')) closeMenu('bdNeedMenu', 'chNeed'); if (!e.target.closest('.bd-lgdw')) closeMenu('bdLgdMenu', 'bdLgdBtn'); });
|
|
291
319
|
/** Read Jira again: the Jira source's own refresh (POST /api/sources/refresh), the one the Sources list runs. It starts a read and changes no record. */
|
|
292
320
|
async function refreshJira(btn) {
|
|
293
321
|
if (btn) btn.disabled = true;
|
|
@@ -517,9 +545,8 @@ function drawInit() {
|
|
|
517
545
|
$$('#ibTabs [data-v]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
|
|
518
546
|
{ const sb = $('#bdSort'), Ls = SO.labels(); if (sb) { sb.setAttribute('aria-label', Ls.aria); sb.title = Ls.title; sb.classList.toggle('on', Ls.on); } }
|
|
519
547
|
$('#bdView').innerHTML = SV(v === 'time' ? I.time : I.track); $('#bdView').setAttribute('aria-label', 'Change view. Now: ' + (v === 'time' ? 'Timeline' : 'Stage track'));
|
|
520
|
-
|
|
548
|
+
drawSpanPicker('desk', v === 'time' && !phone);
|
|
521
549
|
$('#bdColsw').hidden = v !== 'time' || phone; if ($('#bdColsw').hidden) closeCols();
|
|
522
|
-
$$('#bdSpans [data-sp]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.sp === span)));
|
|
523
550
|
drawSpanPick(phone && v === 'time');
|
|
524
551
|
document.body.dataset.bdView = v;
|
|
525
552
|
$('#stage').dataset.bdDesk = view; // what fit() measures: the desktop's view, so the narrow form never flips it back and forth
|
|
@@ -571,22 +598,27 @@ $('#bdShowing').addEventListener('click', (e) => {
|
|
|
571
598
|
if (u.dataset.unf === 'q') F.q = ''; else if (u.dataset.unf === 'wf') WF.setPick(''); else F[u.dataset.unf].clear(); drawChips(); drawInit();
|
|
572
599
|
});
|
|
573
600
|
function setSpan(s) { span = s; store.set(SPAN_KEY, span); drawChips(); drawInit(); }
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
601
|
+
/** The span picker: one chip that opens the six spans (batch 24 item 7, canvas BbB, as a phone's TsCPhone, TsCPhoneSpan). A desktop draws it in the
|
|
602
|
+
* chip row after the tabs (#bdSpans, items data-sp); a phone draws it beside the product picker (#bdSpanPick, items data-spick), and the page title
|
|
603
|
+
* gives it the room. */
|
|
604
|
+
const SPK = { desk: ['bdSpans', 'bdSpansBtn', 'bdSpansMenu', 'sp'], phone: ['bdSpanPick', 'bdSpanBtn', 'bdSpanMenu', 'spick'] };
|
|
605
|
+
function drawSpanPicker(which, on) {
|
|
606
|
+
const [wid, bid, mid, attr] = SPK[which], w = $('#' + wid);
|
|
578
607
|
w.hidden = !on; if (!on) { w.innerHTML = ''; return; }
|
|
579
|
-
const open =
|
|
580
|
-
w.innerHTML = `<button type="button" class="bd-chip bd-spk" id="
|
|
581
|
-
+ `<span class="bd-grpm" id="
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
|
|
608
|
+
const open = menuOpen(mid), name = (SPANS.find(([k]) => k === span) || SPANS[1])[1];
|
|
609
|
+
w.innerHTML = `<button type="button" class="bd-chip bd-spk" id="${bid}" aria-haspopup="menu" aria-expanded="${open}" aria-label="Span: ${esc(name)}. Change the span">${esc(name)}${SV(I.chev)}</button>`
|
|
610
|
+
+ `<span class="bd-grpm" id="${mid}" role="menu"${open ? '' : ' hidden'}>${SPANS.map(([k, n]) => `<button type="button" role="menuitemradio" aria-checked="${k === span}" data-${attr}="${k}">${esc(n)}</button>`).join('')}</span>`;
|
|
611
|
+
}
|
|
612
|
+
function drawSpanPick(on) { document.body.classList.toggle('bd-spick', on); drawSpanPicker('phone', on); }
|
|
613
|
+
for (const which of Object.keys(SPK)) {
|
|
614
|
+
const [wid, bid, mid, attr] = SPK[which], w = $('#' + wid);
|
|
615
|
+
w.addEventListener('click', (e) => {
|
|
616
|
+
const k = e.target.closest(`[data-${attr}]`); if (k && M) { closeMenu(mid, bid); setSpan(k.dataset[attr]); const b = $('#' + bid); if (b) b.focus(); return; }
|
|
617
|
+
if (e.target.closest('#' + bid)) { const m = $('#' + mid), o = m.hidden; m.hidden = !o; $('#' + bid).setAttribute('aria-expanded', String(o)); if (o) m.querySelector('[aria-checked="true"]').focus(); }
|
|
618
|
+
});
|
|
619
|
+
w.addEventListener('keydown', (e) => { if (e.key === 'Escape' && closeMenu(mid, bid)) { e.stopPropagation(); $('#' + bid).focus(); } });
|
|
620
|
+
document.addEventListener('click', (e) => { if (!e.target.closest('#' + wid)) closeMenu(mid, bid); });
|
|
621
|
+
}
|
|
590
622
|
$('#ibBody').addEventListener('click', (e) => {
|
|
591
623
|
if (e.target.closest('#fold, [data-mefold]')) { foldOpen = !foldOpen; drawInit(); const f = $('#fold') || $('#ibBody [data-mefold]'); if (f) f.focus(); return; }
|
|
592
624
|
const gf = e.target.closest('[data-fold]'); if (gf) { const k = gf.dataset.fold; PG.fold(k); drawInit(); const b = $$('#ibBody [data-fold]').find((x) => x.dataset.fold === k); if (b) b.focus(); return; }
|
|
@@ -860,24 +892,20 @@ $('#bdPublish').addEventListener('click', (e) => { const btn = e.currentTarget;
|
|
|
860
892
|
/* ── tabs: Initiatives and Documents (Documents has no frame yet: a plain list) ──────────────────────────────────────────────────────── */
|
|
861
893
|
function setTab(t) {
|
|
862
894
|
tab = t;
|
|
863
|
-
$('#tabIni').setAttribute('aria-selected', String(t === 'ini')); $('#
|
|
864
|
-
$('#cIni').hidden = t !== 'ini'; $('#
|
|
865
|
-
if (t === 'docs') { closePanel(); drawDocs(); loadDocMeta(); }
|
|
895
|
+
$('#tabIni').setAttribute('aria-selected', String(t === 'ini')); $('#tabShared').setAttribute('aria-selected', String(t === 'shared'));
|
|
896
|
+
$('#cIni').hidden = t !== 'ini'; $('#cShared').hidden = t !== 'shared';
|
|
866
897
|
// Shared (initiative board-publish, slice 4): reads each joined team's host now, on opening the tab and on Refresh, and never in between
|
|
867
898
|
if (t === 'shared') { closePanel(); import('./bd-shared.js').then((S) => { SH_FULL = true; S.show($('#cShared'), { onCount: setSharedCount }); }); }
|
|
868
899
|
}
|
|
869
900
|
$('#tabIni').onclick = () => setTab('ini');
|
|
870
|
-
$('#tabDocs').onclick = () => setTab('docs');
|
|
871
901
|
$('#tabShared').onclick = () => setTab('shared');
|
|
872
|
-
$('.bd-tabs').addEventListener('keydown', (e) => { if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; const ord = ['ini', '
|
|
902
|
+
$('.bd-tabs').addEventListener('keydown', (e) => { if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; const ord = ['ini', 'shared']; const n = ord[(ord.indexOf(tab) + (e.key === 'ArrowRight' ? 1 : ord.length - 1)) % ord.length]; setTab(n); $('#' + { ini: 'tabIni', shared: 'tabShared' }[n]).focus(); });
|
|
873
903
|
/** The number on the Shared tab and on the phone's Shared chip: nothing when there is nothing (zero is silence). Your own open links at load, every open copy after a read. */
|
|
874
904
|
function setSharedCount(n) { SH_N = n; $('#nShared').textContent = n ? String(n) : ''; const c = $('#chShared'); if (c) c.textContent = 'Shared' + (n ? ' ' + n : ''); }
|
|
875
905
|
document.addEventListener('bd:links', (e) => { if (!SH_FULL && e.detail) setSharedCount(e.detail.n); });
|
|
876
906
|
|
|
877
|
-
/**
|
|
878
|
-
|
|
879
|
-
$('#docBody').addEventListener('click', (e) => { const k = e.target.closest('[data-dkind]'); if (k) { V.setDocKind(k.dataset.dkind); drawDocs(); const b = $(`#docBody [data-dkind="${k.dataset.dkind}"]`); if (b) b.focus(); } });
|
|
880
|
-
/** The history of the documents on screen (GET /api/board-docs): when each last changed, by whom, its last commits. Read-only. */
|
|
907
|
+
/** The history of the documents (GET /api/board-docs) for the document panel of pm/bd-acts.js. Batch 24 item 7 took the Documents tab off the Board,
|
|
908
|
+
* so nothing on the Board opens that panel now; it is kept for a later way in (see the PR).: when each last changed, by whom, its last commits. Read-only. */
|
|
881
909
|
async function loadDocMeta(list) {
|
|
882
910
|
if (!M) return;
|
|
883
911
|
// each document is read from its own product's workspace (All products lists every product's documents)
|
|
@@ -891,7 +919,6 @@ async function loadDocMeta(list) {
|
|
|
891
919
|
for (const p of P) DM[ws + ':' + p] = r.docs[p] || { none: true };
|
|
892
920
|
got = true;
|
|
893
921
|
}
|
|
894
|
-
if (got && tab === 'docs') drawDocs();
|
|
895
922
|
}
|
|
896
923
|
const docMeta = (p, ws) => { const d = DM[(ws || (M && M.board.ws) || '') + ':' + p]; return d && !d.none ? d : null; };
|
|
897
924
|
|
|
@@ -976,8 +1003,6 @@ function draw() {
|
|
|
976
1003
|
drawHead(); drawChips(); drawInit();
|
|
977
1004
|
{ const g = $('#bdGone'), G = M.initiatives.gone || { cancelled: [], archived: [] }, n = [G.cancelled.length ? G.cancelled.length + ' cancelled' : '', G.archived.length ? G.archived.length + ' archived' : ''].filter(Boolean).join(' · ');
|
|
978
1005
|
if (g) g.textContent = 'Cancelled and archived' + (n ? ' · ' + n : ''); } // the ⋯ menu item counts them
|
|
979
|
-
if (tab === 'docs') drawDocs();
|
|
980
|
-
$('#docCap').textContent = String(V.docRows().length);
|
|
981
1006
|
// an open panel follows the new read
|
|
982
1007
|
if (peek && peek.kind === 'ini') BI.refresh(); // a row that left the Board (cancelled, archived) closes it
|
|
983
1008
|
}
|
|
@@ -1037,7 +1062,7 @@ async function readAll(q) {
|
|
|
1037
1062
|
async function load() {
|
|
1038
1063
|
// while the board is read (the pick's default board, then the picked ones) the page says so and shows no row instead of a guess
|
|
1039
1064
|
const reading = () => {
|
|
1040
|
-
const t0 = Date.now(), tick = () => { const s = 'Reading the board · ' + Math.round((Date.now() - t0) / 1000) + ' s so far'; $('#snap').textContent = s; if (!M) $('#ibBody').innerHTML =
|
|
1065
|
+
const t0 = Date.now(), tick = () => { const s = 'Reading the board · ' + Math.round((Date.now() - t0) / 1000) + ' s so far'; $('#snap').textContent = s; if (!M && !$('#ibBody .ls-bd')) { $('#ibBody').innerHTML = SKEL; LS.slowLine($('#ibBody'), () => M); } };
|
|
1041
1066
|
tick(); readTimer = setInterval(tick, 1000);
|
|
1042
1067
|
// a first read after a restart usually lands within a second: ask again soon, then less often (page-speed)
|
|
1043
1068
|
setTimeout(load, Math.min(3000, 300 * 2 ** readTries++));
|
|
@@ -1153,7 +1178,7 @@ V.init({ M: () => M, wf: wfState, statusOf, usual: usualOf, trail: TI, docMeta,
|
|
|
1153
1178
|
groups: (L) => groups(L), folded: PG.folded, laneHead: (g) => PG.laneHtml(g, g.kind === 'program' ? progState(g.program) : null), subTile: (n) => PG.tileHtml(n, 'bd-ti s'),
|
|
1154
1179
|
memberIcon: (id) => PG.memberIcon(PGS, id), product: () => (M.board.product || M.board.name || ''), programOf: (r) => programOf(PGS, r.ws || M.board.ws || '', r.id), tileHtml: PG.tileHtml, progTile: PG.progTile });
|
|
1155
1180
|
Asks.init({ M: () => M, load, say, panel: actsPanel, close: closePanel, rowOf, openIni: openIniAny, openDoc: viewDoc, anchors, programs: () => PGS, reloadPrograms: loadPrograms });
|
|
1156
|
-
Acts.init({ M: () => M, wf: wfState, load, say, panel: actsPanel, close: closePanel, redraw: () => { drawChips(); drawInit();
|
|
1181
|
+
Acts.init({ M: () => M, wf: wfState, load, say, panel: actsPanel, close: closePanel, redraw: () => { drawChips(); drawInit(); }, F, rowOf, statusOf, openIni: openIniAny,
|
|
1157
1182
|
openTicket: (ini, tk) => openTicket(ini, tk, $('#bdFind')), askHref, pageHref, trail: TI, docMeta, loadDocMeta, viewDoc,
|
|
1158
1183
|
grouping: () => ({ on: groupingOn(), byProduct }), setGrouping, sprintFilter: sprintFilterOn, timeline: () => curView() === 'time' });
|
|
1159
1184
|
BI.mount({ M: () => M, statusOf, dayTitle, usual: usualOf, fmt, md, ago, segHtml, barHtml: V.barHtml, grill: (id) => GR[id], rowOf, pageHref, askHref, refreshJira: () => refreshJira(), viewDoc,
|
|
@@ -17,8 +17,8 @@ export const RO = !!PACK;
|
|
|
17
17
|
|
|
18
18
|
/** What a reader can do. Each is a selector; a press inside a match is let through. */
|
|
19
19
|
export const VIEW_CONTROLS = [
|
|
20
|
-
// the Timeline: the span, a column's sort, back to today, the Deliveries lane, the folds, an initiative's name (opens its viewer)
|
|
21
|
-
'#bdSpans button', '.tg-chd', '.tg-tback', '.dl-fb', '.dl-dn', '.dl-e[aria-label*="press to open"]', '.bd-fold', '.bd-prdb', 'label.tg-n', 'button.tg-n.ini2',
|
|
20
|
+
// the Timeline: the span, the legend behind (i), a column's sort, back to today, the Deliveries lane, the folds, an initiative's name (opens its viewer)
|
|
21
|
+
'#bdSpans button', '#bdLgdBtn', '.tg-chd', '.tg-tback', '.dl-fb', '.dl-dn', '.dl-e[aria-label*="press to open"]', '.bd-fold', '.bd-prdb', 'label.tg-n', 'button.tg-n.ini2',
|
|
22
22
|
// the stage track: the switch between it and the Timeline (the two views a publisher shares), an initiative's name, the Monitor and Evaluate fold
|
|
23
23
|
'#ibTabs [data-v="track"]', '#ibTabs [data-v="time"]', 'button.bt-nm', '#fold',
|
|
24
24
|
// the viewer: the layout, the close, the folds, the Tasks view menu, a stage's fold and its done tasks
|
|
@@ -36,7 +36,7 @@ const HIDE = ['#ibTabs [data-v="stages"]', '#ibTabs [data-v="list"]', '#bdView',
|
|
|
36
36
|
/** Controls the code draws that write, or lead to something a share does not hold: removed. */
|
|
37
37
|
const REMOVE = [
|
|
38
38
|
'.po-grip', '.bd-nxa',
|
|
39
|
-
'#bdChipset > :not(.
|
|
39
|
+
'#bdChipset > :not(.bd-lgdw)', '#askDates', '.tg-ft',
|
|
40
40
|
'.cz-vw-ib[aria-label="Discuss in chat"]', '.cz-vw-ib[aria-label="More"]', '.iv-addp', '.iv-prog', '.iv-plus', '.iv-car', '.iv-scgn', '.iv-gx-a',
|
|
41
41
|
'.sm-card', '.pp-p', '.lk-chip.mv', // what Claude proposes to the publisher (batch 20 item 1): the pack carries none, and none is drawn
|
|
42
42
|
// the program page's writes and what a share does not hold (batch 21 item 2): proposals, adding, the charter, a document's file, the talk line
|
|
@@ -98,9 +98,10 @@ export async function press(c, act) {
|
|
|
98
98
|
if (act === 'no') {
|
|
99
99
|
// the pressed button holds the focus inside the Viewer, and a host does not redraw a body that holds it
|
|
100
100
|
if (typeof document !== 'undefined' && document.activeElement && document.activeElement.blur) document.activeElement.blur();
|
|
101
|
-
const
|
|
101
|
+
const facts = sg.kind === 'merge' ? { into: sg.into } : { moves: [...(sg.tasks || []), ...(sg.tickets || [])] };
|
|
102
|
+
const x = await c.pmPost('/api/initiatives/suggest', { ws: c.ws, id: r.id, of: sg.kind, fp: sg.fp, ...facts }).catch(() => null);
|
|
102
103
|
if (!x || !x.ok) { c.say((x && x.error) || 'It is still shown. The server did not say why.'); return; }
|
|
103
|
-
c.say('Not now. Claude
|
|
104
|
+
c.say(sg.kind === 'merge' ? 'Not now. Claude will not suggest this merge again.' : 'Not now. Claude will not suggest this split again.');
|
|
104
105
|
await c.reload();
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<link rel="stylesheet" href="/pm/cozy.css" />
|
|
14
14
|
<link rel="stylesheet" href="/pm/cozy-pick.css" />
|
|
15
15
|
<link rel="stylesheet" href="/pm/cozy-board.css" />
|
|
16
|
+
<link rel="stylesheet" href="/pm/load-shapes.css" />
|
|
16
17
|
<link rel="stylesheet" href="/pm/bd-pin-card.css" />
|
|
17
18
|
<link rel="stylesheet" href="/pm/cozy-panel.css" />
|
|
18
19
|
</head>
|
|
@@ -50,27 +51,23 @@
|
|
|
50
51
|
<div class="tools tools-top"><span class="vh" id="snap" role="status">Reading the board</span></div>
|
|
51
52
|
</div>
|
|
52
53
|
</div>
|
|
53
|
-
<!-- the tabs,
|
|
54
|
+
<!-- the tabs (Initiatives, Shared; batch 24 item 7 took Documents off: an initiative's Documents fold and Search's Documents filter reach them),
|
|
55
|
+
the span picker and the chips: sprint and release, This sprint, one pill for new asks, late, at risk, blocked and idle, the legend behind (i), Jira as Adler -->
|
|
54
56
|
<div class="bd-chiprow">
|
|
55
57
|
<div class="bd-tabs" role="tablist" aria-label="Board">
|
|
56
58
|
<button type="button" role="tab" class="bd-tab" id="tabIni" aria-selected="true" aria-controls="cIni">Initiatives <span class="n" id="nIni"></span></button>
|
|
57
|
-
<button type="button" role="tab" class="bd-tab" id="tabDocs" aria-selected="false" aria-controls="cDocs">Documents <span class="n" id="docCap"></span></button>
|
|
58
59
|
<button type="button" role="tab" class="bd-tab" id="tabShared" aria-selected="false" aria-controls="cShared">Shared <span class="n" id="nShared"></span></button>
|
|
59
60
|
</div>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</div>
|
|
61
|
+
<!-- the span picker (batch 24 item 7, canvas BbB): one chip that opens Sprint to 5 yr; the timeline only -->
|
|
62
|
+
<span class="bd-grpw2 bd-spw" id="bdSpans" role="group" aria-label="Span" hidden></span>
|
|
63
63
|
<div class="bd-chipset" id="bdChipset"></div>
|
|
64
64
|
</div>
|
|
65
65
|
|
|
66
66
|
<section class="bd-sec" id="cIni" role="tabpanel" aria-labelledby="tabIni">
|
|
67
67
|
<h2 class="vh" id="hIni">Initiatives</h2>
|
|
68
68
|
<div id="bdShowing" hidden></div>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<section class="bd-sec" id="cDocs" role="tabpanel" aria-labelledby="tabDocs" hidden>
|
|
72
|
-
<h2 class="vh" id="hDocs">Documents</h2>
|
|
73
|
-
<div id="docBody"></div>
|
|
69
|
+
<!-- the skeleton until the board is read (page-open-speed, SpD): grey rows shaped like programs and initiatives, no names, no counts -->
|
|
70
|
+
<div id="ibBody"><div class="ls-bd" aria-hidden="true"><div class="ls-bh"><i class="ls-sk ls-w40"></i><span class="ls-stg"><i class="ls-sk"></i><i class="ls-sk"></i><i class="ls-sk"></i><i class="ls-sk"></i><i class="ls-sk"></i><i class="ls-sk"></i><i class="ls-sk"></i><i class="ls-sk"></i></span><i class="ls-sk ls-w30"></i></div><div class="ls-br pg"><i class="ls-sk ls-w60"></i><span></span><i class="ls-sk ls-w55"></i></div><div class="ls-br"><i class="ls-sk ls-w75"></i><div class="ls-trk"><i class="ls-tdot"></i><i class="ls-tdot on"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i></div><i class="ls-sk ls-w70"></i></div><div class="ls-br"><i class="ls-sk ls-w75"></i><div class="ls-trk"><i class="ls-tdot"></i><i class="ls-tdot on"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i></div><i class="ls-sk ls-w70"></i></div><div class="ls-br pg"><i class="ls-sk ls-w60"></i><span></span><i class="ls-sk ls-w55"></i></div><div class="ls-br"><i class="ls-sk ls-w75"></i><div class="ls-trk"><i class="ls-tdot"></i><i class="ls-tdot on"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i></div><i class="ls-sk ls-w70"></i></div><div class="ls-br pg"><i class="ls-sk ls-w60"></i><span></span><i class="ls-sk ls-w55"></i></div><div class="ls-br"><i class="ls-sk ls-w75"></i><div class="ls-trk"><i class="ls-tdot"></i><i class="ls-tdot on"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i><i class="ls-tdot"></i></div><i class="ls-sk ls-w70"></i></div></div></div>
|
|
74
71
|
</section>
|
|
75
72
|
<section class="bd-sec" id="cShared" role="tabpanel" aria-labelledby="tabShared" hidden></section>
|
|
76
73
|
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
<link rel="stylesheet" href="/pm/home-impact.css" />
|
|
25
25
|
<link rel="stylesheet" href="/pm/cozy-panel.css" />
|
|
26
26
|
<link rel="stylesheet" href="/pm/cozy-onboard.css" />
|
|
27
|
+
<link rel="stylesheet" href="/pm/load-shapes.css" />
|
|
27
28
|
</head>
|
|
28
29
|
<body class="cz cz-home">
|
|
29
30
|
<header class="hm-top" id="hmTop">
|
|
@@ -167,7 +167,7 @@ const shot = (p, name) => (SHOTS ? p.screenshot({ path: path.join(SHOTS, name) }
|
|
|
167
167
|
await shot(p, 'phone-suggest.png');
|
|
168
168
|
await p.click('#czViewer [data-sm="no"]');
|
|
169
169
|
const t = await until(async () => { const x = await toast(p); return /^Not now/.test(x) ? x : null; });
|
|
170
|
-
ok('P ✕ Not now: the toast says Claude
|
|
170
|
+
ok('P ✕ Not now: the toast says Claude will not suggest it again', /^Not now/.test(t || ''), t);
|
|
171
171
|
ok('P ✕ Not now hides the card and the dot', await until(() => p.evaluate(() => !document.querySelector('#czViewer .sm-card') && !document.querySelector('#ibBody .sm-dot')), 20000));
|
|
172
172
|
ok('P no page error', !errors.length, errors.join(' | '));
|
|
173
173
|
await ctx.close();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Batch 25 item 3: a finished chat turn re-reads what the Viewer shows, with no manual refresh (pm/chat-turn-end.js, home-chat.js, home-app.js). Pure builder plus the wiring.",
|
|
3
|
+
"modules": [
|
|
4
|
+
"chat-turn-end.test.js"
|
|
5
|
+
],
|
|
6
|
+
"seconds": {
|
|
7
|
+
"chat-turn-end.test.js": 1
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Page open speed (2026-11-02): the file-stamp caches that replace git runs on a page open stay as fresh as a git run; the start-up warm-up can be turned off and asks one path at a time.",
|
|
2
|
+
"_comment": "Page open speed (2026-11-02): the file-stamp caches that replace git runs on a page open stay as fresh as a git run; the start-up warm-up can be turned off and asks one path at a time; the loading shapes claim nothing and the last view kept is capped.",
|
|
3
3
|
"modules": ["page-open-speed.test.js"],
|
|
4
4
|
"seconds": { "page-open-speed.test.js": 2 }
|
|
5
5
|
}
|