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,270 @@
|
|
|
1
|
+
/* pm/dest-reports.js — the Reports destination (pm-os-absorb Run 5, slice 5C; PRD R9, R22.5, R25, R29).
|
|
2
|
+
*
|
|
3
|
+
* A DESTINATION MODULE (registry.js): mountDestination(host, ctx) on every render with the same host, disposeDestination(host).
|
|
4
|
+
* It keeps ONE persistent root per host and rebuilds it only when its own data changed.
|
|
5
|
+
*
|
|
6
|
+
* What it shows, for the repository the tab names (or the open one):
|
|
7
|
+
* - the report schedules of that repository, joined to their report by the schedule's `skill` (report-biweekly →
|
|
8
|
+
* biweekly), never by its cadence; Pause and Resume write through the schedules route; one press sets the recommended
|
|
9
|
+
* schedules up; the schedules that feed no report are one disclosure, "Other schedules (n)";
|
|
10
|
+
* - the reports, newest first: the 4 newest, and "Show older";
|
|
11
|
+
* - the selected report, read from its file: Edit notes, Redraft, Issue, and for an issued report Print / PDF and Copy as
|
|
12
|
+
* markdown. Issue is the PM's act and takes 2 taps (report-view.js openIssueSheet).
|
|
13
|
+
* Nothing here sends a report anywhere. The writes go through /api/report/* (lib/routes-reports.js), which calls the guards.
|
|
14
|
+
*
|
|
15
|
+
* It reads /api/report and /api/schedules?all=1 once, then again when its own `sig` changes (sig-watch.js, seams §4.9) or
|
|
16
|
+
* after its own writes. In fixture mode it reads nothing and starts no watcher.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { el } from './dom.js';
|
|
20
|
+
import { button, iconButton, row, signal, menu, toast, empty, ensureComponentsCss } from './primitives.js';
|
|
21
|
+
import { ensureSprite } from './icons.js';
|
|
22
|
+
import { pmGet, pmPost } from './http.js';
|
|
23
|
+
import { watchSig } from './sig-watch.js';
|
|
24
|
+
import { wsOf } from './data.js';
|
|
25
|
+
import { COPY, cadenceName } from './reports-copy.js';
|
|
26
|
+
import { renderReport, openIssueSheet, openNotesSheet, draftNow, printReport, copyMarkdown, clearPrint, ensurePrintCss } from './report-view.js';
|
|
27
|
+
|
|
28
|
+
const SHOWN = 4; // the reports listed before "Show older"
|
|
29
|
+
const insts = new WeakMap(); // host -> instance
|
|
30
|
+
|
|
31
|
+
/** report-biweekly -> biweekly. The same join as lib/report-needs.js cadenceOfSkill. */
|
|
32
|
+
const cadenceOfSkill = (skill) => { const m = /^report-([a-z][a-z0-9-]{0,39})$/.exec(String(skill || '')); return m ? m[1] : null; };
|
|
33
|
+
const keyOf = (r) => r.cadence + ':' + r.slot;
|
|
34
|
+
|
|
35
|
+
function whenOf(s) {
|
|
36
|
+
const c = String(s.cadence || '');
|
|
37
|
+
const on = c === 'weekly' || c === 'bi-weekly' || c === 'biweekly' ? s.day : c === 'monthly' && s.dayOfMonth ? 'day ' + s.dayOfMonth : '';
|
|
38
|
+
return [c, on, s.time].filter(Boolean).join(' ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function make(host) {
|
|
42
|
+
const root = el('div', { class: 'rp-page', 'data-reports': '' });
|
|
43
|
+
const inst = {
|
|
44
|
+
host, root, ws: null, loaded: false, error: null, reports: [], flags: [], sig: null, schedules: [], doc: null, docKey: null,
|
|
45
|
+
sel: null, showAll: false, otherOpen: false, stop: null, seq: 0, last: '',
|
|
46
|
+
};
|
|
47
|
+
return inst;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mountDestination(host, ctx) {
|
|
51
|
+
let inst = insts.get(host);
|
|
52
|
+
if (!inst) { inst = make(host); insts.set(host, inst); }
|
|
53
|
+
ensureComponentsCss();
|
|
54
|
+
if (!inst.root.isConnected || inst.root.parentNode !== host) host.replaceChildren(inst.root);
|
|
55
|
+
if (ctx && ctx.FIXTURE) { inst.root.replaceChildren(empty({ text: COPY.fixture })); return; }
|
|
56
|
+
const repo = (ctx && ctx.tab && ctx.tab.repo) || (ctx && ctx.state && ctx.state.product) || null;
|
|
57
|
+
const ws = repo ? wsOf(repo) : null;
|
|
58
|
+
if (ws !== inst.ws) {
|
|
59
|
+
if (inst.stop) { inst.stop(); inst.stop = null; }
|
|
60
|
+
Object.assign(inst, { ws, loaded: false, error: null, reports: [], flags: [], sig: null, schedules: [], doc: null, docKey: null, sel: null, showAll: false, last: '' });
|
|
61
|
+
if (ws) {
|
|
62
|
+
ensureSprite();
|
|
63
|
+
ensurePrintCss();
|
|
64
|
+
inst.stop = watchSig(() => pmGet('/api/report?ws=' + encodeURIComponent(ws)).then((j) => (j && j.ok ? j.sig : null)), () => refresh(inst));
|
|
65
|
+
refresh(inst);
|
|
66
|
+
}
|
|
67
|
+
draw(inst);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!inst.last) draw(inst);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function disposeDestination(host) {
|
|
74
|
+
const inst = insts.get(host);
|
|
75
|
+
if (!inst) return;
|
|
76
|
+
if (inst.stop) inst.stop();
|
|
77
|
+
inst.stop = null; inst.seq++;
|
|
78
|
+
clearPrint();
|
|
79
|
+
inst.root.remove();
|
|
80
|
+
insts.delete(host);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ── reads ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
84
|
+
|
|
85
|
+
/** Read the list and the schedules, then the selected report; draw only when something changed. */
|
|
86
|
+
async function refresh(inst) {
|
|
87
|
+
const ws = inst.ws;
|
|
88
|
+
if (!ws) return;
|
|
89
|
+
const seq = ++inst.seq;
|
|
90
|
+
const [list, sched] = await Promise.all([pmGet('/api/report?ws=' + encodeURIComponent(ws)), pmGet('/api/schedules?all=1')]);
|
|
91
|
+
if (seq !== inst.seq || ws !== inst.ws) return;
|
|
92
|
+
inst.loaded = true;
|
|
93
|
+
if (!list || list.ok === false) { inst.error = (list && list.error) || COPY.readError; inst.reports = []; }
|
|
94
|
+
else { inst.error = null; inst.reports = list.reports || []; inst.flags = list.flags || []; inst.sig = list.sig; }
|
|
95
|
+
inst.schedules = sched && Array.isArray(sched.schedules) ? sched.schedules.filter((s) => s && s.ws === ws) : [];
|
|
96
|
+
const keys = inst.reports.map(keyOf);
|
|
97
|
+
if (!inst.sel || !keys.includes(inst.sel)) inst.sel = keys[0] || null;
|
|
98
|
+
await readDoc(inst, seq);
|
|
99
|
+
if (seq === inst.seq) draw(inst);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function readDoc(inst, seq) {
|
|
103
|
+
const r = inst.reports.find((x) => keyOf(x) === inst.sel);
|
|
104
|
+
if (!r) { inst.doc = null; inst.docKey = null; return; }
|
|
105
|
+
const out = await pmGet('/api/report/draft?ws=' + encodeURIComponent(inst.ws) + '&cadence=' + encodeURIComponent(r.cadence) + '&slot=' + encodeURIComponent(r.slot));
|
|
106
|
+
if (seq !== inst.seq) return;
|
|
107
|
+
inst.doc = out && out.ok && out.exists ? out : null;
|
|
108
|
+
inst.docKey = inst.doc ? inst.sel : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const selectReport = async (inst, key) => {
|
|
112
|
+
inst.sel = key; inst.doc = null; inst.docKey = null;
|
|
113
|
+
draw(inst);
|
|
114
|
+
const seq = ++inst.seq;
|
|
115
|
+
await readDoc(inst, seq);
|
|
116
|
+
if (seq === inst.seq) draw(inst);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/* ── the page ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
const selected = (inst) => inst.reports.find((x) => keyOf(x) === inst.sel) || null;
|
|
122
|
+
/** A report that is not issued can still be read, noted and issued, whoever wrote it (X36). */
|
|
123
|
+
const isDraft = (r) => !!r && !r.issued;
|
|
124
|
+
|
|
125
|
+
/** Which cadence "Draft now" writes: the selected report's, else the first report schedule's, else biweekly. */
|
|
126
|
+
function draftCadence(inst) {
|
|
127
|
+
const r = selected(inst);
|
|
128
|
+
if (r) return r.cadence;
|
|
129
|
+
const s = inst.schedules.map((x) => cadenceOfSkill(x.skill)).find(Boolean);
|
|
130
|
+
return s || 'biweekly';
|
|
131
|
+
}
|
|
132
|
+
const knownCadences = (inst) => [...new Set([...inst.reports.map((r) => r.cadence), ...inst.schedules.map((s) => cadenceOfSkill(s.skill)).filter(Boolean)])].sort();
|
|
133
|
+
|
|
134
|
+
async function afterDraft(inst, r) {
|
|
135
|
+
if (r && r.ok && r.slot) inst.sel = (r.cadence || draftCadence(inst)) + ':' + r.slot;
|
|
136
|
+
await refresh(inst);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function primaryAction(inst) {
|
|
140
|
+
const r = selected(inst);
|
|
141
|
+
if (isDraft(r)) {
|
|
142
|
+
return button({ label: COPY.issue, variant: 'primary', attrs: { 'data-rp-act': 'issue', 'data-rp-key': 'primary' },
|
|
143
|
+
onClick: () => openIssueSheet({ ws: inst.ws, cadence: r.cadence, slot: r.slot, onDone: () => refresh(inst) }) });
|
|
144
|
+
}
|
|
145
|
+
const cadence = draftCadence(inst);
|
|
146
|
+
return button({ label: COPY.draftNow, variant: 'primary', attrs: { 'data-rp-act': 'draft-now', 'data-rp-key': 'primary' },
|
|
147
|
+
onClick: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence })) });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* the schedules, split by what they feed */
|
|
151
|
+
function schedulesBlock(inst) {
|
|
152
|
+
const feeds = inst.schedules.filter((s) => cadenceOfSkill(s.skill));
|
|
153
|
+
const others = inst.schedules.filter((s) => !cadenceOfSkill(s.skill));
|
|
154
|
+
const sec = el('section', { class: 'rp-section', 'data-rp-section': 'schedules', 'aria-label': COPY.schedulesTitle }, el('h3', { class: 'rp-section__title' }, COPY.schedulesTitle));
|
|
155
|
+
if (!feeds.length) {
|
|
156
|
+
sec.appendChild(el('p', { class: 'rp-section__empty' }, COPY.schedulesNone));
|
|
157
|
+
sec.appendChild(button({ label: COPY.schedulesSeed, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'seed', 'data-rp-key': 'seed' }, onClick: async () => {
|
|
158
|
+
const r = await pmPost('/api/schedules/seed', { ws: inst.ws, set: 'reports' });
|
|
159
|
+
if (r && r.ok) toast({ text: COPY.schedulesSeeded((r.created || []).length), timeout: 4000 }); else toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' });
|
|
160
|
+
await refresh(inst);
|
|
161
|
+
} }));
|
|
162
|
+
}
|
|
163
|
+
const list = el('div', { class: 'rp-list', role: 'list' });
|
|
164
|
+
for (const s of feeds) {
|
|
165
|
+
const on = s.enabled !== false;
|
|
166
|
+
const cadence = cadenceOfSkill(s.skill);
|
|
167
|
+
list.appendChild(row({
|
|
168
|
+
title: s.name || cadenceName(cadence),
|
|
169
|
+
meta: [COPY.scheduleMeta(whenOf(s), s.lastRun ? COPY.lastRan(s.lastRun) : COPY.neverRan)],
|
|
170
|
+
signals: on ? [] : [signal({ text: COPY.paused, tone: 'warning' })],
|
|
171
|
+
trail: [button({ label: on ? COPY.pause : COPY.resume, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': on ? 'pause' : 'resume', 'data-rp-key': 'sched:' + s.id }, onClick: async () => {
|
|
172
|
+
const r = await pmPost('/api/schedules/update', { id: s.id, enabled: !on });
|
|
173
|
+
if (!r || !r.ok) { toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' }); }
|
|
174
|
+
await refresh(inst);
|
|
175
|
+
} })],
|
|
176
|
+
attrs: { 'data-schedule': s.id, 'data-cadence': cadence, role: 'listitem' },
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
if (feeds.length) sec.appendChild(list);
|
|
180
|
+
if (others.length) {
|
|
181
|
+
sec.appendChild(button({ label: COPY.otherSchedules(others.length), variant: 'ghost', size: 'sm', icon: inst.otherOpen ? 'chevron-down' : 'chevron-right',
|
|
182
|
+
attrs: { 'data-rp-act': 'other', 'data-rp-key': 'other', 'aria-expanded': inst.otherOpen ? 'true' : 'false' }, onClick: () => { inst.otherOpen = !inst.otherOpen; draw(inst); } }));
|
|
183
|
+
if (inst.otherOpen) {
|
|
184
|
+
sec.appendChild(el('div', { class: 'rp-list', role: 'list', 'data-rp-section': 'other-schedules' }, others.map((s) => row({
|
|
185
|
+
title: s.name || s.skill, meta: [COPY.scheduleMeta(whenOf(s), s.lastRun ? COPY.lastRan(s.lastRun) : COPY.neverRan)],
|
|
186
|
+
signals: s.enabled === false ? [signal({ text: COPY.paused, tone: 'warning' })] : [], attrs: { 'data-schedule': s.id, role: 'listitem' } }))));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return sec;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* the reports, newest first */
|
|
193
|
+
function listBlock(inst) {
|
|
194
|
+
const sec = el('section', { class: 'rp-section', 'data-rp-section': 'list', 'aria-label': COPY.list }, el('h3', { class: 'rp-section__title' }, COPY.list));
|
|
195
|
+
if (!inst.reports.length) { sec.appendChild(el('p', { class: 'rp-section__empty' }, COPY.none)); return sec; }
|
|
196
|
+
const shown = inst.showAll ? inst.reports : inst.reports.slice(0, SHOWN);
|
|
197
|
+
// the selected report stays on screen even when it is older than the shown ones
|
|
198
|
+
const sel = selected(inst);
|
|
199
|
+
const rows = sel && !shown.includes(sel) ? [...shown, sel] : shown;
|
|
200
|
+
const list = el('div', { class: 'rp-list', role: 'list' });
|
|
201
|
+
for (const r of rows) {
|
|
202
|
+
const key = keyOf(r);
|
|
203
|
+
list.appendChild(row({
|
|
204
|
+
title: COPY.rowTitle(r.cadence, r.slot),
|
|
205
|
+
meta: [r.issued ? COPY.issued(r.issued.at, r.issued.by) : COPY.draft],
|
|
206
|
+
signals: r.edited ? [signal({ text: COPY.edited, tone: 'warning', title: COPY.editedWhy })] : [],
|
|
207
|
+
owed: isDraft(r),
|
|
208
|
+
onOpen: () => selectReport(inst, key),
|
|
209
|
+
openAttrs: { 'aria-current': key === inst.sel ? 'true' : null, 'data-rp-key': 'row:' + key },
|
|
210
|
+
attrs: { class: key === inst.sel ? 'rp-list__row rp-list__row--on' : 'rp-list__row', 'data-report': key, 'data-status': r.issued ? 'issued' : 'draft', role: 'listitem' },
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
sec.appendChild(list);
|
|
214
|
+
if (inst.reports.length > SHOWN) {
|
|
215
|
+
sec.appendChild(button({ label: inst.showAll ? COPY.showFewer : COPY.showOlder(inst.reports.length - SHOWN), variant: 'ghost', size: 'sm',
|
|
216
|
+
icon: inst.showAll ? 'chevron-down' : 'chevron-right', attrs: { 'data-rp-act': 'older', 'data-rp-key': 'older' }, onClick: () => { inst.showAll = !inst.showAll; draw(inst); } }));
|
|
217
|
+
}
|
|
218
|
+
return sec;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* the selected report and what can be done to it */
|
|
222
|
+
function viewerBlock(inst) {
|
|
223
|
+
const r = selected(inst);
|
|
224
|
+
const sec = el('section', { class: 'rp-section rp-viewer', 'data-rp-section': 'viewer', 'aria-label': r ? COPY.reportName(r.cadence, r.slot) : COPY.title });
|
|
225
|
+
if (!r) return sec;
|
|
226
|
+
const doc = inst.doc && inst.docKey === inst.sel ? inst.doc : null;
|
|
227
|
+
const bar = el('div', { class: 'rp-toolbar', role: 'toolbar', 'aria-label': COPY.reportName(r.cadence, r.slot) });
|
|
228
|
+
bar.appendChild(el('h3', { class: 'rp-section__title rp-toolbar__title' }, COPY.reportName(r.cadence, r.slot)));
|
|
229
|
+
const acts = el('div', { class: 'rp-toolbar__acts' });
|
|
230
|
+
if (r.issued) {
|
|
231
|
+
acts.appendChild(button({ label: COPY.print, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'print', 'data-rp-key': 'print' }, disabled: !doc, onClick: () => doc && printReport(doc.markdown) }));
|
|
232
|
+
acts.appendChild(button({ label: COPY.copyMd, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'copy', 'data-rp-key': 'copy' }, disabled: !doc, onClick: () => doc && copyMarkdown(doc.markdown) }));
|
|
233
|
+
} else {
|
|
234
|
+
acts.appendChild(button({ label: COPY.editNotes, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'notes', 'data-rp-key': 'notes' }, disabled: !doc,
|
|
235
|
+
onClick: () => doc && openNotesSheet({ ws: inst.ws, cadence: r.cadence, slot: r.slot, markdown: doc.markdown, onSaved: () => refresh(inst) }) }));
|
|
236
|
+
acts.appendChild(button({ label: COPY.redraft, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'redraft', 'data-rp-key': 'redraft' },
|
|
237
|
+
onClick: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: r.cadence, slot: r.slot, redraft: true })) }));
|
|
238
|
+
const more = iconButton({ icon: 'ellipsis', ariaLabel: COPY.more, expanded: false, size: 'sm', attrs: { 'data-rp-act': 'more', 'data-rp-key': 'more' }, onClick: (e) => {
|
|
239
|
+
const items = [{ label: COPY.draftNow, onSelect: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: r.cadence })) }];
|
|
240
|
+
for (const c of knownCadences(inst)) if (c !== r.cadence) items.push({ label: COPY.draftNow + ' · ' + c, onSelect: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: c })) });
|
|
241
|
+
items.push({ sep: true }, { label: COPY.print, onSelect: () => doc && printReport(doc.markdown) }, { label: COPY.copyMd, onSelect: () => doc && copyMarkdown(doc.markdown) });
|
|
242
|
+
menu({ anchor: e.currentTarget, items, ariaLabel: COPY.more });
|
|
243
|
+
} });
|
|
244
|
+
acts.appendChild(more);
|
|
245
|
+
}
|
|
246
|
+
bar.appendChild(acts);
|
|
247
|
+
sec.appendChild(bar);
|
|
248
|
+
if (r.issued) sec.appendChild(el('p', { class: 'rp-viewer__state' }, r.edited ? COPY.editedWhy : COPY.frozen));
|
|
249
|
+
sec.appendChild(doc ? renderReport(doc.markdown) : el('p', { class: 'rp-section__empty' }, COPY.loading));
|
|
250
|
+
return sec;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Rebuild the persistent root from the instance, if what it shows changed; keep the focus on the same control. */
|
|
254
|
+
function draw(inst) {
|
|
255
|
+
const root = inst.root;
|
|
256
|
+
if (!inst.ws) { root.replaceChildren(empty({ text: COPY.noRepo })); inst.last = 'none'; return; }
|
|
257
|
+
const fingerprint = JSON.stringify([inst.loaded, inst.error, inst.reports, inst.flags, inst.schedules, inst.sel, inst.doc && inst.doc.sha, inst.showAll, inst.otherOpen]);
|
|
258
|
+
if (fingerprint === inst.last && root.childNodes.length) return;
|
|
259
|
+
inst.last = fingerprint;
|
|
260
|
+
const keep = document.activeElement && root.contains(document.activeElement) ? document.activeElement.getAttribute('data-rp-key') : null;
|
|
261
|
+
const scroll = root.querySelector('.rp-viewer') ? root.scrollTop : 0;
|
|
262
|
+
if (!inst.loaded) { root.replaceChildren(el('p', { class: 'rp-section__empty' }, COPY.loading)); return; }
|
|
263
|
+
const kids = [el('div', { class: 'rp-head' }, el('h2', { class: 'rp-head__title' }, COPY.title), inst.error ? null : primaryAction(inst))];
|
|
264
|
+
if (inst.error) kids.push(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, inst.error));
|
|
265
|
+
if (inst.flags && inst.flags.length) kids.push(el('p', { class: 'rp-flag' }, signal({ text: COPY.flagTitle, tone: 'warning', title: inst.flags[0].why })));
|
|
266
|
+
if (!inst.error) kids.push(el('div', { class: 'rp-cols' }, el('div', { class: 'rp-col rp-col--list' }, schedulesBlock(inst), listBlock(inst)), el('div', { class: 'rp-col rp-col--doc' }, viewerBlock(inst))));
|
|
267
|
+
root.replaceChildren(...kids.filter(Boolean));
|
|
268
|
+
root.scrollTop = scroll;
|
|
269
|
+
if (keep) { const again = root.querySelector('[data-rp-key="' + keep.replace(/"/g, '') + '"]'); if (again && !again.disabled) again.focus({ preventScroll: true }); }
|
|
270
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/* pm/dest-settings.js — the Settings destination, the frame's ⚙ (pm-os-absorb Run 3, slice 3A; PRD R9).
|
|
2
|
+
*
|
|
3
|
+
* Settings holds settings only, in five sections and in this order:
|
|
4
|
+
* Repositories every registered repository as a row (open it to make it current; ⋯ for more), and Add repository,
|
|
5
|
+
* which draws the existing add-repository flow (view-addrepo.js) in place
|
|
6
|
+
* Profiles one row that opens the Claude profiles in the detail
|
|
7
|
+
* New sessions the default CLI and the default model the dock's + Session applies (dock.js)
|
|
8
|
+
* Validate consent the current repository's consent switch (GET and POST /api/validate/repo)
|
|
9
|
+
* Theme Dark or Light
|
|
10
|
+
* The work itself (what needs you, initiatives, items, reports) is never here.
|
|
11
|
+
*
|
|
12
|
+
* The mount contract (registry.js): mountDestination(host, ctx) is called on every render with the same host. The page
|
|
13
|
+
* keeps one root per host and rebuilds it only when what it shows changed, and never while the PM types in it, so a
|
|
14
|
+
* focused control keeps its focus across the workspace's polls.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { el, setChildren } from './dom.js';
|
|
18
|
+
import { state, setState, FIXTURE } from './state.js';
|
|
19
|
+
import { pmGet, pmPost } from './http.js';
|
|
20
|
+
import { button, iconButton, field, badge, row, tabs, menu, empty } from './primitives.js';
|
|
21
|
+
import { repoTuples, activeProduct, renameRepo, removeRepo } from './panel-repos.js';
|
|
22
|
+
import { repoRow } from './fixture.js';
|
|
23
|
+
import { sessionDefaults, setSessionDefaults } from './dock.js';
|
|
24
|
+
import { COPY } from './frame-copy.js';
|
|
25
|
+
|
|
26
|
+
const P = COPY.page;
|
|
27
|
+
const roots = new WeakMap(); // host -> { root, sig }
|
|
28
|
+
const consentByWs = new Map(); // ws -> { on: boolean | null, error, at, pending }
|
|
29
|
+
let addOpen = false;
|
|
30
|
+
let addMod = null;
|
|
31
|
+
|
|
32
|
+
const frame = () => (window.pmApp && window.pmApp.frame) || null;
|
|
33
|
+
const dataMod = () => (window.pmApp && window.pmApp.mods && window.pmApp.mods.data) || null;
|
|
34
|
+
const wsOfRepo = (name) => { const d = dataMod(); return name && d && typeof d.wsOf === 'function' ? d.wsOf(name) : null; };
|
|
35
|
+
|
|
36
|
+
function readConsent(ws) {
|
|
37
|
+
if (!ws || FIXTURE) return;
|
|
38
|
+
const have = consentByWs.get(ws);
|
|
39
|
+
if (have && (have.pending || Date.now() - have.at < 30000)) return;
|
|
40
|
+
const entry = { on: have ? have.on : null, error: null, at: Date.now(), pending: true };
|
|
41
|
+
consentByWs.set(ws, entry);
|
|
42
|
+
pmGet('/api/validate/repo?ws=' + encodeURIComponent(ws)).then((j) => {
|
|
43
|
+
entry.pending = false; entry.at = Date.now();
|
|
44
|
+
if (j && j.ok !== false) { entry.on = !!j.consent; entry.error = null; } else entry.error = (j && j.error) || P.validateUnread;
|
|
45
|
+
setState({});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function setConsent(ws, on) {
|
|
50
|
+
const j = await pmPost('/api/validate/repo', { ws, consent: on });
|
|
51
|
+
consentByWs.set(ws, { on: j && j.ok !== false ? !!j.consent : null, error: j && j.ok !== false ? null : ((j && j.error) || P.validateUnread), at: Date.now() });
|
|
52
|
+
setState({});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const section = (id, title, kids, action) => el('section', { class: 'fr-set__section', 'data-fr-section': id },
|
|
56
|
+
el('div', { class: 'fr-set__head' }, el('h2', { class: 'fr-set__title' }, title), action || null),
|
|
57
|
+
kids);
|
|
58
|
+
|
|
59
|
+
/* ── the five sections ──────────────────────────────────────────────────────────────────────────────────────────── */
|
|
60
|
+
|
|
61
|
+
function reposSection(scope) {
|
|
62
|
+
const tuples = repoTuples();
|
|
63
|
+
const current = scope === 'all' ? null : activeProduct(tuples);
|
|
64
|
+
const rows = tuples.map((t) => {
|
|
65
|
+
const rp = repoRow(t);
|
|
66
|
+
const on = rp.name === current;
|
|
67
|
+
const use = () => { const f = frame(); if (f) f.chooseRepo(rp.name); };
|
|
68
|
+
const more = iconButton({
|
|
69
|
+
icon: 'ellipsis', ariaLabel: P.more(rp.name), attrs: { 'data-fr': 'repo-more' },
|
|
70
|
+
onClick: () => menu({
|
|
71
|
+
anchor: more, ariaLabel: P.more(rp.name),
|
|
72
|
+
items: [
|
|
73
|
+
{ label: P.use, onSelect: use },
|
|
74
|
+
{ label: P.show, onSelect: () => { use(); const f = frame(); if (f) f.go('initiatives'); } },
|
|
75
|
+
{ label: P.rename, onSelect: () => renameRepo(rp.name) },
|
|
76
|
+
{ label: P.remove, onSelect: () => removeRepo(rp.name) },
|
|
77
|
+
],
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
return row({
|
|
81
|
+
title: rp.name, meta: [rp.kindLabel], signals: on ? [badge({ text: P.current, tone: 'outline' })] : [],
|
|
82
|
+
onOpen: use, openLabel: P.use + ': ' + rp.name, trail: [more],
|
|
83
|
+
attrs: { 'data-fr-row': 'repo', 'data-repo': rp.name, 'data-current': on ? 'true' : 'false' },
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
const add = button({
|
|
87
|
+
label: addOpen ? P.addClose : P.add, variant: 'outline', size: 'sm', icon: addOpen ? 'x' : 'plus',
|
|
88
|
+
attrs: { 'data-fr': 'add-repo', 'aria-expanded': addOpen ? 'true' : 'false' },
|
|
89
|
+
onClick: () => { addOpen = !addOpen; if (addOpen && !addMod) loadAdd(); setState({}); },
|
|
90
|
+
});
|
|
91
|
+
const kids = rows.length ? rows : [empty({ text: P.reposEmpty })];
|
|
92
|
+
if (addOpen) kids.push(addBoard());
|
|
93
|
+
return section('repositories', P.repos, kids, add);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadAdd() {
|
|
97
|
+
import('./view-addrepo.js').then((m) => { addMod = m; setState({}); }).catch(() => { addMod = false; setState({}); });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The existing add-repository board, drawn in place: the same form, scan review and confirm as the old tab. */
|
|
101
|
+
function addBoard() {
|
|
102
|
+
const host = el('div', { class: 'fr-set__add', 'data-fr': 'add-board' });
|
|
103
|
+
if (!addMod || typeof addMod.renderBoard !== 'function') return host;
|
|
104
|
+
const cockpit = window.pmApp && window.pmApp.mods ? window.pmApp.mods.cockpit : null;
|
|
105
|
+
try {
|
|
106
|
+
addMod.renderBoard(host, {
|
|
107
|
+
tab: { id: 'settings:addrepo', kind: 'view', view: 'addrepo' }, view: 'addrepo', renderMode: () => {}, ready: true,
|
|
108
|
+
el, setChildren, state, setState, FIXTURE, openView: cockpit ? cockpit.openView : () => {}, repo: state.product || null,
|
|
109
|
+
});
|
|
110
|
+
} catch (e) { console.error('[pm] Add repository failed:', e); }
|
|
111
|
+
return host;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function profilesSection() {
|
|
115
|
+
const open = () => setState({ detail: { kind: 'prof' }, col2: true });
|
|
116
|
+
// every row has its ⋯, here as on the repositories: on a phone it is the one control a row is known by
|
|
117
|
+
const more = iconButton({
|
|
118
|
+
icon: 'ellipsis', ariaLabel: P.more(P.profilesRow), attrs: { 'data-fr': 'profiles-more' },
|
|
119
|
+
onClick: () => menu({ anchor: more, ariaLabel: P.more(P.profilesRow), items: [{ label: P.profilesOpen, onSelect: open }] }),
|
|
120
|
+
});
|
|
121
|
+
return section('profiles', P.profiles, [row({
|
|
122
|
+
title: P.profilesRow, meta: [P.profilesMeta], onOpen: open, openLabel: P.profilesOpen, trail: [more],
|
|
123
|
+
attrs: { 'data-fr-row': 'profiles' },
|
|
124
|
+
})]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sessionsSection() {
|
|
128
|
+
const d = sessionDefaults();
|
|
129
|
+
const cli = field({
|
|
130
|
+
label: P.cli, kind: 'select', value: d.cli,
|
|
131
|
+
options: [{ value: 'shell', label: P.cliShell }, { value: 'claude', label: P.cliClaude }],
|
|
132
|
+
onCommit: (v) => { setSessionDefaults({ cli: v }); setState({}); },
|
|
133
|
+
attrs: { 'data-fr': 'default-cli' },
|
|
134
|
+
});
|
|
135
|
+
const model = field({
|
|
136
|
+
label: P.model, kind: 'select', value: d.tier,
|
|
137
|
+
options: [['auto', P.modelAuto], ['fast', P.modelFast], ['balanced', P.modelBalanced], ['deep', P.modelDeep]].map(([value, label]) => ({ value, label })),
|
|
138
|
+
onCommit: (v) => { setSessionDefaults({ tier: v }); setState({}); },
|
|
139
|
+
attrs: { 'data-fr': 'default-model' },
|
|
140
|
+
});
|
|
141
|
+
return section('sessions', P.sessions, [el('div', { class: 'fr-set__fields' }, cli, model), el('p', { class: 'fr-set__note' }, P.sessionsNote)]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateSection(scope) {
|
|
145
|
+
const repo = scope === 'all' ? null : activeProduct();
|
|
146
|
+
const ws = wsOfRepo(repo);
|
|
147
|
+
if (!ws) return section('validate', P.validate, [empty({ text: P.validateNoRepo })]);
|
|
148
|
+
readConsent(ws);
|
|
149
|
+
const c = consentByWs.get(ws) || {};
|
|
150
|
+
const known = c.on === true || c.on === false;
|
|
151
|
+
const line = c.error ? P.validateUnread : !known ? '' : c.on ? P.validateOn : P.validateOff;
|
|
152
|
+
const toggle = known ? button({
|
|
153
|
+
label: c.on ? P.turnOff : P.turnOn, variant: 'outline', size: 'sm',
|
|
154
|
+
attrs: { 'data-fr': 'consent-toggle' }, onClick: () => setConsent(ws, !c.on),
|
|
155
|
+
}) : null;
|
|
156
|
+
// Validate itself is a tool, not a place: it opens as a tab in the dock, from here only
|
|
157
|
+
const open = button({
|
|
158
|
+
label: P.validateOpen, variant: 'outline', size: 'sm', attrs: { 'data-fr': 'open-validate' },
|
|
159
|
+
onClick: () => {
|
|
160
|
+
const cockpit = window.pmApp && window.pmApp.mods ? window.pmApp.mods.cockpit : null;
|
|
161
|
+
if (cockpit && typeof cockpit.openView === 'function') cockpit.openView('validate');
|
|
162
|
+
const f = frame(); if (f) f.openDock();
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return section('validate', P.validate, [el('div', { class: 'fr-set__line' },
|
|
166
|
+
el('span', { 'data-fr': 'consent-state', 'data-on': known ? String(c.on) : 'unread' }, line), toggle, open)]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function themeSection() {
|
|
170
|
+
return section('theme', P.theme, [tabs({
|
|
171
|
+
variant: 'segmented', role: 'radiogroup', ariaLabel: P.theme, value: state.theme,
|
|
172
|
+
items: [{ id: 'dark', label: P.dark }, { id: 'light', label: P.light }],
|
|
173
|
+
onChange: (id) => setState({ theme: id }),
|
|
174
|
+
attrs: { 'data-fr': 'theme' },
|
|
175
|
+
})]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── mount ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
179
|
+
|
|
180
|
+
function signature(scope) {
|
|
181
|
+
const ws = wsOfRepo(scope === 'all' ? null : activeProduct());
|
|
182
|
+
const addKeys = addOpen ? Object.keys(state).filter((k) => /^add[A-Z]/.test(k)).sort().map((k) => [k, state[k]]) : null;
|
|
183
|
+
let add = '';
|
|
184
|
+
try { add = JSON.stringify(addKeys); } catch { add = String(Date.now()); }
|
|
185
|
+
return JSON.stringify([repoTuples(), activeProduct(), scope, state.theme, sessionDefaults(), ws, consentByWs.get(ws) || null, addOpen, !!addMod]) + add;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** True while the PM is typing in the page or a menu of it is open: a rebuild then would take the focus away. */
|
|
189
|
+
function busy(root) {
|
|
190
|
+
const a = document.activeElement;
|
|
191
|
+
return !!a && root.contains(a) && /^(INPUT|TEXTAREA|SELECT)$/.test(a.tagName);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function mountDestination(host, ctx) {
|
|
195
|
+
const scope = ctx && ctx.tab && ctx.tab.scope === 'all' ? 'all' : 'repo';
|
|
196
|
+
let r = roots.get(host);
|
|
197
|
+
if (!r) {
|
|
198
|
+
r = { root: el('div', { class: 'fr-set', 'data-fr': 'settings' }), sig: null };
|
|
199
|
+
roots.set(host, r);
|
|
200
|
+
}
|
|
201
|
+
if (r.root.parentNode !== host) setChildren(host, [r.root]);
|
|
202
|
+
const sig = signature(scope);
|
|
203
|
+
if (sig === r.sig || busy(r.root)) return;
|
|
204
|
+
r.sig = sig;
|
|
205
|
+
setChildren(r.root, [
|
|
206
|
+
el('header', { class: 'fr-set__intro' }, el('h1', { class: 'fr-set__h1' }, P.title), el('p', { class: 'fr-set__note' }, P.intro)),
|
|
207
|
+
reposSection(scope),
|
|
208
|
+
profilesSection(),
|
|
209
|
+
sessionsSection(),
|
|
210
|
+
validateSection(scope),
|
|
211
|
+
themeSection(),
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function disposeDestination(host) {
|
|
216
|
+
roots.delete(host);
|
|
217
|
+
}
|