create-pmos 0.52.0 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/mods/pmos-cockpit/README.md +13 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
- package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/plan-items-stage.js +271 -0
- package/payload/scripts/pmos-log.sh +18 -7
- package/payload/scripts/workflow-state.js +28 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -0
- package/workspace/REMOTE.md +24 -63
- package/workspace/bin/pmos.js +19 -3
- package/workspace/cut-stub.js +40 -0
- package/workspace/first-paint-audit.js +145 -0
- package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
- package/workspace/fixtures/initiatives-api/list.json +176 -0
- package/workspace/fixtures/initiatives-api/post-403.json +5 -0
- package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
- package/workspace/fixtures/initiatives-api/sheet.json +112 -0
- package/workspace/fixtures/report-api/draft.json +177 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
- package/workspace/fixtures/today-api/provider-rows.json +204 -0
- package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
- package/workspace/guide/board.md +81 -0
- package/workspace/guide/breakdown.md +154 -0
- package/workspace/guide/command-palette.md +75 -0
- package/workspace/guide/index.md +28 -17
- package/workspace/guide/initiatives-roadmap.md +130 -0
- package/workspace/guide/initiatives-screen.md +89 -0
- package/workspace/guide/initiatives-timeline.md +57 -0
- package/workspace/guide/integrated-work.md +94 -0
- package/workspace/guide/knowledge-graph.md +3 -68
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +102 -32
- package/workspace/guide/live-notes.md +3 -3
- package/workspace/guide/pm-only-acts.md +89 -0
- package/workspace/guide/pm-only-lines.md +61 -0
- package/workspace/guide/proactive-engine.md +21 -5
- package/workspace/guide/reports-page.md +80 -0
- package/workspace/guide/reports.md +73 -0
- package/workspace/guide/timeline.md +70 -0
- package/workspace/guide/today.md +85 -0
- package/workspace/guide/validate-with-typesafe.md +1 -1
- package/workspace/lib/actor.js +91 -0
- package/workspace/lib/board-cache.js +32 -2
- package/workspace/lib/board-cli.js +108 -0
- package/workspace/lib/board-fold.js +232 -0
- package/workspace/lib/breakdown-export.js +75 -0
- package/workspace/lib/brief.js +20 -2
- package/workspace/lib/capture.js +98 -0
- package/workspace/lib/engine/plan-items-stage.js +271 -0
- package/workspace/lib/engine/workflow-state.js +28 -2
- package/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/goals.js +165 -0
- package/workspace/lib/grant-mint.js +113 -0
- package/workspace/lib/health-rules-factory.js +92 -0
- package/workspace/lib/health-rules-plan.js +164 -0
- package/workspace/lib/health.js +79 -0
- package/workspace/lib/initiative-cli.js +222 -0
- package/workspace/lib/initiative-display.js +183 -0
- package/workspace/lib/initiative-needs.js +127 -0
- package/workspace/lib/initiative-rows.js +210 -0
- package/workspace/lib/initiative-writer.js +141 -0
- package/workspace/lib/issues.js +206 -0
- package/workspace/lib/item-cli.js +195 -0
- package/workspace/lib/item-spans.js +137 -0
- package/workspace/lib/launcher-cli.js +207 -0
- package/workspace/lib/measure-cli.js +100 -0
- package/workspace/lib/measurements.js +189 -0
- package/workspace/lib/mobile-nav.js +57 -53
- package/workspace/lib/needs-providers.js +120 -0
- package/workspace/lib/placements.js +440 -0
- package/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/plan-ledgers.js +85 -0
- package/workspace/lib/plan-rules.js +72 -0
- package/workspace/lib/plan-tree.js +380 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +69 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +9 -6
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +93 -1
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +578 -0
- package/workspace/lib/report-needs.js +213 -0
- package/workspace/lib/route-modules.js +128 -0
- package/workspace/lib/routes-board.js +91 -0
- package/workspace/lib/routes-capture.js +52 -0
- package/workspace/lib/routes-initiatives.js +371 -0
- package/workspace/lib/routes-reports.js +163 -0
- package/workspace/lib/routes-timeline.js +197 -0
- package/workspace/lib/routes-today.js +32 -0
- package/workspace/lib/today-cli.js +99 -0
- package/workspace/lib/today.js +333 -0
- package/workspace/lib/triage.js +163 -0
- package/workspace/lib/verify-work-cli.js +81 -0
- package/workspace/lib/work-check-needs.js +99 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +4 -5
- package/workspace/public/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +40 -19
- package/workspace/public/pm/app.js +330 -30
- package/workspace/public/pm/bd-board.js +113 -0
- package/workspace/public/pm/bd-drag.js +59 -0
- package/workspace/public/pm/bd-model.js +152 -0
- package/workspace/public/pm/bd-timeline.js +233 -0
- package/workspace/public/pm/bd-tree.js +244 -0
- package/workspace/public/pm/board-copy.js +57 -0
- package/workspace/public/pm/breakdown-api.js +58 -0
- package/workspace/public/pm/breakdown-copy.js +138 -0
- package/workspace/public/pm/components.css +3135 -0
- package/workspace/public/pm/cycle-review.js +125 -0
- package/workspace/public/pm/data.js +40 -187
- package/workspace/public/pm/dest-board.js +438 -0
- package/workspace/public/pm/dest-initiatives.js +134 -0
- package/workspace/public/pm/dest-reports.js +270 -0
- package/workspace/public/pm/dest-settings.js +217 -0
- package/workspace/public/pm/dest-today.js +182 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +175 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +99 -0
- package/workspace/public/pm/handoff-box.js +82 -0
- package/workspace/public/pm/http.js +88 -0
- package/workspace/public/pm/icons.js +53 -0
- package/workspace/public/pm/initiative-list.js +218 -0
- package/workspace/public/pm/initiative-sheet.js +261 -0
- package/workspace/public/pm/initiatives-api.js +59 -0
- package/workspace/public/pm/initiatives-copy.js +215 -0
- package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
- package/workspace/public/pm/initiatives-timeline.js +410 -0
- package/workspace/public/pm/initiatives-ui.js +123 -0
- package/workspace/public/pm/item-sheet.js +251 -0
- package/workspace/public/pm/keep-card.js +204 -0
- package/workspace/public/pm/lifecycle.js +7 -1
- package/workspace/public/pm/mobile.css +49 -20
- package/workspace/public/pm/mobile.js +157 -29
- package/workspace/public/pm/modes-live.js +7 -883
- package/workspace/public/pm/modes.js +32 -1024
- package/workspace/public/pm/new-initiative-sheet.js +74 -0
- package/workspace/public/pm/next-step-ui.js +3 -2
- package/workspace/public/pm/palette-copy.js +51 -0
- package/workspace/public/pm/palette.js +312 -0
- package/workspace/public/pm/panel-breakdown.js +702 -0
- package/workspace/public/pm/panel-cockpit.js +63 -295
- package/workspace/public/pm/panel-detail.js +25 -172
- package/workspace/public/pm/panel-initiatives.js +1 -19
- package/workspace/public/pm/panel-repos.js +31 -81
- package/workspace/public/pm/panel-tabs.js +200 -111
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/portfolio-report.js +61 -0
- package/workspace/public/pm/primitives.js +656 -0
- package/workspace/public/pm/print.css +33 -0
- package/workspace/public/pm/provider-cards.js +136 -0
- package/workspace/public/pm/registry.js +194 -0
- package/workspace/public/pm/release-card.js +110 -0
- package/workspace/public/pm/report-view.js +309 -0
- package/workspace/public/pm/reports-copy.js +114 -0
- package/workspace/public/pm/roadmap.js +271 -0
- package/workspace/public/pm/sig-watch.js +41 -0
- package/workspace/public/pm/state.js +8 -4
- package/workspace/public/pm/status-banner.js +13 -5
- package/workspace/public/pm/tab-doc.js +2 -4
- package/workspace/public/pm/tab-term.js +12 -2
- package/workspace/public/pm/tab-web.js +3 -2
- package/workspace/public/pm/timeline-copy.js +48 -0
- package/workspace/public/pm/today-cards.js +244 -0
- package/workspace/public/pm/today-copy.js +126 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/public/pm/tokens.css +16 -18
- package/workspace/public/pm/topbar.js +102 -0
- package/workspace/public/pm/triage-card.js +60 -0
- package/workspace/public/pm/undo-hold.js +103 -0
- package/workspace/public/pm/view-addrepo.js +2 -1
- package/workspace/public/pm/view-livenotes.js +3 -1
- package/workspace/public/pm/view-today.js +7 -162
- package/workspace/public/vendor/Sortable.min.js +2 -0
- package/workspace/public/vendor/lucide-sprite.js +79 -0
- package/workspace/replaced-tests.json +2877 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +446 -40
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +21 -3
- package/workspace/test-lists/base.json +6 -0
- package/workspace/test-lists/run2-a.json +6 -0
- package/workspace/test-lists/run2-b.json +13 -0
- package/workspace/test-lists/run2-c.json +19 -0
- package/workspace/test-lists/run2-d.json +22 -0
- package/workspace/test-lists/run3-0.json +5 -0
- package/workspace/test-lists/run3-a.json +5 -0
- package/workspace/test-lists/run3-b.json +6 -0
- package/workspace/test-lists/run3-c.json +15 -0
- package/workspace/test-lists/run3-d.json +6 -0
- package/workspace/test-lists/run4-a.json +8 -0
- package/workspace/test-lists/run4-b.json +6 -0
- package/workspace/test-lists/run4-c.json +13 -0
- package/workspace/test-lists/run4-d.json +9 -0
- package/workspace/test-lists/run5-a.json +6 -0
- package/workspace/test-lists/run5-b.json +6 -0
- package/workspace/test-lists/run5-c.json +15 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -0
- package/workspace/test-run.js +27 -1
- package/workspace/third-party/README.md +44 -0
- package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
- package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
- package/workspace/design-parity.js +0 -673
- package/workspace/public/pm/tab-task.js +0 -86
- package/workspace/public/pm/view-guide.js +0 -141
- package/workspace/public/pm/view-knowledge.js +0 -225
- package/workspace/public/pm/view-memory.js +0 -122
- package/workspace/public/pm/view-portfolio.js +0 -153
- package/workspace/public/pm/view-reports.js +0 -190
- package/workspace/public/pm/view-scheduled.js +0 -161
- package/workspace/public/pm/view-skills.js +0 -216
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/* pm/item-sheet.js — the item sheet (pm-os-absorb Run 1, PRD R13).
|
|
2
|
+
*
|
|
3
|
+
* One item, every field, in the order R13 sets: state buttons, Title, Owner, Stage, After, Start and Due, Milestone,
|
|
4
|
+
* Blocked (with its reason), Note, Hand to agent, History. Under the state buttons, two lines show only when they apply:
|
|
5
|
+
* "Proposed by …" with Keep all, for an item an agent proposed; and Add evidence, for an item that reads Done with no
|
|
6
|
+
* evidence (it writes the same done → done move with evidence that `pmos item move … done --evidence` writes). A 420px side sheet above 760px, a bottom sheet below it, with
|
|
7
|
+
* a sticky row: Withdraw (held for Undo) on the left, Close on the right.
|
|
8
|
+
*
|
|
9
|
+
* Every field commits by itself and writes one line: text on Enter or blur, selects, dates and boxes on change. A press
|
|
10
|
+
* on a state button writes `moved` at once, except Done, which is held for Undo. When the item changes underneath — a
|
|
11
|
+
* poll, an agent's write, the PM's own write coming back — the sheet refreshes every field the PM is not typing in.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { el } from './dom.js';
|
|
15
|
+
import { sheet, tabs, field, button, iconButton } from './primitives.js';
|
|
16
|
+
import { icon } from './icons.js';
|
|
17
|
+
import { COPY, STATES, ago, dayOf, ownerLabel } from './breakdown-copy.js';
|
|
18
|
+
import { stageLabel } from './bd-model.js';
|
|
19
|
+
import { handoffBox } from './handoff-box.js';
|
|
20
|
+
|
|
21
|
+
const ownerKind = (o) => (o === 'you' || o === 'agent' ? o : String(o || '').split(':')[0] || 'you');
|
|
22
|
+
const ownerName = (o) => { const s = String(o || ''); const i = s.indexOf(':'); return i > 0 ? s.slice(i + 1) : ''; };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* openItemSheet(ctx, id, { handoff }) -> { id, refresh(), close() }
|
|
26
|
+
* ctx: { ws, slug, view() -> Layout, move(id,to), setFields(id,fields), flag(id,blocked,reason), withdraw(id),
|
|
27
|
+
* attachEvidence(id, 'kind:ref'), keep(batch), sessions() -> [], onClose(id) }
|
|
28
|
+
*/
|
|
29
|
+
export function openItemSheet(ctx, id, { handoff = false } = {}) {
|
|
30
|
+
const L0 = ctx.view();
|
|
31
|
+
const it0 = L0 && L0.items[id];
|
|
32
|
+
if (!it0) return null;
|
|
33
|
+
const item = () => { const L = ctx.view(); return L && L.items[id]; };
|
|
34
|
+
const focused = (node) => node && document.activeElement === node;
|
|
35
|
+
|
|
36
|
+
/* 1 · state buttons */
|
|
37
|
+
const stateSlot = el('div', { class: 'bd-group', 'data-bd-field': 'state' });
|
|
38
|
+
const drawState = (it) => {
|
|
39
|
+
while (stateSlot.firstChild) stateSlot.removeChild(stateSlot.firstChild);
|
|
40
|
+
stateSlot.appendChild(tabs({
|
|
41
|
+
variant: 'segmented', role: 'radiogroup', full: true, ariaLabel: COPY.labels.state, value: it.state,
|
|
42
|
+
items: STATES.map((s) => ({ id: s, label: COPY.states[s] })),
|
|
43
|
+
onChange: (to) => { const cur = item(); if (cur && cur.state !== to) ctx.move(id, to); },
|
|
44
|
+
}));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/* under the state: proposed (Keep all) and no evidence (Add evidence) */
|
|
48
|
+
const noteSlot = el('div', { class: 'bd-group', 'data-bd-status': '' }); // part of field 1, the state
|
|
49
|
+
const evidence = field({
|
|
50
|
+
label: COPY.addEvidence, kind: 'text', placeholder: COPY.evidenceHint, attrs: { 'data-bd-input': 'evidence', spellcheck: 'false', maxlength: '220' },
|
|
51
|
+
onCommit: (v) => {
|
|
52
|
+
const t = v.trim();
|
|
53
|
+
if (!t || !ctx.attachEvidence) return;
|
|
54
|
+
// The server checks the form (commit:, pr:, file:, session:, note:). A refusal shows as a toast and leaves the
|
|
55
|
+
// text in the field to correct (Enter sends it again). Once it is attached the field goes, and focus goes back to
|
|
56
|
+
// the state buttons rather than falling to the page.
|
|
57
|
+
Promise.resolve(ctx.attachEvidence(id, t)).then((out) => {
|
|
58
|
+
const typed = evidence.control.value;
|
|
59
|
+
evidence.control.resetCommitted('');
|
|
60
|
+
if (!out || out.ok === false) { evidence.control.value = typed; return; }
|
|
61
|
+
const cur = item();
|
|
62
|
+
if (cur) drawNote(cur, true);
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
/* Drawn again only when what it says changes, so a refresh (every poll) keeps the nodes and the focus in them. While
|
|
67
|
+
* the evidence field has focus it stays until the write settles. When the focused control goes with the line (Keep
|
|
68
|
+
* all, the attached evidence), focus goes to the state buttons rather than falling to the page. */
|
|
69
|
+
let noteKey = null;
|
|
70
|
+
const drawNote = (it, force = false) => {
|
|
71
|
+
const L = ctx.view();
|
|
72
|
+
const batch = it.proposed ? ((L && L.batches) || []).find((b) => (b.items || []).includes(id)) : null;
|
|
73
|
+
const needsEvidence = it.state === 'done' && !!it.noEvidence;
|
|
74
|
+
const key = JSON.stringify([it.proposed ? [batch ? batch.byLabel : it.byLabel, dayOf(it.createdAt), !!(batch && ctx.keep)] : null, needsEvidence]);
|
|
75
|
+
if (key === noteKey) return;
|
|
76
|
+
if (!force && document.activeElement === evidence.control) return;
|
|
77
|
+
noteKey = key;
|
|
78
|
+
const had = noteSlot.contains(document.activeElement);
|
|
79
|
+
while (noteSlot.firstChild) noteSlot.removeChild(noteSlot.firstChild);
|
|
80
|
+
if (it.proposed) {
|
|
81
|
+
noteSlot.appendChild(el('div', { class: 'bd-proposed', 'data-bd-proposed': '' },
|
|
82
|
+
el('span', { class: 'bd-muted' }, COPY.proposedBy(batch ? batch.byLabel : it.byLabel, dayOf(it.createdAt))),
|
|
83
|
+
batch && ctx.keep ? button({ label: COPY.keepAll, variant: 'outline', size: 'sm', attrs: { 'data-bd-keep-all': '' }, onClick: () => ctx.keep(batch) }) : null));
|
|
84
|
+
}
|
|
85
|
+
if (needsEvidence) noteSlot.appendChild(evidence);
|
|
86
|
+
noteSlot.hidden = !noteSlot.firstChild;
|
|
87
|
+
if (had && !noteSlot.contains(document.activeElement)) { const b = stateSlot.querySelector('[aria-checked="true"]'); if (b) b.focus(); }
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/* 2 · title */
|
|
91
|
+
const title = field({
|
|
92
|
+
label: COPY.labels.title, kind: 'title', value: it0.title, attrs: { 'data-bd-input': 'title', maxlength: '200' },
|
|
93
|
+
onCommit: (v) => { const t = v.trim(); const cur = item(); if (t && cur && t !== cur.title) ctx.setFields(id, { title: t }); else if (cur) title.control.resetCommitted(cur.title); },
|
|
94
|
+
});
|
|
95
|
+
title.setAttribute('data-bd-field', 'title');
|
|
96
|
+
|
|
97
|
+
/* 3 · owner (+ a name for a person or a tracker reference) and 4 · stage */
|
|
98
|
+
const ownerNameField = field({
|
|
99
|
+
label: COPY.labels.ownerName, kind: 'text', value: ownerName(it0.owner), attrs: { 'data-bd-input': 'owner-name', maxlength: '60' },
|
|
100
|
+
onCommit: (v) => {
|
|
101
|
+
const k = owner.control.value; const name = v.trim();
|
|
102
|
+
if ((k === 'person' || k === 'tracker') && name) { const o = k + ':' + name; const cur = item(); if (cur && cur.owner !== o) ctx.setFields(id, { owner: o }); }
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const owner = field({
|
|
106
|
+
label: COPY.labels.owner, kind: 'select', value: ownerKind(it0.owner), attrs: { 'data-bd-input': 'owner' },
|
|
107
|
+
options: ['you', 'agent', 'person', 'tracker'].map((k) => ({ value: k, label: COPY.owners[k] })),
|
|
108
|
+
onCommit: (k) => {
|
|
109
|
+
showOwnerName(k);
|
|
110
|
+
if (k === 'you' || k === 'agent') { const cur = item(); if (cur && cur.owner !== k) ctx.setFields(id, { owner: k }); }
|
|
111
|
+
else { ownerNameField.querySelector('label').textContent = k === 'tracker' ? COPY.labels.trackerRef : COPY.labels.ownerName; ownerNameField.control.focus(); }
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
owner.setAttribute('data-bd-field', 'owner');
|
|
115
|
+
const showOwnerName = (k) => { ownerNameField.hidden = !(k === 'person' || k === 'tracker'); };
|
|
116
|
+
const stage = field({
|
|
117
|
+
label: COPY.labels.stage, kind: 'select', value: it0.stage, attrs: { 'data-bd-input': 'stage' },
|
|
118
|
+
options: (L0.stages || []).map((s) => ({ value: s.id, label: s.label })),
|
|
119
|
+
onCommit: (v) => { const cur = item(); if (cur && cur.stage !== v) ctx.setFields(id, { stage: v }); },
|
|
120
|
+
});
|
|
121
|
+
stage.setAttribute('data-bd-field', 'stage');
|
|
122
|
+
|
|
123
|
+
/* 5 · after */
|
|
124
|
+
const afterSlot = el('div', { class: 'bd-group', 'data-bd-field': 'after' });
|
|
125
|
+
const otherId = field({ label: COPY.otherItemId, kind: 'text', attrs: { 'data-bd-input': 'after-other', spellcheck: 'false' } });
|
|
126
|
+
const drawAfter = (it) => {
|
|
127
|
+
while (afterSlot.firstChild) afterSlot.removeChild(afterSlot.firstChild);
|
|
128
|
+
const L = ctx.view();
|
|
129
|
+
const after = it.after || [];
|
|
130
|
+
const nameOf = (x) => (L.items[x] ? L.items[x].title : x);
|
|
131
|
+
const chips = after.length
|
|
132
|
+
? el('div', { class: 'bd-chips' }, after.map((x) => el('span', { class: 'bd-chip', 'data-after': x },
|
|
133
|
+
el('span', null, nameOf(x)),
|
|
134
|
+
iconButton({ icon: 'x', size: 'sm', ariaLabel: COPY.aria.remove(nameOf(x)), onClick: () => ctx.setFields(id, { after: after.filter((y) => y !== x) }) }))))
|
|
135
|
+
: el('div', { class: 'bd-muted' }, COPY.afterNone);
|
|
136
|
+
const choices = Object.values(L.items).filter((x) => x.id !== id && !after.includes(x.id) && !x.pending);
|
|
137
|
+
const add = field({
|
|
138
|
+
label: COPY.addPredecessor, kind: 'select', value: '', hideLabel: true, attrs: { 'data-bd-input': 'after-add' },
|
|
139
|
+
options: [{ value: '', label: COPY.addPredecessor + '…' }, ...choices.map((x) => ({ value: x.id, label: x.title }))],
|
|
140
|
+
onCommit: (v) => { if (v) ctx.setFields(id, { after: [...after, v] }); },
|
|
141
|
+
});
|
|
142
|
+
const addOther = button({
|
|
143
|
+
label: COPY.add, variant: 'outline', size: 'sm', attrs: { 'data-bd-after-other': '' },
|
|
144
|
+
onClick: () => { const v = otherId.control.value.trim(); if (v && !after.includes(v)) { otherId.control.value = ''; ctx.setFields(id, { after: [...after, v] }); } },
|
|
145
|
+
});
|
|
146
|
+
afterSlot.append(el('span', { class: 'bd-group__label' }, COPY.labels.after), chips, add,
|
|
147
|
+
el('div', { class: 'bd-after-add' }, el('div', { class: 'bd-after-add__field' }, otherId), addOther));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/* 6 · start and due, 7 · milestone, 8 · blocked */
|
|
151
|
+
const start = field({ label: COPY.labels.start, kind: 'date', value: it0.start || '', attrs: { 'data-bd-input': 'start' }, onCommit: (v) => ctx.setFields(id, { start: v || null }) });
|
|
152
|
+
const due = field({ label: COPY.labels.due, kind: 'date', value: it0.due || '', attrs: { 'data-bd-input': 'due' }, onCommit: (v) => ctx.setFields(id, { due: v || null }) });
|
|
153
|
+
start.setAttribute('data-bd-field', 'start'); due.setAttribute('data-bd-field', 'due');
|
|
154
|
+
const milestone = field({ label: COPY.labels.milestone, kind: 'check', value: !!it0.milestone, attrs: { 'data-bd-input': 'milestone' }, onCommit: (v) => ctx.setFields(id, { milestone: !!v }) });
|
|
155
|
+
milestone.setAttribute('data-bd-field', 'milestone');
|
|
156
|
+
const reason = field({
|
|
157
|
+
label: COPY.labels.blockedBecause, kind: 'text', value: (it0.blocked && it0.blocked.reason) || '', attrs: { 'data-bd-input': 'blocked-reason', maxlength: '200' },
|
|
158
|
+
onCommit: (v) => { const r = v.trim(); if (blocked.control.checked && r) ctx.flag(id, true, r); },
|
|
159
|
+
});
|
|
160
|
+
reason.setAttribute('data-bd-field', 'blocked-reason');
|
|
161
|
+
const blocked = field({
|
|
162
|
+
label: COPY.labels.blocked, kind: 'check', value: !!it0.blocked, attrs: { 'data-bd-input': 'blocked' },
|
|
163
|
+
onCommit: (on) => {
|
|
164
|
+
reason.hidden = !on;
|
|
165
|
+
const cur = item();
|
|
166
|
+
if (on) { const r = reason.control.value.trim(); if (r) ctx.flag(id, true, r); else reason.control.focus(); }
|
|
167
|
+
else if (cur && cur.blocked) ctx.flag(id, false);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
blocked.setAttribute('data-bd-field', 'blocked');
|
|
171
|
+
reason.hidden = !it0.blocked;
|
|
172
|
+
|
|
173
|
+
/* 9 · note */
|
|
174
|
+
const note = field({ label: COPY.labels.note, kind: 'textarea', value: it0.note || '', attrs: { 'data-bd-input': 'note', maxlength: '1000' }, onCommit: (v) => ctx.setFields(id, { note: v.trim() ? v : null }) });
|
|
175
|
+
note.setAttribute('data-bd-field', 'note');
|
|
176
|
+
|
|
177
|
+
/* 10 · hand to agent */
|
|
178
|
+
const handSlot = el('div', { 'data-bd-field': 'handoff' });
|
|
179
|
+
const handButton = () => button({
|
|
180
|
+
label: COPY.handToAgent, variant: 'outline', block: true, icon: 'bot', iconEnd: 'chevron-right', attrs: { 'data-bd-hand': '' },
|
|
181
|
+
onClick: () => showHandoff(),
|
|
182
|
+
});
|
|
183
|
+
const showHandoff = () => {
|
|
184
|
+
while (handSlot.firstChild) handSlot.removeChild(handSlot.firstChild);
|
|
185
|
+
handSlot.appendChild(handoffBox({
|
|
186
|
+
ws: ctx.ws, slug: ctx.slug, item: item() || it0, sessions: ctx.sessions(),
|
|
187
|
+
onCancel: () => { while (handSlot.firstChild) handSlot.removeChild(handSlot.firstChild); const b = handButton(); handSlot.appendChild(b); b.focus(); },
|
|
188
|
+
onStarted: () => sh.close(),
|
|
189
|
+
}));
|
|
190
|
+
};
|
|
191
|
+
handSlot.appendChild(handButton());
|
|
192
|
+
|
|
193
|
+
/* 11 · history */
|
|
194
|
+
const histList = el('ol', { class: 'bd-hist', 'aria-label': COPY.labels.history });
|
|
195
|
+
const drawHistory = (it) => {
|
|
196
|
+
while (histList.firstChild) histList.removeChild(histList.firstChild);
|
|
197
|
+
const rows = [...(it.history || [])].reverse();
|
|
198
|
+
for (const h of rows) {
|
|
199
|
+
histList.appendChild(el('li', { class: 'bd-hist__row' },
|
|
200
|
+
el('span', { class: 'bd-hist__by' }, h.byLabel || ''),
|
|
201
|
+
el('span', { class: 'bd-hist__text' }, h.text || h.ev),
|
|
202
|
+
el('span', { class: 'bd-hist__when', title: h.at || '' }, ago(h.at))));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const history = el('div', { 'data-bd-field': 'history' },
|
|
206
|
+
el('div', { class: 'bd-hist__head' }, icon('history'), el('span', null, COPY.labels.history)), histList);
|
|
207
|
+
|
|
208
|
+
const withdraw = button({ label: COPY.labels.withdraw, variant: 'danger', attrs: { 'data-bd-withdraw': '' }, onClick: () => { sh.close(); ctx.withdraw(id); } });
|
|
209
|
+
const closeBtn = button({ label: COPY.labels.close, variant: 'outline', attrs: { 'data-bd-close': '' }, onClick: () => sh.close() });
|
|
210
|
+
|
|
211
|
+
const sh = sheet({
|
|
212
|
+
mode: 'auto', title: it0.title, sub: ctx.slug + ' · ' + stageLabel(L0, it0.stage),
|
|
213
|
+
attrs: { 'data-bd-sheet': id },
|
|
214
|
+
body: [stateSlot, noteSlot, title, el('div', { class: 'bd-grid2' }, el('div', { class: 'bd-group' }, owner, ownerNameField), stage),
|
|
215
|
+
afterSlot, el('div', { class: 'bd-grid2' }, start, due), el('div', { class: 'bd-checks' }, milestone, blocked), reason, note, handSlot, history],
|
|
216
|
+
actions: [withdraw, closeBtn],
|
|
217
|
+
onClose: () => { if (ctx.onClose) ctx.onClose(id); },
|
|
218
|
+
});
|
|
219
|
+
showOwnerName(ownerKind(it0.owner));
|
|
220
|
+
|
|
221
|
+
const refresh = () => {
|
|
222
|
+
const it = item();
|
|
223
|
+
if (!it) { sh.close(); return; }
|
|
224
|
+
const L = ctx.view();
|
|
225
|
+
sh.setHead(it.title, ctx.slug + ' · ' + stageLabel(L, it.stage));
|
|
226
|
+
const tabFocused = stateSlot.contains(document.activeElement);
|
|
227
|
+
drawState(it);
|
|
228
|
+
drawNote(it);
|
|
229
|
+
if (tabFocused) { const b = stateSlot.querySelector('[aria-checked="true"]'); if (b) b.focus(); }
|
|
230
|
+
if (!focused(title.control)) title.control.resetCommitted(it.title);
|
|
231
|
+
if (!focused(owner.control)) { owner.control.value = ownerKind(it.owner); showOwnerName(ownerKind(it.owner)); }
|
|
232
|
+
if (!focused(ownerNameField.control) && ownerName(it.owner)) ownerNameField.control.resetCommitted(ownerName(it.owner));
|
|
233
|
+
if (!focused(stage.control)) stage.control.value = it.stage;
|
|
234
|
+
if (!afterSlot.contains(document.activeElement)) drawAfter(it);
|
|
235
|
+
if (!focused(start.control)) start.control.value = it.start || '';
|
|
236
|
+
if (!focused(due.control)) due.control.value = it.due || '';
|
|
237
|
+
milestone.control.checked = !!it.milestone;
|
|
238
|
+
if (!focused(reason.control) && !focused(blocked.control)) {
|
|
239
|
+
blocked.control.checked = !!it.blocked;
|
|
240
|
+
reason.control.resetCommitted((it.blocked && it.blocked.reason) || '');
|
|
241
|
+
reason.hidden = !it.blocked;
|
|
242
|
+
}
|
|
243
|
+
if (!focused(note.control)) note.control.resetCommitted(it.note || '');
|
|
244
|
+
drawHistory(it);
|
|
245
|
+
};
|
|
246
|
+
drawState(it0); drawNote(it0); drawAfter(it0); drawHistory(it0);
|
|
247
|
+
if (handoff) showHandoff();
|
|
248
|
+
else { const b = stateSlot.querySelector('[aria-checked="true"]'); if (b) b.focus(); }
|
|
249
|
+
return { id, refresh, close: () => sh.close(), get open() { return sh.open; } };
|
|
250
|
+
}
|
|
251
|
+
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/* pm/keep-card.js — the Keep card, and the breakdown rows of Today's needs-you list (pm-os-absorb Run 1, PRD R21/R23).
|
|
2
|
+
*
|
|
3
|
+
* An agent's items arrive PROPOSED: they show in the Tree and on the Board but count in no progress until the PM keeps
|
|
4
|
+
* them. One card per batch (everything one agent session proposed on one initiative) offers Keep or Withdraw all.
|
|
5
|
+
* Tree "Agent · sess-3 proposed 9 items on 5 Oct." [Keep] [Withdraw all]
|
|
6
|
+
* Today • <initiative> · 9 items · agent · sess-3 [Keep] [Withdraw all]
|
|
7
|
+
* The batch is named by the session label the rows already show, so two batches on one initiative read apart.
|
|
8
|
+
* Keep writes one `kept` line, and only from this page (the server refuses `kept` from anywhere else). Withdraw all goes
|
|
9
|
+
* through the Undo hold and then writes one `withdrawn` per item in one request. The card reads the batch as the server
|
|
10
|
+
* last sent it, so an item proposed after the read stays proposed and appears on the next refresh as a new count.
|
|
11
|
+
*
|
|
12
|
+
* Today's other two breakdown rows — an agent Done with no evidence, and a ledger line that has no effect (a forged Keep) —
|
|
13
|
+
* render as plain cards that open the initiative. All three come from the existing GET /api/needs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { el } from './dom.js';
|
|
17
|
+
import { button, toast, ensureComponentsCss } from './primitives.js';
|
|
18
|
+
import { COPY, dayOf } from './breakdown-copy.js';
|
|
19
|
+
import { postEvents, getNeeds } from './breakdown-api.js';
|
|
20
|
+
import { hold, isHeld } from './undo-hold.js';
|
|
21
|
+
import { wsOf } from './data.js';
|
|
22
|
+
import { state, setState, FIXTURE } from './state.js';
|
|
23
|
+
import { registerTodaySource } from './registry.js';
|
|
24
|
+
import { records } from './board-read.js';
|
|
25
|
+
import { COPY as TODAY } from './today-copy.js';
|
|
26
|
+
import { todayCard, ensureFeed, onFeed } from './today-cards.js';
|
|
27
|
+
|
|
28
|
+
export const keepKey = (ws, initiative, by) => 'batch:' + ws + ':' + initiative + ':' + by;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* keepCard({ ws, initiative, initiativeTitle, batch, place: 'tree'|'today', onChanged(what), onHoldApply(layout) })
|
|
32
|
+
* `onChanged` runs after Keep is written, when Withdraw all is held, undone or written. `onHoldApply` is the overlay the
|
|
33
|
+
* hold applies to a fetched layout (the Tree passes one; Today hides the card while the hold runs).
|
|
34
|
+
*/
|
|
35
|
+
export function keepCard({ ws, initiative, initiativeTitle, batch, place = 'tree', repo, cardId, onChanged, onHoldApply, onKeepApply }) {
|
|
36
|
+
const n = batch.count || (batch.items || []).length;
|
|
37
|
+
const key = keepKey(ws, initiative, batch.by);
|
|
38
|
+
const doKeep = async (btn) => {
|
|
39
|
+
btn.disabled = true;
|
|
40
|
+
if (onKeepApply) onKeepApply();
|
|
41
|
+
const out = await postEvents(ws, [{ ev: 'kept', initiative, batch: batch.by, items: batch.items }]);
|
|
42
|
+
if (!out || out.ok === false) { toast({ text: (out && out.error) || 'nothing was saved', tone: 'error' }); btn.disabled = false; }
|
|
43
|
+
else toast({ text: COPY.kept(n) });
|
|
44
|
+
if (onChanged) onChanged({ kept: true, ok: !!(out && out.ok !== false) });
|
|
45
|
+
};
|
|
46
|
+
const doWithdrawAll = () => {
|
|
47
|
+
hold({
|
|
48
|
+
key, ws, text: COPY.withdrewN(n),
|
|
49
|
+
events: (batch.items || []).map((id) => ({ ev: 'withdrawn', item: id })),
|
|
50
|
+
apply: onHoldApply || null,
|
|
51
|
+
onDone: () => { if (onChanged) onChanged({ withdrawn: true }); },
|
|
52
|
+
onUndo: () => { if (onChanged) onChanged({ undone: true }); },
|
|
53
|
+
});
|
|
54
|
+
if (onChanged) onChanged({ held: true });
|
|
55
|
+
};
|
|
56
|
+
const attrs = { 'data-bd-keep-card': place, 'data-batch': batch.by, 'data-initiative': initiative, 'data-count': String(n) };
|
|
57
|
+
if (place === 'today') {
|
|
58
|
+
// R12: a Today card has ONE button. Keep is it; Withdraw all is in the ⋯ menu and is said on the card.
|
|
59
|
+
return todayCard({
|
|
60
|
+
kind: 5, id: cardId || key, repo, ws, initiative, cause: (initiativeTitle || initiative) + ' · ' + COPY.keepToday(n, batch.byLabel),
|
|
61
|
+
evidence: TODAY.keepHint, act: { label: COPY.keep }, actAttrs: { 'data-bd-keep': '' },
|
|
62
|
+
menuExtra: [{ label: TODAY.withdrawAll, onSelect: doWithdrawAll }], attrs: { ...attrs, 'data-today-row': 'keep' },
|
|
63
|
+
onAct: (e) => doKeep(e.currentTarget),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const keep = button({ label: COPY.keep, variant: 'outline', size: 'sm', attrs: { 'data-bd-keep': '' }, onClick: () => doKeep(keep) });
|
|
67
|
+
const withdrawAll = button({ label: COPY.withdrawAll, variant: 'ghost', size: 'sm', attrs: { 'data-bd-withdraw-all': '' }, onClick: doWithdrawAll });
|
|
68
|
+
const text = [el('span', null, COPY.keepTree(n, dayOf(batch.firstAt || batch.lastAt), batch.byLabel))];
|
|
69
|
+
return el('div', { class: 'bd-keep', ...attrs }, el('div', { class: 'bd-keep__text' }, text), el('div', { class: 'bd-keep__actions' }, keep, withdrawAll));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ── Today ─────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
73
|
+
|
|
74
|
+
const KINDS = ['keep', 'evidence', 'plan-flag'];
|
|
75
|
+
const cache = new Map(); // ws -> { rows, loading, stale }
|
|
76
|
+
let rerender = null;
|
|
77
|
+
let hooked = false;
|
|
78
|
+
|
|
79
|
+
function load(ws) {
|
|
80
|
+
const c = cache.get(ws) || { rows: [], loading: false, stale: true };
|
|
81
|
+
cache.set(ws, c);
|
|
82
|
+
// A change that arrives while a read is in flight is not dropped: that read may have started before the change, so
|
|
83
|
+
// it is read once more when the read ends.
|
|
84
|
+
if (c.loading) { c.again = true; return; }
|
|
85
|
+
c.loading = true; c.again = false;
|
|
86
|
+
getNeeds(ws).then((out) => {
|
|
87
|
+
c.loading = false; c.stale = false;
|
|
88
|
+
c.rows = out && Array.isArray(out.needs) ? out.needs.filter((r) => r && KINDS.includes(r.kind) && r.run_id !== 'plan-items:unreadable') : [];
|
|
89
|
+
if (c.again) load(ws);
|
|
90
|
+
if (rerender) rerender(); else setState({});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hook() {
|
|
95
|
+
if (hooked || typeof window === 'undefined') return;
|
|
96
|
+
hooked = true;
|
|
97
|
+
window.addEventListener('pmos:ledgers', (e) => {
|
|
98
|
+
const changed = (e && e.detail && e.detail.changed) || [];
|
|
99
|
+
let any = false;
|
|
100
|
+
for (const ws of changed) { const c = cache.get(ws); if (c) { c.stale = true; any = true; load(ws); } }
|
|
101
|
+
if (any && rerender) rerender();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read the breakdown rows of every repository once, so the nav's Today count includes them before Today has rendered.
|
|
107
|
+
* The nav calls it on each render. It starts a request only for a workspace that is not in the cache yet, and only after
|
|
108
|
+
* the first sessions poll has answered (`state.sessions` is set), so the page's startup makes no extra request. After
|
|
109
|
+
* that, a workspace is read again only on a `pmos:ledgers` change, as for Today. Fixture mode reads nothing.
|
|
110
|
+
*/
|
|
111
|
+
export function primePlanNeeds(repos) {
|
|
112
|
+
if (FIXTURE || !Array.isArray(state.sessions)) return;
|
|
113
|
+
hook();
|
|
114
|
+
for (const repo of repos || []) {
|
|
115
|
+
const ws = wsOf(repo);
|
|
116
|
+
if (ws && !cache.has(ws)) load(ws);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* How many breakdown rows need the PM, over every workspace read so far (Keep batches not held for Undo, evidence and
|
|
122
|
+
* check rows). The nav's Today count adds it, so the badge and Today's header count the same rows. Reads the cache that
|
|
123
|
+
* primePlanNeeds() and Today fill; it fetches nothing itself. 0 in fixture mode.
|
|
124
|
+
*/
|
|
125
|
+
function planCount(repos) {
|
|
126
|
+
let n = 0;
|
|
127
|
+
for (const repo of repos || []) {
|
|
128
|
+
const ws = wsOf(repo);
|
|
129
|
+
if (!ws) continue;
|
|
130
|
+
primePlanNeeds([repo]);
|
|
131
|
+
ensureFeed(ws);
|
|
132
|
+
for (const row of (cache.get(ws) || { rows: [] }).rows) {
|
|
133
|
+
if (row.kind === 'keep' && row.batch && isHeld(keepKey(ws, row.initiative_id, row.batch.by))) continue;
|
|
134
|
+
n++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return n;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function planNeedsCount() {
|
|
141
|
+
let n = 0;
|
|
142
|
+
for (const [ws, c] of cache) {
|
|
143
|
+
for (const row of c.rows || []) {
|
|
144
|
+
if (row.kind === 'keep' && row.batch && isHeld(keepKey(ws, row.initiative_id, row.batch.by))) continue;
|
|
145
|
+
n++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The breakdown cards for Today's needs-you column: Keep cards, then evidence and check cards, for the given
|
|
153
|
+
* repositories. `open(repo, slug)` opens the initiative; `titleOf(slug)` names it; `onRender` is called when new rows
|
|
154
|
+
* arrive. Reads /api/needs once per workspace and again when that workspace's ledger changes. Fixture mode reads nothing.
|
|
155
|
+
* Every card is a Today card (today-cards.js): a cause, its evidence, one button and the ⋯ menu, with the id the server
|
|
156
|
+
* ranks by (`<ws>|<run id>`).
|
|
157
|
+
*/
|
|
158
|
+
export function planNeedsCards({ repos, fixture, onRender, open, titleOf }) {
|
|
159
|
+
if (fixture) return [];
|
|
160
|
+
rerender = onRender || rerender;
|
|
161
|
+
hook();
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const repo of repos || []) {
|
|
164
|
+
const ws = wsOf(repo);
|
|
165
|
+
if (!ws) continue;
|
|
166
|
+
ensureFeed(ws);
|
|
167
|
+
const c = cache.get(ws);
|
|
168
|
+
if (!c || (c.stale && !c.loading)) load(ws);
|
|
169
|
+
for (const row of (c && c.rows) || []) {
|
|
170
|
+
const slug = row.initiative_id;
|
|
171
|
+
const cardId = ws + '|' + row.run_id;
|
|
172
|
+
if (row.kind === 'keep' && row.batch) {
|
|
173
|
+
if (isHeld(keepKey(ws, slug, row.batch.by))) continue;
|
|
174
|
+
out.push(keepCard({
|
|
175
|
+
ws, repo, cardId, initiative: slug, initiativeTitle: titleOf ? titleOf(slug) : slug, batch: row.batch, place: 'today',
|
|
176
|
+
onChanged: () => { const cc = cache.get(ws); if (cc) { cc.stale = true; load(ws); } if (rerender) rerender(); },
|
|
177
|
+
}));
|
|
178
|
+
} else {
|
|
179
|
+
const evidence = row.kind === 'evidence';
|
|
180
|
+
out.push(todayCard({
|
|
181
|
+
kind: evidence ? 4 : 8, id: cardId, repo, ws, initiative: slug, cause: row.question,
|
|
182
|
+
evidence: [titleOf ? titleOf(slug) : slug, evidence ? COPY.needs.evidence : COPY.needs.flag, COPY.derived].filter(Boolean).join(' · '),
|
|
183
|
+
act: { label: COPY.needs.open }, onAct: () => { if (open) open(repo, slug); },
|
|
184
|
+
attrs: { 'data-bd-need': row.kind, 'data-item': row.item || null, 'data-today-row': row.kind },
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (out.length) ensureComponentsCss();
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* The Today source: the breakdown's rows as cards of kinds 4, 5 and 8, counted once with every other source. */
|
|
194
|
+
if (!FIXTURE) {
|
|
195
|
+
onFeed((ws) => { const c = cache.get(ws); if (c) { c.stale = true; load(ws); } });
|
|
196
|
+
registerTodaySource({
|
|
197
|
+
id: 'plan-rows', kind: null, rank: 30,
|
|
198
|
+
count: (ctx) => planCount(ctx.repos),
|
|
199
|
+
cards: (ctx) => planNeedsCards({
|
|
200
|
+
repos: ctx.repos, fixture: false, onRender: () => (ctx.setState ? ctx.setState({}) : setState({})),
|
|
201
|
+
open: (r, slug) => (ctx.open ? ctx.open(r, slug) : null), titleOf: (slug) => (records()[slug] && records()[slug].tag) || slug,
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { el } from './dom.js';
|
|
10
10
|
import { state, setState, FIXTURE } from './state.js';
|
|
11
|
+
import { askText } from './dialogs.js';
|
|
11
12
|
|
|
12
13
|
let dataMod = null;
|
|
13
14
|
const dataReady = import('./data.js').then((m) => { dataMod = m; }).catch(() => { dataMod = null; });
|
|
@@ -29,7 +30,12 @@ function refresh(product) {
|
|
|
29
30
|
|
|
30
31
|
async function apply(product, slug, to) {
|
|
31
32
|
const word = to === 'active' ? 'Reopen' : to === 'closed' ? 'Close' : to === 'on-hold' ? 'Put on hold' : 'Cancel';
|
|
32
|
-
|
|
33
|
+
// The two buttons never share a word: the back-out says Back and the act says what it does (Cancel initiative).
|
|
34
|
+
const act = to === 'cancelled' ? 'Cancel initiative' : word;
|
|
35
|
+
const reason = to === 'active' ? '' : await askText({
|
|
36
|
+
title: word + ' “' + slug + '”', label: 'Why? One line, kept in the record.',
|
|
37
|
+
confirmLabel: act, cancelLabel: 'Back', danger: to === 'closed' || to === 'cancelled',
|
|
38
|
+
});
|
|
33
39
|
if (reason === null) return; // the PM backed out
|
|
34
40
|
const out = await dataMod.setLifecycle(product, slug, to, String(reason).trim());
|
|
35
41
|
if (!out || out.ok === false) { setState({ lcNotice: { slug, text: (out && out.error) || 'not recorded' } }); return; }
|
|
@@ -85,9 +85,9 @@
|
|
|
85
85
|
gap: 10px;
|
|
86
86
|
height: 52px;
|
|
87
87
|
padding: 0 14px;
|
|
88
|
-
background:
|
|
89
|
-
color:
|
|
90
|
-
border-bottom: 1px solid
|
|
88
|
+
background: var(--sidebar);
|
|
89
|
+
color: var(--foreground);
|
|
90
|
+
border-bottom: 1px solid var(--border);
|
|
91
91
|
}
|
|
92
92
|
/* 44px minimums on every control: the phone's targets are thumbs, not a pointer. */
|
|
93
93
|
.mob-back {
|
|
@@ -98,17 +98,17 @@
|
|
|
98
98
|
min-width: 44px;
|
|
99
99
|
min-height: 44px;
|
|
100
100
|
margin-left: -6px;
|
|
101
|
-
color:
|
|
101
|
+
color: var(--muted-foreground);
|
|
102
102
|
font-size: 14px;
|
|
103
103
|
}
|
|
104
104
|
.mob-brand {
|
|
105
105
|
flex: none;
|
|
106
106
|
display: grid;
|
|
107
107
|
place-items: center;
|
|
108
|
-
width:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
font-
|
|
108
|
+
width: auto;
|
|
109
|
+
font-family: var(--mono);
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
font-weight: 600;
|
|
112
112
|
line-height: 1;
|
|
113
113
|
}
|
|
114
114
|
.mob-title {
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
text-align: center;
|
|
118
118
|
font-family: var(--mono);
|
|
119
119
|
font-size: 12px;
|
|
120
|
-
color:
|
|
120
|
+
color: var(--muted-foreground);
|
|
121
121
|
overflow: hidden;
|
|
122
122
|
text-overflow: ellipsis;
|
|
123
123
|
white-space: nowrap;
|
|
@@ -128,17 +128,40 @@
|
|
|
128
128
|
place-items: center;
|
|
129
129
|
min-width: 44px;
|
|
130
130
|
min-height: 44px;
|
|
131
|
-
color:
|
|
131
|
+
color: var(--muted-foreground);
|
|
132
132
|
}
|
|
133
133
|
.mob-pal { font-family: var(--mono); font-size: 12px; }
|
|
134
|
-
.mob-theme {
|
|
135
|
-
|
|
134
|
+
.mob-back:active, .mob-pal:active, .mob-theme:active { color: var(--foreground); }
|
|
135
|
+
|
|
136
|
+
/* The frame's bar controls (pm-os-absorb Run 3, slice 3A): Repository, which opens the repository menu, and
|
|
137
|
+
* Sync (only while the current repository is not in sync), and Settings. Words, at the bar's own ink; 44px targets like
|
|
138
|
+
* every control here. A button does not inherit the font, so Back says it too. */
|
|
139
|
+
.mob-back { font-family: var(--sans); }
|
|
140
|
+
.mob-repo, .mob-set, .mob-sync {
|
|
141
|
+
flex: none;
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
min-width: 44px;
|
|
145
|
+
min-height: 44px;
|
|
146
|
+
padding: 0 6px;
|
|
147
|
+
color: inherit;
|
|
148
|
+
font-family: var(--sans);
|
|
149
|
+
font-size: 12px;
|
|
150
|
+
}
|
|
151
|
+
.mob-repo {
|
|
152
|
+
max-width: 128px;
|
|
153
|
+
overflow: hidden;
|
|
154
|
+
text-overflow: ellipsis;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
}
|
|
158
|
+
.mob-set[aria-current="page"] { text-decoration: underline; }
|
|
136
159
|
|
|
137
160
|
/* ── the bottom block ────────────────────────────────────────────────────────────────────── */
|
|
138
161
|
|
|
139
162
|
.mobmenu {
|
|
140
|
-
background:
|
|
141
|
-
border-top: 1px solid
|
|
163
|
+
background: var(--sidebar);
|
|
164
|
+
border-top: 1px solid var(--border);
|
|
142
165
|
/* viewport-fit=cover is on, so the home indicator's strip is ours to pad past. */
|
|
143
166
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
144
167
|
}
|
|
@@ -153,7 +176,7 @@
|
|
|
153
176
|
overflow-x: auto;
|
|
154
177
|
-webkit-overflow-scrolling: touch;
|
|
155
178
|
background: var(--muted);
|
|
156
|
-
border-bottom: 1px solid
|
|
179
|
+
border-bottom: 1px solid var(--border);
|
|
157
180
|
}
|
|
158
181
|
.mobmenu[data-view="terminal"] .ctrlbar { display: flex; }
|
|
159
182
|
.ctrlbar button {
|
|
@@ -170,9 +193,9 @@
|
|
|
170
193
|
}
|
|
171
194
|
.ctrlbar button:active { background: var(--accent); }
|
|
172
195
|
.ctrlbar button[aria-pressed="true"] {
|
|
173
|
-
border-color: var(--
|
|
174
|
-
color: var(--
|
|
175
|
-
background: var(--
|
|
196
|
+
border-color: var(--foreground);
|
|
197
|
+
color: var(--foreground);
|
|
198
|
+
background: var(--accent);
|
|
176
199
|
}
|
|
177
200
|
.ctrlbar .lbl { font-size: 11px; letter-spacing: .04em; }
|
|
178
201
|
|
|
@@ -187,14 +210,20 @@
|
|
|
187
210
|
min-width: 0;
|
|
188
211
|
min-height: 64px;
|
|
189
212
|
padding: 0 2px;
|
|
190
|
-
color:
|
|
213
|
+
color: var(--muted-foreground);
|
|
191
214
|
}
|
|
192
215
|
.mobnav-b .g { font-size: 17px; line-height: 1; }
|
|
193
216
|
.mobnav-b .l { font-size: 10px; line-height: 1; }
|
|
194
|
-
.mobnav-b[aria-current="page"] { color:
|
|
217
|
+
.mobnav-b[aria-current="page"] { color: var(--foreground); }
|
|
218
|
+
/* The tabs are words (the frame's five tabs); the Today tab carries the count of what needs the PM. */
|
|
219
|
+
.mobnav-b { font-family: var(--sans); }
|
|
220
|
+
.mobnav-b .l { font-size: 12px; }
|
|
221
|
+
.mobnav-b[aria-current="page"] .l { text-decoration: underline; text-underline-offset: 4px; }
|
|
222
|
+
.mobnav-n { font-family: var(--mono); font-size: 11px; line-height: 1; }
|
|
195
223
|
}
|
|
196
224
|
|
|
197
225
|
/* `hidden` wins over every display above, in both directions of the breakpoint. Back is ABSENT at a
|
|
198
226
|
* destination root rather than present-and-inert: a control that looks available and does nothing is
|
|
199
227
|
* the thing this workspace refuses to ship. */
|
|
200
228
|
.mob[hidden], .mobmenu[hidden], .mob-back[hidden], .mob-brand[hidden] { display: none !important; }
|
|
229
|
+
.mob-repo[hidden], .mob-set[hidden], .mob-sync[hidden], .mob-theme[hidden], .mobnav-n[hidden] { display: none !important; }
|