create-pmos 0.51.0 → 0.53.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 +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/bin/pmos.js +15 -1
- 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 +27 -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/layout.md +98 -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/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 +80 -0
- 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/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 +206 -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/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 +367 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +66 -0
- package/workspace/lib/proactive.js +7 -4
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +579 -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 +5 -5
- package/workspace/public/index.html +29 -19
- package/workspace/public/pm/app.js +299 -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 +3054 -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 +176 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +105 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +94 -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 +197 -110
- 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 +97 -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 +2864 -0
- package/workspace/run-tests.js +117 -6
- package/workspace/server.js +312 -36
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +20 -4
- 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-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,244 @@
|
|
|
1
|
+
/* pm/today-cards.js — the card every Today source draws, the one feed they read, and the ranking (pm-os-absorb Run 3,
|
|
2
|
+
* slice 3B, PRD R12).
|
|
3
|
+
*
|
|
4
|
+
* Today lists eight kinds of card and no other. A card has a CAUSE, its EVIDENCE, ONE button and the ⋯ menu. It never has
|
|
5
|
+
* a dismiss: it leaves when its cause is gone.
|
|
6
|
+
*
|
|
7
|
+
* todayCard({ kind, id, repo, ws, initiative, cause, evidence, act, onAct, menuExtra, attrs, panel }) → the card
|
|
8
|
+
* rankNodes(nodes) the cards in Today's order: the kind (data-today-kind), then the id (data-today-id)
|
|
9
|
+
* scopeCtx(st) { all, repos, repo }: the repositories Today (and the nav badge) count, from the scope
|
|
10
|
+
* ensureFeed(ws) starts reading GET /api/today?ws= for a repository: once, then on every change of its sig
|
|
11
|
+
* feedOf(ws) the last answer for that repository, or null
|
|
12
|
+
* onFeed(cb(ws)) cb runs when a repository's list changed (the sources that read /api/needs read it again)
|
|
13
|
+
* refreshFeed(ws) read it now (after a press that changed a record)
|
|
14
|
+
* feedErrorOf(ws) why the last read of that repository's list failed, or null (a list that cannot be read is said)
|
|
15
|
+
* bannerOf(repos) the sources that could not be read: [{ repo, source, error }], and a list that could not be read
|
|
16
|
+
*
|
|
17
|
+
* The server owns the list (lib/today.js). The page draws what the server sent and what the registered sources add, and
|
|
18
|
+
* counts the same rows once: the nav badge and the header read todayCount() from the registry, which adds the sources.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { el } from './dom.js';
|
|
22
|
+
import { button, iconButton, menu, needsDot, tabs, field, toast, ensureComponentsCss } from './primitives.js';
|
|
23
|
+
import { pmGet, pmPost } from './http.js';
|
|
24
|
+
import { watchSig } from './sig-watch.js';
|
|
25
|
+
import { wsOf } from './data.js';
|
|
26
|
+
import { state, setState, FIXTURE } from './state.js';
|
|
27
|
+
import { openInitiative } from './registry.js';
|
|
28
|
+
import { COPY } from './today-copy.js';
|
|
29
|
+
|
|
30
|
+
/* ── the feed ────────────────────────────────────────────────────────────────────────────── */
|
|
31
|
+
|
|
32
|
+
const feeds = new Map(); // ws -> { data, sig, error, stop }
|
|
33
|
+
const listeners = new Set();
|
|
34
|
+
let hooked = false;
|
|
35
|
+
|
|
36
|
+
const notify = (ws) => { for (const cb of [...listeners]) { try { cb(ws); } catch (e) { console.error('[pm] a Today listener failed:', e); } } };
|
|
37
|
+
|
|
38
|
+
async function read(ws) {
|
|
39
|
+
const f = feeds.get(ws);
|
|
40
|
+
const out = await pmGet('/api/today?ws=' + encodeURIComponent(ws));
|
|
41
|
+
if (!f) return null;
|
|
42
|
+
if (!out || out.ok === false || !Array.isArray(out.cards)) {
|
|
43
|
+
// an answer that is not a list is said (R12): the page never reads "Reading what needs you." for ever, and never reads as empty
|
|
44
|
+
const had = f.error;
|
|
45
|
+
f.error = (out && out.error ? String(out.error) : COPY.health.noAnswer).slice(0, 240);
|
|
46
|
+
if (f.error !== had) notify(ws);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const changed = !f.data || !!f.error;
|
|
50
|
+
f.data = out; f.sig = out.sig || null; f.error = null;
|
|
51
|
+
if (changed) notify(ws);
|
|
52
|
+
return f.sig;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hook() {
|
|
56
|
+
if (hooked || typeof window === 'undefined') return;
|
|
57
|
+
hooked = true;
|
|
58
|
+
window.addEventListener('pmos:ledgers', (e) => {
|
|
59
|
+
for (const ws of (e && e.detail && e.detail.changed) || []) if (feeds.has(ws)) refreshFeed(ws);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Start reading a repository's list, once the first sessions poll has answered (the page's startup makes no extra request). */
|
|
64
|
+
export function ensureFeed(ws) {
|
|
65
|
+
if (FIXTURE || !ws) return null;
|
|
66
|
+
if (feeds.has(ws)) return feeds.get(ws);
|
|
67
|
+
if (!Array.isArray(state.sessions)) return null;
|
|
68
|
+
hook();
|
|
69
|
+
const f = { data: null, sig: null, error: null, stop: null };
|
|
70
|
+
feeds.set(ws, f);
|
|
71
|
+
f.stop = watchSig(() => read(ws), () => notify(ws), { everyMs: 10000 });
|
|
72
|
+
return f;
|
|
73
|
+
}
|
|
74
|
+
export const feedOf = (ws) => { const f = feeds.get(ws); return f && f.data ? f.data : null; };
|
|
75
|
+
export const feedErrorOf = (ws) => { const f = feeds.get(ws); return f && f.error ? f.error : null; };
|
|
76
|
+
export function onFeed(cb) { listeners.add(cb); return () => listeners.delete(cb); }
|
|
77
|
+
export async function refreshFeed(ws) {
|
|
78
|
+
const f = feeds.get(ws);
|
|
79
|
+
if (!f) return;
|
|
80
|
+
const before = f.sig;
|
|
81
|
+
await read(ws);
|
|
82
|
+
if (f.sig !== before) notify(ws);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The sources that could not be read, over the given repositories: never an empty list that reads as "nothing needs you". */
|
|
86
|
+
export function bannerOf(repos) {
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const repo of repos || []) {
|
|
89
|
+
const ws = wsOf(repo);
|
|
90
|
+
const d = feedOf(ws);
|
|
91
|
+
const err = feedErrorOf(ws);
|
|
92
|
+
if (err) out.push({ repo, source: COPY.health.todaySource, error: err });
|
|
93
|
+
for (const b of (d && d.banner) || []) out.push({ repo, source: String(b.source || ''), error: String(b.error || '') });
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The repositories Today counts: the one the person chose, or every one. The nav badge reads the same scope. */
|
|
99
|
+
export function scopeCtx(st, tuples, current) {
|
|
100
|
+
const all = (st || state).todayScope === 'all';
|
|
101
|
+
const names = (tuples || []).map((t) => t[0]);
|
|
102
|
+
return { all, repos: all ? names : [current].filter(Boolean), repo: current || null };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── ranking ─────────────────────────────────────────────────────────────────────────────── */
|
|
106
|
+
|
|
107
|
+
const kindOf = (n) => { const k = Number(n.getAttribute('data-today-kind')); return Number.isInteger(k) && k >= 1 && k <= 8 ? k : 9; };
|
|
108
|
+
/** The cards in Today's order (lib/today.js rank): the kind, then the id. Returns a new array; the nodes are not moved. */
|
|
109
|
+
export function rankNodes(nodes) {
|
|
110
|
+
const idOf = (n) => n.getAttribute('data-today-id') || '';
|
|
111
|
+
// the same comparison as lib/today.js rank (code-unit order), so the page and GET /api/today list a kind the same way
|
|
112
|
+
return nodes.slice().sort((a, b) => kindOf(a) - kindOf(b) || (idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ── the card ────────────────────────────────────────────────────────────────────────────── */
|
|
116
|
+
|
|
117
|
+
function copyCommand(cli) {
|
|
118
|
+
if (!cli) { toast({ text: COPY.noCommand }); return; }
|
|
119
|
+
const done = () => toast({ text: COPY.commandCopied(cli) });
|
|
120
|
+
const shown = () => toast({ text: COPY.commandShown(cli), timeout: 12000 });
|
|
121
|
+
try {
|
|
122
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(cli).then(done, shown);
|
|
123
|
+
else shown();
|
|
124
|
+
} catch { shown(); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* todayCard({ kind 1–8, id, repo, ws, initiative, cause, evidence, act: { label, cli? }, onAct(event), menuExtra: [{ label,
|
|
129
|
+
* onSelect }], attrs, panel: Node|null })
|
|
130
|
+
* `id` is the card's stable id (`<ws>|<run id>`): the ranking sorts by it and a test finds the card by it.
|
|
131
|
+
*/
|
|
132
|
+
/** The ⋯ of a Today card: the card's own entries, then Open initiative, Copy command and Why. provider-cards.js gives it
|
|
133
|
+
* to a card a run's renderer drew without one, so every card on Today has its ⋯. */
|
|
134
|
+
export function moreButton({ kind, repo, initiative, cause, act, menuExtra } = {}) {
|
|
135
|
+
const label = (act && act.label) || 'Open';
|
|
136
|
+
const more = iconButton({
|
|
137
|
+
icon: 'ellipsis', ariaLabel: COPY.aria.more(cause || label), attrs: { 'data-today-more': '' },
|
|
138
|
+
onClick: () => menu({
|
|
139
|
+
anchor: more, ariaLabel: COPY.aria.more(cause || label),
|
|
140
|
+
items: [
|
|
141
|
+
...(menuExtra || []),
|
|
142
|
+
{ label: COPY.menu.open, onSelect: () => { if (initiative) openInitiative(repo, initiative); else toast({ text: COPY.noInitiative }); } },
|
|
143
|
+
{ label: COPY.menu.command, onSelect: () => copyCommand(act && act.cli) },
|
|
144
|
+
{ label: COPY.menu.why, onSelect: () => toast({ text: COPY.why[kind] || COPY.why[8] }) },
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
return more;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `face`: a run's renderer card (registerTodayCard). The face IS the card's body: it shows the one cause and the one
|
|
153
|
+
* action, so the Today card adds only the ⋯ menu beside it, never a second cause or a second button. The face's cause
|
|
154
|
+
* element carries data-today-cause and its action data-today-act, as a plain card's do.
|
|
155
|
+
*/
|
|
156
|
+
export function todayCard({ kind, id, repo, ws, initiative, cause, evidence, act, onAct, menuExtra, attrs, actAttrs, panel, face } = {}) {
|
|
157
|
+
ensureComponentsCss();
|
|
158
|
+
const label = (act && act.label) || 'Open';
|
|
159
|
+
const more = moreButton({ kind, repo, initiative, cause, act, menuExtra });
|
|
160
|
+
const box = {
|
|
161
|
+
class: face ? 'td-card td-card--host' : 'td-card', 'data-today': 'card', 'data-today-kind': String(kind), 'data-today-id': id || null,
|
|
162
|
+
'data-initiative': initiative || null, 'data-repo': repo || null, 'data-ws': ws || null, ...(attrs || {}),
|
|
163
|
+
};
|
|
164
|
+
if (face) return el('div', box, el('div', { class: 'td-card__host' }, el('div', { class: 'td-card__face' }, face), more));
|
|
165
|
+
return el('div', box,
|
|
166
|
+
el('div', { class: 'td-card__top' },
|
|
167
|
+
el('div', { class: 'td-card__text' },
|
|
168
|
+
el('div', { class: 'td-card__cause', 'data-today-cause': '' }, needsDot(), el('span', null, cause || '')),
|
|
169
|
+
evidence ? el('div', { class: 'td-card__evidence' }, evidence) : null),
|
|
170
|
+
el('div', { class: 'td-card__acts' },
|
|
171
|
+
button({ label, variant: 'outline', size: 'sm', attrs: { 'data-today-act': '', ...(actAttrs || {}) }, onClick: (e) => { if (onAct) onAct(e); } }),
|
|
172
|
+
more)),
|
|
173
|
+
panel || null);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ── the Acceptance Gate card (D12): Review › Right or Wrong › note › Accept ──────────────────────────────── */
|
|
177
|
+
|
|
178
|
+
const ui = new Map(); // card id -> { open, right, noting, note, busy, error }
|
|
179
|
+
const uiOf = (id) => { if (!ui.has(id)) ui.set(id, { open: false, right: null, noting: false, note: '', busy: false, error: '' }); return ui.get(id); };
|
|
180
|
+
const verdictLabel = (v) => (/^pass/i.test(String(v || '')) ? 'pass' : /^fail/i.test(String(v || '')) ? 'fail' : null);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The card of an open acceptance. The PM's answer to "Was the evaluator right?" is written as `pm_label`: the evaluator's own
|
|
184
|
+
* verdict when the answer is Right, the other one when it is Wrong. Nothing is written until Accept or Return is pressed,
|
|
185
|
+
* and the write is one POST to the guarded gate (pmPost carries the nonce).
|
|
186
|
+
*/
|
|
187
|
+
export function acceptCard(c, { repo, rerender }) {
|
|
188
|
+
const u = uiOf(c.id);
|
|
189
|
+
const verdict = c.detail && c.detail.verdict ? c.detail.verdict : null;
|
|
190
|
+
const score = c.detail && c.detail.score !== undefined ? c.detail.score : null;
|
|
191
|
+
const lbl = verdictLabel(verdict);
|
|
192
|
+
const need = lbl !== null; // with no verdict there is nothing to rate, and the buttons need no answer
|
|
193
|
+
const ready = !need || u.right !== null;
|
|
194
|
+
|
|
195
|
+
const send = async (action) => {
|
|
196
|
+
if (u.busy || !ready) return;
|
|
197
|
+
u.busy = true; u.error = ''; rerender();
|
|
198
|
+
const note = noteField && noteField.control ? String(noteField.control.value || '').trim() : u.note;
|
|
199
|
+
const pm_label = need ? ((u.right === true) === (lbl === 'pass') ? 'pass' : 'fail') : undefined;
|
|
200
|
+
const out = await pmPost('/api/gate', {
|
|
201
|
+
ws: c.ws, action, run_id: c.run_id, ...(c.initiative_id ? { initiative_id: c.initiative_id } : {}), ...(pm_label ? { pm_label } : {}), note,
|
|
202
|
+
});
|
|
203
|
+
u.busy = false;
|
|
204
|
+
if (out && out.ok !== false) {
|
|
205
|
+
ui.delete(c.id);
|
|
206
|
+
toast({ text: action === 'accept' ? COPY.accept.accepted(c.run_id) : COPY.accept.returned(c.run_id) });
|
|
207
|
+
await refreshFeed(c.ws);
|
|
208
|
+
} else {
|
|
209
|
+
u.error = out && out.error ? COPY.accept.refused(String(out.error)) : COPY.accept.unreached;
|
|
210
|
+
}
|
|
211
|
+
rerender();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let noteField = null;
|
|
215
|
+
let panel = null;
|
|
216
|
+
if (u.open) {
|
|
217
|
+
noteField = u.noting ? field({
|
|
218
|
+
label: COPY.accept.note, hideLabel: true, kind: 'text', value: u.note, placeholder: COPY.accept.noteHint,
|
|
219
|
+
attrs: { 'data-today-note': '' }, onCommit: (v) => { u.note = String(v || ''); },
|
|
220
|
+
}) : null;
|
|
221
|
+
if (noteField) noteField.control.addEventListener('input', (e) => { u.note = e.target.value; });
|
|
222
|
+
panel = el('div', { class: 'td-accept', 'data-today-panel': c.id },
|
|
223
|
+
el('p', { class: 'td-accept__line' }, verdict ? COPY.accept.verdict(verdict, score) : COPY.accept.noVerdict),
|
|
224
|
+
need ? el('div', { class: 'td-accept__ask' },
|
|
225
|
+
el('span', { class: 'td-accept__q' }, COPY.accept.question),
|
|
226
|
+
tabs({
|
|
227
|
+
variant: 'segmented', role: 'radiogroup', ariaLabel: COPY.accept.question, value: u.right === null ? null : (u.right ? 'right' : 'wrong'),
|
|
228
|
+
items: [{ id: 'right', label: COPY.accept.right }, { id: 'wrong', label: COPY.accept.wrong }],
|
|
229
|
+
onChange: (id) => { u.right = id === 'right'; rerender(); },
|
|
230
|
+
attrs: { 'data-today-verdict': '' },
|
|
231
|
+
})) : null,
|
|
232
|
+
noteField || button({ label: COPY.accept.note, variant: 'ghost', size: 'sm', attrs: { 'data-today-add-note': '' }, onClick: () => { u.noting = true; rerender(); } }),
|
|
233
|
+
el('div', { class: 'td-accept__acts' },
|
|
234
|
+
button({ label: COPY.accept.accept, variant: 'outline', disabled: u.busy || !ready, attrs: { 'data-today-accept': '' }, onClick: () => send('accept') }),
|
|
235
|
+
button({ label: COPY.accept.return, variant: 'ghost', disabled: u.busy || !ready, attrs: { 'data-today-return': '' }, onClick: () => send('return') })),
|
|
236
|
+
u.error ? el('p', { class: 'td-accept__error', role: 'alert', 'data-today-error': '' }, u.error) : null);
|
|
237
|
+
}
|
|
238
|
+
return todayCard({
|
|
239
|
+
kind: 1, id: c.id, repo, ws: c.ws, initiative: c.initiative_id, cause: c.cause, evidence: c.evidence,
|
|
240
|
+
act: { label: COPY.accept.review, cli: c.act && c.act.cli }, panel,
|
|
241
|
+
attrs: { 'data-today-row': 'accept', 'data-run': c.run_id, 'data-open': u.open ? 'true' : 'false' },
|
|
242
|
+
onAct: () => { u.open = !u.open; rerender(); },
|
|
243
|
+
});
|
|
244
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* pm/today-copy.js — every word Today shows (pm-os-absorb Run 3, slice 3B, PRD R12).
|
|
2
|
+
*
|
|
3
|
+
* Pure data and pure functions. No DOM and no import, so `glossary.test.js` can load this file in Node and check every
|
|
4
|
+
* string against the glossary rules. A sentence a person reads on Today, in a Today card or its menu, or in a toast
|
|
5
|
+
* raised from a Today card, is written here and nowhere else. The cause and the evidence of a card come from the server
|
|
6
|
+
* (lib/today.js, or the needs provider that made the row); they are not written here.
|
|
7
|
+
*
|
|
8
|
+
* Instructional register (PMOS-STE): one idea per sentence, active voice, the verb first on a button.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const COPY = {
|
|
12
|
+
title: 'Today',
|
|
13
|
+
/** "3 need you", "1 needs you". */
|
|
14
|
+
count: (n) => `${n} ${n === 1 ? 'needs' : 'need'} you`,
|
|
15
|
+
countIn: (n, repo) => `${n} ${n === 1 ? 'needs' : 'need'} you in ${repo || 'this repository'}`,
|
|
16
|
+
countAcross: (n, repos) => `${n} ${n === 1 ? 'needs' : 'need'} you across ${repos} ${repos === 1 ? 'repository' : 'repositories'}`,
|
|
17
|
+
scope: { label: 'Show', repo: 'Repository', all: 'All repositories' },
|
|
18
|
+
/** The empty state of R12. `next` is the label of the next scheduled draft, or null when none is known. */
|
|
19
|
+
empty: (next) => (next ? `Nothing needs you. Next: ${next}.` : 'Nothing needs you.'),
|
|
20
|
+
reading: 'Reading what needs you.',
|
|
21
|
+
more: (n) => `Show ${n} more`,
|
|
22
|
+
fewer: 'Show fewer',
|
|
23
|
+
/** The state-health notice: sources that could not be read are said here, never shown as an empty list. */
|
|
24
|
+
health: {
|
|
25
|
+
title: (n) => (n === 1 ? 'One source could not be read.' : `${n} sources could not be read.`),
|
|
26
|
+
line: (source, error) => `${source}: ${error}`,
|
|
27
|
+
todaySource: 'Today',
|
|
28
|
+
noAnswer: 'The list could not be read. The workspace did not answer.',
|
|
29
|
+
},
|
|
30
|
+
aria: {
|
|
31
|
+
list: 'What needs you',
|
|
32
|
+
more: (what) => `More actions for ${what}`,
|
|
33
|
+
badge: (n) => `${n} ${n === 1 ? 'needs' : 'need'} you`,
|
|
34
|
+
},
|
|
35
|
+
/** The ⋯ menu of a card. */
|
|
36
|
+
menu: {
|
|
37
|
+
open: 'Open the initiative',
|
|
38
|
+
command: 'Copy the command',
|
|
39
|
+
why: 'Why is this here?',
|
|
40
|
+
},
|
|
41
|
+
/** One sentence per kind: why a card of that kind is on Today. The kinds are the eight of R12. */
|
|
42
|
+
why: {
|
|
43
|
+
1: 'A gate is owed. Only you can close it.',
|
|
44
|
+
2: 'A session is waiting for you.',
|
|
45
|
+
3: 'You sent a step back. It is still open.',
|
|
46
|
+
4: 'Work is ready for you to check.',
|
|
47
|
+
5: 'An agent proposed items. They count in no progress until you keep them.',
|
|
48
|
+
6: 'A capture was triaged. Add it or dismiss it.',
|
|
49
|
+
7: 'A cycle review is due. Confirm what is in Now, or move it.',
|
|
50
|
+
8: 'A record needs repair. Today shows it until it is fixed.',
|
|
51
|
+
},
|
|
52
|
+
noCommand: 'This card has no command.',
|
|
53
|
+
withdrawAll: 'Withdraw all',
|
|
54
|
+
keepHint: 'Keep them, or withdraw all from the menu.',
|
|
55
|
+
commandCopied: (cmd) => `Copied: ${cmd}`,
|
|
56
|
+
commandShown: (cmd) => `Run this in a terminal: ${cmd}`,
|
|
57
|
+
noInitiative: 'This card names no initiative.',
|
|
58
|
+
sessionGone: 'That session has ended.',
|
|
59
|
+
/** The Acceptance Gate card (D12): the PM's own act on a run. */
|
|
60
|
+
accept: {
|
|
61
|
+
review: 'Review',
|
|
62
|
+
question: 'Was the evaluator right?',
|
|
63
|
+
verdict: (v, score) => `The evaluator says ${v}${score !== null && score !== undefined ? ` (${score})` : ''}.`,
|
|
64
|
+
noVerdict: 'The evaluator gave no verdict.',
|
|
65
|
+
right: 'Right',
|
|
66
|
+
wrong: 'Wrong',
|
|
67
|
+
note: 'Add a note',
|
|
68
|
+
noteHint: 'Optional. One line on why.',
|
|
69
|
+
accept: 'Accept',
|
|
70
|
+
return: 'Return',
|
|
71
|
+
accepted: (run) => `Recorded the acceptance of ${run}.`,
|
|
72
|
+
returned: (run) => `Returned ${run}.`,
|
|
73
|
+
refused: (why) => `Nothing was recorded. ${why}`,
|
|
74
|
+
unreached: 'Nothing was recorded. The server did not answer.',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The glossary's `accept` and `next` rules are lifted and put back with one allowance each, so a later misuse in Today's
|
|
80
|
+
* copy ("Accept the proposal", "Next step") still fails glossary.test.js (contract C12):
|
|
81
|
+
* accept the Acceptance Gate card's button, `Accept` (D12), alone or as a bold button label in the guide
|
|
82
|
+
* next the empty state of R12, "Nothing needs you. Next: <draft>."
|
|
83
|
+
* Each extra rule reads: from the start of the text, skip anything that is not the allowed wording, then find the word.
|
|
84
|
+
*/
|
|
85
|
+
export const GLOSSARY = {
|
|
86
|
+
exempt: {
|
|
87
|
+
accept: 'Re-imposed below by accept-today, except the Acceptance Gate card\'s own button (D12).',
|
|
88
|
+
next: 'Re-imposed below by next-today, except the empty state of R12.',
|
|
89
|
+
},
|
|
90
|
+
extra: [
|
|
91
|
+
{ id: 'accept-today', re: /^(?!Accept$)(?:(?!\*\*Accept\*\*)[^])*?\bAccept(?:s|ed|ing)?\b/,
|
|
92
|
+
why: 'A proposal is kept, not accepted. Only the Acceptance Gate card\'s button reads Accept (D12).' },
|
|
93
|
+
{ id: 'next-today', re: /^(?:(?!Nothing needs you\. Next:)[^])*?(?:\bnext(?:\s*:|\s+steps?\b|\s+up\b)|\bup next\b)/i,
|
|
94
|
+
why: 'Next is the horizon. Only the empty state says "Nothing needs you. Next: <draft>." (R12).' },
|
|
95
|
+
],
|
|
96
|
+
guide: 'today.md',
|
|
97
|
+
};
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* secondary text, status badges and warnings reach about 4.5:1 on the light surfaces. The dark theme, which the parity board
|
|
10
10
|
* compares, is untouched.
|
|
11
11
|
*
|
|
12
|
+
* Second exception, pm-os-absorb Run 3 (PRD R27): `--primary` is the foreground on the background, so a primary control is
|
|
13
|
+
* neutral in both themes. The brand colour has one meaning, the Today dot and count, and lives in `--brand-mark`, which only
|
|
14
|
+
* .pmc-needs-dot and .pmc-needs-count read (the gate's R28-8). `--brand` is a retired alias of the foreground: the old
|
|
15
|
+
* surfaces that still name it read neutral until each is swept, so the brand colour cannot leak back through them. The
|
|
16
|
+
* tab strip and the bottom bar follow the theme like the rest of the chrome: only the terminal itself (--term-bg) stays dark.
|
|
17
|
+
*
|
|
12
18
|
* The faces are VENDORED (D62: the workspace is a local companion app that must render correctly
|
|
13
19
|
* with no network at all). The prototype linked Google Fonts; a CDN font is network egress and is
|
|
14
20
|
* forbidden here, so the same three IBM Plex Mono weights and the Space Grotesk variable face are
|
|
@@ -52,41 +58,33 @@
|
|
|
52
58
|
--muted:oklch(0.275 0.011 216.9);--muted-foreground:oklch(0.723 0.014 214.4);
|
|
53
59
|
--accent:oklch(0.275 0.011 216.9);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 15%);
|
|
54
60
|
--ring:oklch(0.56 0.021 213.5);--sidebar:oklch(0.218 0.008 223.9);
|
|
55
|
-
--
|
|
61
|
+
--primary:var(--foreground);--primary-foreground:var(--background);
|
|
62
|
+
--brand-mark:oklch(0.623 0.214 259.815);
|
|
63
|
+
--brand:var(--foreground);--brand-wash:color-mix(in oklab, var(--foreground) 8%, transparent);
|
|
56
64
|
--success:oklch(0.76 0.17 152);--warning:oklch(0.82 0.15 82);--info:oklch(0.79 0.11 195);
|
|
57
65
|
--purple:oklch(0.72 0.15 300);--destructive:oklch(0.704 0.191 22.216);
|
|
58
66
|
--term-bg:oklch(0.148 0.004 228.8);
|
|
59
67
|
--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
60
68
|
--sans:"Space Grotesk",ui-sans-serif,system-ui,sans-serif;
|
|
61
69
|
}
|
|
62
|
-
[data-pm-theme="light"]{
|
|
70
|
+
[data-pm-theme="light"],html[data-pm-page-theme="light"]{
|
|
63
71
|
--background:oklch(1 0 0);--foreground:oklch(0.148 0.004 228.8);
|
|
64
72
|
--card:oklch(1 0 0);--secondary:oklch(0.967 0.001 286.375);
|
|
65
73
|
--muted:oklch(0.963 0.002 197.1);--muted-foreground:oklch(0.48 0.021 213.5);
|
|
66
74
|
--accent:oklch(0.963 0.002 197.1);--border:oklch(0.925 0.005 214.3);--input:oklch(0.925 0.005 214.3);
|
|
67
75
|
--ring:oklch(0.723 0.014 214.4);--sidebar:oklch(0.987 0.002 197.1);
|
|
68
|
-
--
|
|
76
|
+
--primary:var(--foreground);--primary-foreground:var(--background);
|
|
77
|
+
--brand-mark:oklch(0.488 0.243 264.376);
|
|
78
|
+
--brand:var(--foreground);--brand-wash:color-mix(in oklab, var(--foreground) 8%, transparent);
|
|
69
79
|
--success:oklch(0.46 0.13 152);--warning:oklch(0.48 0.11 72);--info:oklch(0.47 0.09 195);
|
|
70
80
|
--purple:oklch(0.5 0.2 300);--destructive:oklch(0.5 0.21 27.325);
|
|
71
81
|
--term-bg:oklch(0.148 0.004 228.8);
|
|
72
82
|
}
|
|
73
|
-
/* The tab strip and the bottom bar of the tabs panel sit on --term-bg, dark in BOTH themes (D41). In the light theme they take the
|
|
74
|
-
dark theme's tokens, so their text and controls stay readable. Copied from the :root block above; keep the two in step. */
|
|
75
|
-
[data-pm-theme="light"] [data-term-chrome]{
|
|
76
|
-
--background:oklch(0.148 0.004 228.8);--foreground:oklch(0.987 0.002 197.1);
|
|
77
|
-
--card:oklch(0.218 0.008 223.9);--secondary:oklch(0.274 0.006 286.033);
|
|
78
|
-
--muted:oklch(0.275 0.011 216.9);--muted-foreground:oklch(0.723 0.014 214.4);
|
|
79
|
-
--accent:oklch(0.275 0.011 216.9);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 15%);
|
|
80
|
-
--ring:oklch(0.56 0.021 213.5);--sidebar:oklch(0.218 0.008 223.9);
|
|
81
|
-
--brand:oklch(0.623 0.214 259.815);--brand-wash:color-mix(in oklab, oklch(0.623 0.214 259.815) 12%, transparent);
|
|
82
|
-
--success:oklch(0.76 0.17 152);--warning:oklch(0.82 0.15 82);--info:oklch(0.79 0.11 195);
|
|
83
|
-
--purple:oklch(0.72 0.15 300);--destructive:oklch(0.704 0.191 22.216);
|
|
84
|
-
color:var(--foreground);
|
|
85
|
-
}
|
|
86
83
|
@keyframes pmblink{0%,49%{opacity:1}50%,100%{opacity:0}}
|
|
87
84
|
*{box-sizing:border-box}
|
|
88
|
-
body{margin:0;background:
|
|
89
|
-
a{color:
|
|
85
|
+
body{margin:0;background:var(--background);-webkit-font-smoothing:antialiased}
|
|
86
|
+
a{color:var(--foreground);text-decoration:underline;text-underline-offset:2px}
|
|
87
|
+
@media (hover:hover){a:hover{color:var(--muted-foreground)}}
|
|
90
88
|
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;text-align:left;padding:0}
|
|
91
89
|
input,textarea{font:inherit;color:inherit;background:none;border:0;outline:none;padding:0;resize:none}
|
|
92
90
|
/* overhaul-r1 R4: a collapsed-rail glyph draws its initials and kind badge from data attributes, so
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* pm/topbar.js — the frame's one top bar (pm-os-absorb Run 3, slice 3A; PRD R9, R10).
|
|
2
|
+
*
|
|
3
|
+
* Repository ▾ · Today Initiatives Board Reports · ⌘K · Sync (only when unsynced) · Settings
|
|
4
|
+
*
|
|
5
|
+
* The bar is built once and updated on every render: app.js calls `bar.update(model)` and the bar changes only what
|
|
6
|
+
* the model changed, so a focused control keeps its focus across a repaint. It holds no state of its own except the
|
|
7
|
+
* last sync reading of each repository, which it reads from local git through data.js (no network: GET /api/sync/status
|
|
8
|
+
* runs `git status` against the remote-tracking ref the last fetch left).
|
|
9
|
+
*
|
|
10
|
+
* createTopbar({ go, openPalette, repoMenu, sync }) → { node, update(model) }
|
|
11
|
+
* go(id) open destination id (today, initiatives, board, reports, settings)
|
|
12
|
+
* openPalette() open ⌘K
|
|
13
|
+
* repoMenu(anchor) open the Repository menu under `anchor`
|
|
14
|
+
* sync(repo) run Sync for `repo`; resolves to the server's answer
|
|
15
|
+
* model = { dest, repoLabel, needs, unsynced, syncing }
|
|
16
|
+
*
|
|
17
|
+
* Hooks the suites find things by: [data-fr="bar"], [data-fr="repo"], [data-fr-dest="<id>"] (aria-current="page" on
|
|
18
|
+
* the open one), [data-today-badge] (the Today count, seams §2.6), [data-fr="search"], [data-fr="sync"].
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { el, setChildren } from './dom.js';
|
|
22
|
+
import { button, needsDot, needsCount } from './primitives.js';
|
|
23
|
+
import { COPY } from './frame-copy.js';
|
|
24
|
+
|
|
25
|
+
/** The destinations the bar names, in order. Settings sits at the end of the bar, after Sync. */
|
|
26
|
+
export const BAR_DESTS = Object.freeze(['today', 'initiatives', 'board', 'reports']);
|
|
27
|
+
|
|
28
|
+
/** Is this sync reading "not in sync"? Behind, ahead or records not yet recorded. An unreadable status is not shown. */
|
|
29
|
+
export function unsyncedOf(st) {
|
|
30
|
+
return !!st && st.ok !== false && ((st.behind | 0) > 0 || (st.ahead | 0) > 0 || (st.dirty_records | 0) > 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
34
|
+
const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
|
|
35
|
+
|
|
36
|
+
let repoBtn = null;
|
|
37
|
+
let repoLabel = null;
|
|
38
|
+
const repoSlot = el('div', { class: 'fr-bar__repo' });
|
|
39
|
+
|
|
40
|
+
const count = needsCount('0', { 'data-today-badge': '' });
|
|
41
|
+
const dot = needsDot();
|
|
42
|
+
const links = {};
|
|
43
|
+
for (const id of [...BAR_DESTS, 'settings']) {
|
|
44
|
+
links[id] = button({
|
|
45
|
+
label: COPY.nav[id], variant: 'ghost', icon: id === 'settings' ? 'settings' : undefined,
|
|
46
|
+
attrs: { class: 'fr-link', 'data-fr-dest': id, 'aria-current': 'false' },
|
|
47
|
+
onClick: () => go && go(id),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const todayMark = el('span', { class: 'fr-mark' }, dot, count);
|
|
51
|
+
links.today.appendChild(todayMark);
|
|
52
|
+
const nav = el('nav', { class: 'fr-nav', 'aria-label': COPY.nav.label }, BAR_DESTS.map((id) => links[id]));
|
|
53
|
+
|
|
54
|
+
const search = button({
|
|
55
|
+
label: COPY.search, variant: 'outline', icon: 'search',
|
|
56
|
+
attrs: { class: 'fr-search', 'data-fr': 'search', 'aria-keyshortcuts': 'Meta+K Control+K' },
|
|
57
|
+
onClick: () => openPalette && openPalette(),
|
|
58
|
+
});
|
|
59
|
+
search.appendChild(el('kbd', { class: 'fr-kbd' }, COPY.searchKey));
|
|
60
|
+
|
|
61
|
+
let syncBtn = null;
|
|
62
|
+
let syncLabel = null;
|
|
63
|
+
|
|
64
|
+
function repoButton(label) {
|
|
65
|
+
const b = button({
|
|
66
|
+
label, variant: 'ghost', iconEnd: 'chevron-down',
|
|
67
|
+
attrs: { class: 'fr-repo', 'data-fr': 'repo', 'aria-haspopup': 'menu', 'aria-label': COPY.repo.open(label) },
|
|
68
|
+
onClick: () => repoMenu && repoMenu(b),
|
|
69
|
+
});
|
|
70
|
+
return b;
|
|
71
|
+
}
|
|
72
|
+
function syncButton(label, busy) {
|
|
73
|
+
return button({
|
|
74
|
+
label, variant: 'outline', size: 'sm', disabled: busy,
|
|
75
|
+
attrs: { class: 'fr-sync', 'data-fr': 'sync', title: COPY.sync.title },
|
|
76
|
+
onClick: () => sync && sync(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function update(m) {
|
|
81
|
+
const model = m || {};
|
|
82
|
+
if (model.repoLabel !== repoLabel) {
|
|
83
|
+
repoLabel = model.repoLabel;
|
|
84
|
+
repoBtn = repoButton(repoLabel || COPY.repo.none);
|
|
85
|
+
setChildren(repoSlot, [repoBtn]);
|
|
86
|
+
}
|
|
87
|
+
for (const id of Object.keys(links)) links[id].setAttribute('aria-current', id === model.dest ? 'page' : 'false');
|
|
88
|
+
const n = Math.max(0, Number(model.needs) || 0);
|
|
89
|
+
if (count.textContent !== String(n)) count.textContent = String(n);
|
|
90
|
+
todayMark.hidden = n === 0;
|
|
91
|
+
count.setAttribute('aria-label', COPY.needs(n));
|
|
92
|
+
const want = model.unsynced || model.syncing ? (model.syncing ? COPY.sync.busy : COPY.sync.label) : null;
|
|
93
|
+
if (want !== syncLabel) {
|
|
94
|
+
syncLabel = want;
|
|
95
|
+
syncBtn = want ? syncButton(want, !!model.syncing) : null;
|
|
96
|
+
}
|
|
97
|
+
setChildren(node, [repoSlot, nav, search, syncBtn, links.settings]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
update({});
|
|
101
|
+
return { node, update, links };
|
|
102
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* pm/triage-card.js — the triage card of Today, kind 6 (pm-os-absorb Run 2, slice 2D; PRD R12, R17).
|
|
2
|
+
*
|
|
3
|
+
* "Proposed: <title> · <lane> · <anchor> · Add". A capture was triaged by an agent; one press of Add writes the
|
|
4
|
+
* initiative (`pmos initiative new --from <capture>`), and it starts in Later. The row's run id ends with the capture's id.
|
|
5
|
+
*
|
|
6
|
+
* The writer takes a title, a lane and an anchor and no more, so the card reads them back from the row's own line. When the
|
|
7
|
+
* line says "no anchor yet", the writer would refuse the initiative: the card opens the New-initiative form with the title
|
|
8
|
+
* and the lane filled in, and the PM chooses the anchor.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { button, card, toast, ensureComponentsCss } from './primitives.js';
|
|
12
|
+
import { registerTodayCard } from './registry.js';
|
|
13
|
+
import { COPY } from './initiatives-copy.js';
|
|
14
|
+
import { createInitiative } from './initiatives-api.js';
|
|
15
|
+
import { changed } from './initiative-sheet.js';
|
|
16
|
+
import { openNewInitiative } from './new-initiative-sheet.js';
|
|
17
|
+
|
|
18
|
+
const captureOf = (row) => String(row.run_id || '').replace(/^initiative-needs:triage:/, '');
|
|
19
|
+
const titleOf = (q) => String(q || '').replace(/^Proposed:\s*/, '').split(' · ')[0];
|
|
20
|
+
|
|
21
|
+
/** "Proposed: <title> · <lane label> · <anchor>" → { title, lane, anchor }, or null. The anchor is null for "no anchor yet". */
|
|
22
|
+
export function proposalOf(question) {
|
|
23
|
+
const parts = String(question || '').replace(/^Proposed:\s*/, '').split(' · ');
|
|
24
|
+
if (parts.length < 3) return null;
|
|
25
|
+
const anchor = parts.pop().trim();
|
|
26
|
+
const lane = parts.pop().trim().replace(/\s+/g, '-');
|
|
27
|
+
const title = parts.join(' · ').trim();
|
|
28
|
+
return title && lane ? { title, lane, anchor: /^no anchor yet$/i.test(anchor) ? null : anchor } : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderTriageCard(row, ctx) {
|
|
32
|
+
ensureComponentsCss();
|
|
33
|
+
let busy = false;
|
|
34
|
+
const add = button({
|
|
35
|
+
label: (row.today && row.today.act && row.today.act.label) || COPY.triage.add, variant: 'primary', attrs: { 'data-in-triage-add': '' },
|
|
36
|
+
onClick: async () => {
|
|
37
|
+
if (busy) return;
|
|
38
|
+
const p = proposalOf(row.question);
|
|
39
|
+
if (!p) { toast({ text: COPY.triage.addFailed(COPY.triage.unreadable), tone: 'error', timeout: 8000 }); return; }
|
|
40
|
+
if (!p.anchor) {
|
|
41
|
+
openNewInitiative({ repo: ctx.repo, ws: ctx.ws, anchors: [], prefill: { title: p.title, lane: p.lane, from: captureOf(row) }, onAdded: () => { changed(); try { window.dispatchEvent(new CustomEvent('pmos:ledgers', { detail: { changed: [ctx.ws] } })); } catch { /* no window */ } } });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
busy = true; add.disabled = true;
|
|
45
|
+
const out = await createInitiative(ctx.ws, { title: p.title, lane: p.lane, anchor: p.anchor, from: captureOf(row) });
|
|
46
|
+
busy = false; add.disabled = false;
|
|
47
|
+
if (out && out.ok) {
|
|
48
|
+
toast({ text: COPY.triage.added(titleOf(row.question)), timeout: 4000 });
|
|
49
|
+
changed();
|
|
50
|
+
try { window.dispatchEvent(new CustomEvent('pmos:ledgers', { detail: { changed: [ctx.ws] } })); } catch { /* no window */ }
|
|
51
|
+
} else toast({ text: COPY.triage.addFailed((out && out.error) || 'The server did not answer.'), tone: 'error', timeout: 8000 });
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return card({
|
|
55
|
+
title: row.question, meta: [row.today && row.today.cause, row.today && row.today.evidence], trail: [add],
|
|
56
|
+
attrs: { class: 'in-today-card', 'data-today-row': 'triage', 'data-provider': row.provider || null, 'data-capture': captureOf(row) },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
registerTodayCard('triage', renderTriageCard);
|