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.
Files changed (52) hide show
  1. package/package.json +57 -33
  2. package/payload/AGENTS.md +2 -0
  3. package/payload/KIT-VERSION +2 -2
  4. package/payload/planning/evals/eval-calibration.md +1 -1
  5. package/payload/planning/okrs/health-budgets.md +1 -1
  6. package/payload/scripts/workflow-state.js +7 -19
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/adler-moments-test.js +3 -1
  10. package/workspace/board-programs-test.js +6 -6
  11. package/workspace/board-shared-ui-test.js +2 -2
  12. package/workspace/briefing-premium-adddoc-test.js +1 -48
  13. package/workspace/briefing-premium-board-test.js +18 -19
  14. package/workspace/changelog.json +56 -0
  15. package/workspace/guide/adler.md +1 -1
  16. package/workspace/guide/briefing-premium.md +1 -0
  17. package/workspace/guide/claude-proposes.md +6 -2
  18. package/workspace/guide/data-freshness.md +7 -1
  19. package/workspace/guide/home.md +1 -1
  20. package/workspace/guide/published-board.md +1 -1
  21. package/workspace/guide/workflows.md +4 -3
  22. package/workspace/lib/board-premium.js +17 -2
  23. package/workspace/lib/engine/workflow-state.js +7 -19
  24. package/workspace/lib/initiative-chat.js +4 -1
  25. package/workspace/lib/proposals.js +18 -4
  26. package/workspace/lib/routes-board-premium.js +2 -2
  27. package/workspace/lib/routes-proposals.js +14 -2
  28. package/workspace/lib/split-merge-suggest.js +37 -3
  29. package/workspace/lib/split-merge.js +10 -4
  30. package/workspace/lib/task-cli.js +5 -1
  31. package/workspace/lib/vendor/scripts/workflow-state.js +7 -19
  32. package/workspace/lib/workflow-cli.js +70 -15
  33. package/workspace/package-lock.json +2 -2
  34. package/workspace/package.json +1 -1
  35. package/workspace/premium-board-actions-test.js +7 -9
  36. package/workspace/premium-core-doc-issue-test.js +9 -38
  37. package/workspace/public/pm/chat-turn-end.js +19 -0
  38. package/workspace/public/pm/cozy-board.css +17 -5
  39. package/workspace/public/pm/home-app.js +21 -5
  40. package/workspace/public/pm/home-chat.js +5 -0
  41. package/workspace/public/pm/home-ini.js +7 -0
  42. package/workspace/public/pm/home-today.js +11 -4
  43. package/workspace/public/pm/load-shapes.css +57 -0
  44. package/workspace/public/pm/load-shapes.js +57 -0
  45. package/workspace/public/pm/premium-board.js +82 -57
  46. package/workspace/public/pm/readonly.js +3 -3
  47. package/workspace/public/pm/split-merge.js +3 -2
  48. package/workspace/public/premium-board.html +7 -10
  49. package/workspace/public/premium.html +1 -0
  50. package/workspace/split-merge-ui-test.js +1 -1
  51. package/workspace/test-lists/chat-turn-end.json +9 -0
  52. 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'; // the Board as the first panel: fit(), the rows that open beside, the open row's mark
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', (n) => n + ' late'], ['risk', 'risk', 'm-warn', (n) => n + ' at risk'], ['blocked', 'red', 'm-crit', (n) => n + ' blocked'], ['idle', 'q', '', (n) => n + ' idle 30+ days']];
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 ? (phone ? 'Jira ' + ago(J.readAt, true) : 'Jira · read ' + ago(J.readAt)) : phone ? 'Jira not read' : 'Jira · not read';
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 chip with the oldest read of any product, and every product's new asks; the sprint stays per product
258
- // AdM2: the chip'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
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 jiraChip = `<button type="button" class="bd-chip bd-adc" id="chJira" title="${ALL ? 'Read Jira again for every product' : 'Read Jira again'}">${faceOr(jMood, 's', SV(I.refresh), { label: false })}<span>${esc(jira)}</span></button>`;
262
- const askChip = `<button type="button" class="bd-chip" id="chAsks" aria-haspopup="dialog">${asks ? esc(asks) : `<i class="cz-dot you${C.asks ? ' m-info' : ''}"></i><b>${C.asks}</b> new ${plural(C.asks, 'ask', 'asks')}`}</button>`;
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
- const docs = M.docs && M.docs.rows ? M.docs.rows.length : 0;
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 sits at the end of the chip row (PgTimeline), with a program milestone only where a program is drawn and "On hold" only
269
- // where a hold is drawn (feedback batch 9 item 8: zero is silence)
270
- const lgd = view === 'time' && span !== 'sprint' ? '<span class="bd-sp"></span>' + legendHtml(!byProduct && !!groups(visible()), hasHold(visible()), { ...planKeys(visible()), ...gapKeys(visible()), range: visible().some((r) => fcRange(r)) }) : '';
271
- $('#bdChipset').innerHTML = jiraChip
272
- + (ALL ? '' : `<button type="button" class="bd-chip" id="chSprint" aria-haspopup="dialog">${SV(I.cal)}<span>${esc(sprint)}</span></button>`)
273
- + askChip + '<span class="bd-vr"></span>' + flt(['late', 'risk', 'blocked', 'idle'])
274
- + (thisSprint ? '<span class="bd-sp"></span>' + thisSprint : '') + lgd;
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
- // a metric chip opens its trend (BdPanel), whose "Show only" filters; a chip that is on turns off at once
281
- if (k !== 'sprint' && !F.chips.has(k)) { Acts.open.trend(k, f); return; }
282
- if (F.chips.has(k)) F.chips.delete(k); else F.chips.add(k); drawChips(); drawInit(); return;
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(b);
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
- $('#bdSpans').hidden = v !== 'time' || phone;
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
- $('#bdSpans').addEventListener('click', (e) => { const b = e.target.closest('[data-sp]'); if (!b || !M) return; setSpan(b.dataset.sp); });
575
- /** A phone's span picker (TsCPhone, TsCPhoneSpan): the six span buttons become one picker beside the product picker, and the span row goes. */
576
- function drawSpanPick(on) {
577
- const w = $('#bdSpanPick'); document.body.classList.toggle('bd-spick', on);
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 = !!$('#bdSpanMenu') && !$('#bdSpanMenu').hidden, name = (SPANS.find(([k]) => k === span) || SPANS[1])[1];
580
- w.innerHTML = `<button type="button" class="bd-chip bd-spk" id="bdSpanBtn" aria-haspopup="menu" aria-expanded="${open}" aria-label="Span: ${esc(name)}. Change the span">${esc(name)}${SV(I.chev)}</button>`
581
- + `<span class="bd-grpm" id="bdSpanMenu" role="menu"${open ? '' : ' hidden'}>${SPANS.map(([k, n]) => `<button type="button" role="menuitemradio" aria-checked="${k === span}" data-spick="${k}">${esc(n)}</button>`).join('')}</span>`;
582
- }
583
- const closeSpanPick = () => { const m = $('#bdSpanMenu'); if (m && !m.hidden) { m.hidden = true; $('#bdSpanBtn').setAttribute('aria-expanded', 'false'); return true; } return false; };
584
- $('#bdSpanPick').addEventListener('click', (e) => {
585
- const k = e.target.closest('[data-spick]'); if (k) { closeSpanPick(); setSpan(k.dataset.spick); const b = $('#bdSpanBtn'); if (b) b.focus(); return; }
586
- if (e.target.closest('#bdSpanBtn')) { const m = $('#bdSpanMenu'), o = m.hidden; m.hidden = !o; $('#bdSpanBtn').setAttribute('aria-expanded', String(o)); if (o) m.querySelector('[aria-checked="true"]').focus(); }
587
- });
588
- $('#bdSpanPick').addEventListener('keydown', (e) => { if (e.key === 'Escape' && closeSpanPick()) { e.stopPropagation(); $('#bdSpanBtn').focus(); } });
589
- document.addEventListener('click', (e) => { if (!e.target.closest('#bdSpanPick')) closeSpanPick(); });
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')); $('#tabDocs').setAttribute('aria-selected', String(t === 'docs')); $('#tabShared').setAttribute('aria-selected', String(t === 'shared'));
864
- $('#cIni').hidden = t !== 'ini'; $('#cDocs').hidden = t !== 'docs'; $('#cShared').hidden = t !== 'shared';
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', 'docs', 'shared']; const n = ord[(ord.indexOf(tab) + (e.key === 'ArrowRight' ? 1 : ord.length - 1)) % ord.length]; setTab(n); $('#' + { ini: 'tabIni', docs: 'tabDocs', shared: 'tabShared' }[n]).focus(); });
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
- /** Documents (BdDocs): the kind chips, the rows and their history; a row opens the document's panel (pm/bd-acts.js). */
878
- function drawDocs() { if (M) $('#docBody').innerHTML = V.docsHtml(); }
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 = `<p class="bd-msg"><b>${esc(s)}</b> The board derives each initiative from its records. This page shows nothing until it has read, rather than guess.</p>`; };
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(); if (tab === 'docs') drawDocs(); }, F, rowOf, statusOf, openIni: openIniAny,
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(.tg-lgd)', '#askDates', '.tg-ft',
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 x = await c.pmPost('/api/initiatives/suggest', { ws: c.ws, id: r.id, of: sg.kind, fp: sg.fp }).catch(() => null);
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 asks again when the tasks change.');
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, the timeline spans and the chips: Jira read time, sprint and release, new asks, late, at risk, blocked, idle, This sprint -->
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
- <div class="bd-seg bd-spans" id="bdSpans" role="group" aria-label="Span" hidden>
61
- <button type="button" data-sp="sprint" aria-pressed="false">Sprint</button><button type="button" data-sp="quarter" aria-pressed="true">Quarter</button><button type="button" data-sp="half" aria-pressed="false">Half</button><button type="button" data-sp="year" aria-pressed="false">Year</button><button type="button" data-sp="y2" aria-pressed="false">2 yr</button><button type="button" data-sp="y5" aria-pressed="false">5 yr</button>
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
- <div id="ibBody"></div>
70
- </section>
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 asks again when the tasks change', /^Not now/.test(t || ''), t);
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
  }