create-pmos 0.494.0 → 0.496.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 +75 -27
- 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/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 +3 -49
- package/workspace/briefing-premium-board-test.js +23 -23
- package/workspace/briefing-premium-initiative-test.js +2 -1
- package/workspace/changelog.json +74 -0
- package/workspace/guide/adler.md +1 -1
- package/workspace/guide/briefing-premium.md +3 -1
- package/workspace/guide/claude-proposes.md +6 -2
- package/workspace/guide/data-freshness.md +2 -0
- package/workspace/guide/home.md +1 -1
- package/workspace/guide/initiatives-screen.md +1 -1
- package/workspace/guide/published-board.md +1 -1
- package/workspace/guide/report-slides.md +6 -6
- package/workspace/guide/reports.md +1 -1
- package/workspace/guide/workflows.md +18 -5
- package/workspace/ini-workflow-test.js +2 -1
- package/workspace/lib/board-premium.js +17 -2
- package/workspace/lib/engine/workflows.js +19 -6
- package/workspace/lib/initiative-chat.js +4 -1
- package/workspace/lib/initiative-cli.js +15 -1
- package/workspace/lib/open-mission.js +14 -0
- package/workspace/lib/proposals.js +18 -4
- package/workspace/lib/report-export.js +5 -3
- package/workspace/lib/report-pyramid.js +134 -33
- package/workspace/lib/report-templates.js +33 -3
- package/workspace/lib/routes-board-premium.js +2 -2
- package/workspace/lib/routes-boards.js +35 -2
- package/workspace/lib/routes-brief-page.js +2 -1
- package/workspace/lib/routes-initiatives.js +1 -0
- package/workspace/lib/routes-proposals.js +14 -2
- package/workspace/lib/routes-report-design.js +1 -1
- package/workspace/lib/routes-report-flow.js +11 -4
- package/workspace/lib/routes-report-setups.js +3 -2
- package/workspace/lib/routes-workflows.js +6 -1
- 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/workflow-cli.js +70 -15
- package/workspace/lib/workflows-store.js +15 -1
- package/workspace/metric-drafts-ui-test.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-board-actions-test.js +9 -10
- package/workspace/premium-core-doc-issue-test.js +9 -38
- package/workspace/premium-vw-backlog-test.js +3 -2
- package/workspace/premium-vw-item-test.js +3 -2
- package/workspace/premium-vw-jira-test.js +3 -2
- package/workspace/program-panel-test.js +2 -1
- package/workspace/public/pm/cozy-board.css +17 -5
- package/workspace/public/pm/cozy-home.css +1 -0
- package/workspace/public/pm/home-app.js +15 -5
- package/workspace/public/pm/home-chat.js +49 -8
- package/workspace/public/pm/home-today.js +11 -4
- package/workspace/public/pm/ini-view.js +2 -2
- 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/premium-shell.js +57 -6
- package/workspace/public/pm/premium.css +1 -0
- package/workspace/public/pm/readonly.js +3 -3
- package/workspace/public/pm/split-merge.js +3 -2
- package/workspace/public/pm/wf-page.css +8 -0
- package/workspace/public/pm/wf-page.js +33 -5
- package/workspace/public/premium-board.html +7 -10
- package/workspace/public/premium.html +1 -0
- package/workspace/skills/report-template.skill +4 -2
- package/workspace/split-merge-ui-test.js +4 -3
- package/workspace/stage-card-test.js +2 -1
- package/workspace/task-press-test.js +2 -1
- package/workspace/test-lists/page-open-speed.json +1 -1
- package/workspace/tk-tasks-test.js +2 -1
|
@@ -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,
|
|
@@ -538,14 +538,50 @@ $('#dBody').addEventListener('click', async (e) => {
|
|
|
538
538
|
const b = e.target.closest('[data-lead="refresh"]'); if (!b || !S.key || !S.key.startsWith('pop:')) return;
|
|
539
539
|
b.disabled = true; await sendText(S.key, leadAsk(S.key));
|
|
540
540
|
});
|
|
541
|
+
/* A sent message shows at once (batch 24 item 2: "enter the prompt is so long to appear on the chat"): its turn draws the moment the PM presses
|
|
542
|
+
* Enter, lighter until PMOS takes it. It leaves when the transcript holds the PM's turn (the same words, or any PM turn from that moment on),
|
|
543
|
+
* and after 3 minutes at most. A message that was not sent leaves at once and its words go back in the box. Keyed by row key, or 'sid:' + id. */
|
|
544
|
+
const PEND = { key: null, list: [], th: null }, PEND_MAX = 180000;
|
|
545
|
+
const pendOf = (key) => { if (PEND.key !== key) { PEND.key = key; PEND.list = []; PEND.th = null; } return PEND.list; };
|
|
546
|
+
function pendAdd(key, text, files) {
|
|
547
|
+
const conv = key.startsWith('sid:') ? PEND.sidConv : S.conv;
|
|
548
|
+
const p = { text, files: files || [], at: new Date().toISOString(), sent: false, n: ((conv && conv.messages) || []).filter((m) => m.role === 'user').length };
|
|
549
|
+
pendOf(key).push(p); return p;
|
|
550
|
+
}
|
|
551
|
+
function pendHtml(key, conv) {
|
|
552
|
+
const list = pendOf(key); if (!list.length) return '';
|
|
553
|
+
const k = (t) => splitFiles(shownUser(t)).text.replace(/\s+/g, ' ').trim();
|
|
554
|
+
const users = ((conv && conv.messages) || []).filter((m) => m.role === 'user');
|
|
555
|
+
const lastUser = users.reduce((n, m) => Math.max(n, Date.parse(m.at) || 0), 0), now = Date.now();
|
|
556
|
+
PEND.list = list.filter((p) => {
|
|
557
|
+
if (!p.sent) return true;
|
|
558
|
+
const t0 = Date.parse(p.at);
|
|
559
|
+
return now - t0 <= PEND_MAX && !(lastUser && lastUser >= t0 - 2000) && !users.slice(p.n).some((m) => k(m.text) === k(p.text));
|
|
560
|
+
});
|
|
561
|
+
return PEND.list.map((p) => `<div class="turn u pend${p.sent ? '' : ' sending'}"><div class="ub">${filesHtml(p.files)}${p.text ? userHtml(p.text) : ''}</div></div>`).join('');
|
|
562
|
+
}
|
|
563
|
+
/** Draws the waiting turns now, without a read: the thread as last read plus the new turn, and the stream scrolled to its end. */
|
|
564
|
+
function pendDraw(key) {
|
|
565
|
+
if (key.startsWith('sid:')) { if (S.sid !== key.slice(4)) return; }
|
|
566
|
+
else if (S.key !== key) return;
|
|
567
|
+
const box = $('#dBody .stream'); if (!box) return;
|
|
568
|
+
if (!key.startsWith('sid:') && PEND.th) drawThread(PEND.th, key, S.conv);
|
|
569
|
+
else {
|
|
570
|
+
let w = $('#pendW', box); if (!w) { box.insertAdjacentHTML('beforeend', '<div id="pendW"></div>'); w = $('#pendW', box); }
|
|
571
|
+
putHtml(w, pendHtml(key, key.startsWith('sid:') ? PEND.sidConv : S.conv));
|
|
572
|
+
}
|
|
573
|
+
const b = $('#dBody'); b.scrollTop = b.scrollHeight;
|
|
574
|
+
}
|
|
541
575
|
export function drawThread(th, key, conv) {
|
|
542
576
|
if (S.key !== key) return;
|
|
577
|
+
pendOf(key); PEND.th = th;
|
|
543
578
|
const box = $('#dBody .stream'); if (!box) return;
|
|
544
579
|
drawLead(box, th);
|
|
545
580
|
let t = $('#thr', box); if (!t) { box.insertAdjacentHTML('beforeend', '<div id="thr"></div>'); t = $('#thr', box); }
|
|
546
581
|
const b = $('#dBody'), atEnd = b.scrollHeight - b.scrollTop - b.clientHeight < 80;
|
|
547
582
|
S.conv = conv;
|
|
548
|
-
|
|
583
|
+
const pw = $('#pendW', box); if (pw) pw.remove(); // a first message drew its turn before the thread was read
|
|
584
|
+
putHtml(t, threadHtml(th) + sessionHtml(conv, th.goal, { answer: true }) + pendHtml(key, conv));
|
|
549
585
|
ctxAuto(key, conv);
|
|
550
586
|
S.goal = th.goal || null; S.draft = readyDraft(conv); S.session = th.session || null;
|
|
551
587
|
syncTerm(termTarget({ live: th.session, command: th.resume, ws: new URLSearchParams(location.search).get('ws') }));
|
|
@@ -773,15 +809,29 @@ $('#dBody').addEventListener('submit', async (e) => {
|
|
|
773
809
|
const inp = $('#askIn'), text = inp.value.trim(), key = S.key;
|
|
774
810
|
if (!key && S.sid) { // premium-session-list: a session the list opened that has no row of its own
|
|
775
811
|
if (!text) { $('#askNote').textContent = 'Write a message for Claude.'; return; }
|
|
776
|
-
const id = S.sid,
|
|
777
|
-
|
|
778
|
-
|
|
812
|
+
const id = S.sid, pk = 'sid:' + id, p = pendAdd(pk, text);
|
|
813
|
+
inp.value = ''; drawSlash(); growAsk(); pendDraw(pk); // the turn shows at once (batch 24 item 2)
|
|
814
|
+
const r = await pmPost('/api/session/say', { id, text }).catch(() => null);
|
|
815
|
+
if (!r || !r.ok) {
|
|
816
|
+
PEND.list = pendOf(pk).filter((x) => x !== p); pendDraw(pk);
|
|
817
|
+
if (S.sid === id && !inp.value) { inp.value = text; growAsk(); }
|
|
818
|
+
$('#askNote').textContent = (r && r.error) || 'The message was not sent.'; return;
|
|
819
|
+
}
|
|
820
|
+
p.sent = true; pendDraw(pk);
|
|
821
|
+
$('#askNote').textContent = 'Sent.'; setTimeout(() => loadSid(id), 400); return;
|
|
779
822
|
}
|
|
780
823
|
if (!key) { $('#askNote').textContent = 'This row has no conversation, so the message was not sent.'; return; }
|
|
781
824
|
const files = attachNames();
|
|
782
825
|
if (files === null) { $('#askNote').textContent = 'A file is still saving. Send when its chip shows its size.'; return; }
|
|
783
826
|
if (!text && !files.length) { $('#askNote').textContent = 'Write a message for Claude.'; return; }
|
|
784
|
-
|
|
827
|
+
// the turn shows and the box empties at once (batch 24 item 2); a send that fails takes the turn away and puts the words back
|
|
828
|
+
const p = pendAdd(key, text, files);
|
|
829
|
+
inp.value = ''; drawSlash(); growAsk(); pendDraw(key);
|
|
830
|
+
if (await sendText(key, text, files)) { p.sent = true; inp.placeholder = 'Message Claude, or type /'; clearAttach(); pendDraw(key); }
|
|
831
|
+
else {
|
|
832
|
+
PEND.list = pendOf(key).filter((x) => x !== p); pendDraw(key);
|
|
833
|
+
if (S.key === key && !inp.value) { inp.value = text; growAsk(); }
|
|
834
|
+
}
|
|
785
835
|
});
|
|
786
836
|
|
|
787
837
|
/* ── on scroll: a card's head sticks while its card is on screen. The old hero no longer folds into a dock (premium-agreed-design). ── */
|
|
@@ -1424,7 +1474,8 @@ async function loadSid(id) {
|
|
|
1424
1474
|
syncTerm(termTarget({ live: c && c.ok && c.state !== 'ended' ? id : null }));
|
|
1425
1475
|
const box = $('#dBody .stream'); if (!box) return;
|
|
1426
1476
|
const b = $('#dBody'), atEnd = b.scrollHeight - b.scrollTop - b.clientHeight < 80;
|
|
1427
|
-
|
|
1477
|
+
if (c && c.ok) PEND.sidConv = c;
|
|
1478
|
+
putHtml(box, c && c.ok ? sessionHtml(c) + pendHtml('sid:' + id, c) : turnA(ptx('The conversation of this session could not be read.')));
|
|
1428
1479
|
$('#askNote').textContent = c && c.ask ? 'Claude is waiting for your answer above.' : 'Your message goes to this session.';
|
|
1429
1480
|
if (atEnd) b.scrollTop = b.scrollHeight;
|
|
1430
1481
|
if (c && c.ok && c.state !== 'ended' && $('#dlg').classList.contains('on')) R.sidTimer = setTimeout(() => loadSid(id), 2000);
|
|
@@ -285,6 +285,7 @@ a.round{color:inherit;text-decoration:none}
|
|
|
285
285
|
.tc{min-width:0}
|
|
286
286
|
.tt{margin:0;font-size:var(--fs-3);line-height:1.55;color:var(--ink);max-width:58ch}
|
|
287
287
|
.turn.u{justify-items:end}
|
|
288
|
+
.turn.u.pend.sending .ub{opacity:.72} /* batch 24 item 2: a sent turn shows at once, a touch lighter until PMOS takes it */
|
|
288
289
|
.ub{background:var(--solid);color:var(--on-solid);padding:11px 16px;border-radius:18px 18px 8px 18px;font-size:var(--fs-3);line-height:1.45;max-width:86%}
|
|
289
290
|
.acard{margin-left:40px;background:var(--well);border-radius:var(--r-in);padding:20px 22px;display:grid;gap:14px;animation:rise .6s var(--ease) both}
|
|
290
291
|
.still .turn,.still .acard{animation:none} /* a redraw of a live chat keeps its turns still (premium-shell.js putHtml) */
|
|
@@ -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
|
}
|
|
@@ -23,6 +23,14 @@ body.wf-page { min-height: 100vh; overflow: hidden; font-family: var(--cz-sans);
|
|
|
23
23
|
.wfl .t { font-size: var(--fs-3); font-weight: 700; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
24
24
|
.wfl .n { font-size: var(--fs-2); color: var(--cz-ink3); font-weight: 600; }
|
|
25
25
|
.wfl.arch .t { color: var(--cz-ink3); }
|
|
26
|
+
.wfl .wfb { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; text-align: left; }
|
|
27
|
+
.wfl .wfb:focus-visible, .wfl .wfs:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; border-radius: 8px; }
|
|
28
|
+
/* the star (batch 24 item 3, canvas DfA): filled on the default; an outline only on hover or focus elsewhere, and always on touch */
|
|
29
|
+
.wfl .wfs { width: 30px; height: 30px; margin: -4px 0; border-radius: 999px; display: inline-grid; place-items: center; border: 0; padding: 0; background: none; color: var(--cz-ink3); cursor: pointer; flex: none; opacity: 0; }
|
|
30
|
+
.wfl:hover .wfs, .wfl:focus-within .wfs, .wfl .wfs.on { opacity: 1; }
|
|
31
|
+
.wfl .wfs:hover { background: var(--cz-card); color: var(--cz-ink2); box-shadow: 0 0 0 1px var(--cz-hair); }
|
|
32
|
+
.wfl .wfs.on, .wfl .wfs.on:hover { color: var(--cz-risk); }
|
|
33
|
+
@media (hover: none) { .wfl .wfs { opacity: 1; } }
|
|
26
34
|
.wf-sub { font-size: var(--fs-1); font-weight: 800; color: var(--cz-ink3); padding: 14px 10px 4px; }
|
|
27
35
|
.wf-page .wfi { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 6px; background: var(--cz-well); color: var(--cz-ink2); flex: none; }
|
|
28
36
|
.wfl .wfi, .wf-head .wfi { background: var(--cz-well2); }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* pm/wf-page.js — Settings › Workflows (initiative workflows per initiative, D99; canvas WfAModel, WfAStage, WfACond, WfARemove, WfANew,
|
|
2
2
|
* WfADraft). One page, reached from Settings:
|
|
3
|
-
* the list every workflow with its count of initiatives and +; Product feature first, archived ones last and quiet
|
|
3
|
+
* the list every workflow with its count of initiatives and +; Product feature first, archived ones last and quiet. The star on a row
|
|
4
|
+
* (batch 24 item 3, canvas DfA) makes that workflow the default a new initiative starts on (POST /api/workflows/default)
|
|
4
5
|
* the picked workflow one numbered list across the To do, In progress and Done columns; drag a stage to reorder it or to move it to
|
|
5
6
|
* another column; Add a stage per column; the dashed Won't do · Archived column (in every workflow); ⋯ = Copy, Archive.
|
|
6
7
|
* The name is the rename press (RnA*: tap it, one field with a check).
|
|
@@ -35,6 +36,8 @@ const DRAG = 'M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01', PLUS = '
|
|
|
35
36
|
const S = { list: null, sel: null, draft: null, edit: null, err: '', msg: '', renaming: false, renErr: '', busy: false, pop: null, chat: null, poll: 0, phoneOpen: false };
|
|
36
37
|
const wfOf = (id) => (S.edit && id === S.edit.id ? S.edit : (S.list || []).find((w) => w.id === id) || null);
|
|
37
38
|
const cur = () => (S.sel && S.sel !== 'new' ? wfOf(S.sel) : null);
|
|
39
|
+
/** A New workflow chat is still going (its key is kept until Create), or PMOS has drafted one. */
|
|
40
|
+
const draftLive = () => !!(store.get(CHAT_KEY) || S.draft);
|
|
38
41
|
const readOnly = (wf) => !!wf && (wf.builtin || wf.id === PRODUCT_FEATURE_ID);
|
|
39
42
|
|
|
40
43
|
function toast(text, undo) {
|
|
@@ -62,19 +65,30 @@ async function readDraft() {
|
|
|
62
65
|
const d = r && r.ok && r.draft ? r.draft.draft || null : null; // { draft, note, at } | null
|
|
63
66
|
const was = JSON.stringify(S.draft), now = JSON.stringify(d);
|
|
64
67
|
S.draft = d;
|
|
68
|
+
if (was !== now && S.sel !== 'new') drawList(); // the draft row's name follows the draft
|
|
65
69
|
if (was !== now && S.sel === 'new') { drawList(); if (d) showDraft(); else if (VW.current() && VW.current().id === 'wf-draft') VW.close(); }
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
/* ── the list (WfAModel's left column) ── */
|
|
73
|
+
// the star (batch 24 item 3, canvas DfA): gold and filled on the default, an outline on hover or focus elsewhere; archived rows have none
|
|
74
|
+
const STAR = 'M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.9 6.8 19.6l1-5.8-4.3-4.1 5.9-.9z';
|
|
75
|
+
function starHtml(w) {
|
|
76
|
+
if (w.archived) return '';
|
|
77
|
+
const label = w.default ? 'Default for new initiatives' : 'Make ' + w.name + ' the default for new initiatives';
|
|
78
|
+
return `<button type="button" class="wfs${w.default ? ' on' : ''}" data-wf-star="${esc(w.id)}" aria-pressed="${w.default ? 'true' : 'false'}" aria-label="${esc(label)}" title="${w.default ? 'Default for new initiatives' : 'Make default'}">`
|
|
79
|
+
+ `<svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="${w.default ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${STAR}"/></svg></button>`;
|
|
80
|
+
}
|
|
69
81
|
function rowHtml(w) {
|
|
70
82
|
const n = w.count > 0 ? `<span class="n">${w.count}</span>` : ''; // zero is silence
|
|
71
|
-
return `<
|
|
83
|
+
return `<div class="wfl${S.sel === w.id ? ' sel' : ''}${w.archived ? ' arch' : ''}"><button type="button" class="wfb" data-wf="${esc(w.id)}"${S.sel === w.id ? ' aria-current="true"' : ''}>${wfIcon(w, 22)}<span class="t">${esc(w.name)}</span></button>${starHtml(w)}${n}</div>`;
|
|
72
84
|
}
|
|
73
85
|
function drawList() {
|
|
74
86
|
const el = $('#wfList'); if (!el) return;
|
|
75
87
|
const L = S.list;
|
|
76
88
|
const on = (L || []).filter((w) => !w.archived), off = (L || []).filter((w) => w.archived);
|
|
77
|
-
|
|
89
|
+
// a live draft keeps its row while another workflow is open, so its chat and draft are one press away (batch 24 item 4)
|
|
90
|
+
const onNew = S.sel === 'new';
|
|
91
|
+
const newRow = onNew || draftLive() ? `<button type="button" class="wfl${onNew ? ' sel' : ''}" data-wf="new"${onNew ? ' aria-current="true"' : ''}><span class="wfi new" aria-hidden="true">${svg(PLUS, 14)}</span><span class="t">${esc((S.draft && S.draft.name) || 'New workflow')}</span><span class="n">draft</span></button>` : '';
|
|
78
92
|
el.innerHTML = `<div class="lhead"><h2>Workflows</h2><button type="button" class="ib sm" data-wf-new aria-label="New workflow: describe it and PMOS drafts it" title="New workflow: describe it and PMOS drafts it">${svg(PLUS, 16)}</button></div>`
|
|
79
93
|
+ (L === null ? '<p class="wf-mut wf-small wf-pad">Reading the workflows.</p>' : on.map(rowHtml).join('') + newRow
|
|
80
94
|
+ (off.length ? `<p class="wf-sub">Archived</p>${off.map(rowHtml).join('')}` : ''));
|
|
@@ -234,6 +248,17 @@ async function archive(wf, on) {
|
|
|
234
248
|
await load();
|
|
235
249
|
toast(on ? 'Archived ' + wf.name + '.' : 'Restored ' + wf.name + '.', () => archive(wf, !on));
|
|
236
250
|
}
|
|
251
|
+
async function makeDefault(id, undoTo) {
|
|
252
|
+
const w = (S.list || []).find((x) => x.id === id);
|
|
253
|
+
if (!w || (w.default && !undoTo)) return;
|
|
254
|
+
const was = (S.list || []).find((x) => x.default);
|
|
255
|
+
const r = await post('/api/workflows/default', { id });
|
|
256
|
+
if (!(r && r.ok)) { S.msg = (r && r.error) || 'Not changed.'; drawMain(); return; }
|
|
257
|
+
for (const x of S.list) { if (x.id === id) x.default = true; else delete x.default; }
|
|
258
|
+
drawList();
|
|
259
|
+
const b = document.querySelector(`#wfList [data-wf-star="${CSS.escape(id)}"]`); if (b) b.focus();
|
|
260
|
+
if (!undoTo) toast('New initiatives start on ' + w.name + '.', was ? () => makeDefault(was.id, true) : null);
|
|
261
|
+
}
|
|
237
262
|
async function rename(wf, name) {
|
|
238
263
|
const v = String(name || '').trim();
|
|
239
264
|
if (!v || v === wf.name) { S.renaming = false; S.renErr = ''; drawMain(); return; }
|
|
@@ -389,8 +414,10 @@ function bindDrag(main) {
|
|
|
389
414
|
function bind() {
|
|
390
415
|
$('#wfList').addEventListener('click', (e) => {
|
|
391
416
|
if (e.target.closest('[data-wf-new]')) { startNew(); return; }
|
|
392
|
-
const
|
|
393
|
-
|
|
417
|
+
const star = e.target.closest('[data-wf-star]'); if (star) { makeDefault(star.dataset.wfStar); return; }
|
|
418
|
+
const row = e.target.closest('.wfl');
|
|
419
|
+
const b = e.target.closest('[data-wf]') || (row && row.querySelector('[data-wf]')); if (!b) return;
|
|
420
|
+
if (b.dataset.wf === 'new') { if (S.sel !== 'new') startNew(); return; } // back to the live draft: its chat and draft viewer
|
|
394
421
|
select(b.dataset.wf);
|
|
395
422
|
});
|
|
396
423
|
const main = $('#wfMain');
|
|
@@ -431,5 +458,6 @@ export function boot() {
|
|
|
431
458
|
draw();
|
|
432
459
|
load();
|
|
433
460
|
if (new URLSearchParams(location.search).get('new') === '1') startNew();
|
|
461
|
+
else if (store.get(CHAT_KEY)) readDraft(); // a draft from before shows as its row, by name
|
|
434
462
|
}
|
|
435
463
|
if (typeof document !== 'undefined' && document.getElementById('wfMain')) boot();
|
|
@@ -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">
|