create-pmos 0.123.0 → 0.125.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/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/nine-stages.js +81 -0
- package/payload/scripts/workflow-state.js +2 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-one-home-test.js +131 -0
- package/workspace/fixtures/initiatives-api/list.json +171 -3
- package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
- package/workspace/fixtures/initiatives-api/sheet.json +57 -1
- package/workspace/fixtures/one-home-baseline.json +83 -0
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/notes.md +1 -0
- package/workspace/lib/engine/nine-stages.js +81 -0
- package/workspace/lib/engine/workflow-state.js +2 -0
- package/workspace/lib/facts-async.js +51 -0
- package/workspace/lib/item-steps.js +52 -0
- package/workspace/lib/jira-status.js +8 -3
- package/workspace/lib/jobs-to-be-done.js +52 -0
- package/workspace/lib/outbox-store.js +87 -0
- package/workspace/lib/pm-items.js +115 -0
- package/workspace/lib/route-modules.js +21 -0
- package/workspace/lib/routes-briefing.js +100 -0
- package/workspace/lib/routes-c3-init.js +53 -0
- package/workspace/lib/routes-c3-life.js +119 -0
- package/workspace/lib/routes-c3-strategy.js +169 -0
- package/workspace/lib/routes-c3-timeline.js +191 -0
- package/workspace/lib/routes-calendar-import.js +60 -0
- package/workspace/lib/routes-calendar-views.js +61 -0
- package/workspace/lib/routes-doc-health.js +79 -0
- package/workspace/lib/routes-doc-sheet.js +169 -0
- package/workspace/lib/routes-forecast.js +85 -0
- package/workspace/lib/routes-hooks.js +121 -0
- package/workspace/lib/routes-initiatives.js +3 -0
- package/workspace/lib/routes-intake.js +24 -0
- package/workspace/lib/routes-outbox.js +97 -0
- package/workspace/lib/routes-pm-items.js +216 -0
- package/workspace/lib/routes-records.js +61 -0
- package/workspace/lib/routes-releases.js +129 -0
- package/workspace/lib/routes-report-facts.js +84 -0
- package/workspace/lib/routes-report-families.js +32 -0
- package/workspace/lib/routes-roadmap-ops.js +141 -0
- package/workspace/lib/routes-runs.js +119 -0
- package/workspace/lib/routes-signals.js +50 -0
- package/workspace/lib/run-answers.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +26 -0
- package/workspace/public/pm/briefing-deck.js +415 -0
- package/workspace/public/pm/briefing-model.js +176 -0
- package/workspace/public/pm/c3-breakdown.js +240 -0
- package/workspace/public/pm/c3-brief.js +278 -0
- package/workspace/public/pm/c3-cal-strategy.js +528 -0
- package/workspace/public/pm/c3-case.js +173 -0
- package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
- package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
- package/workspace/public/pm/c3-demo-cal2.js +66 -0
- package/workspace/public/pm/c3-demo-cal3.js +173 -0
- package/workspace/public/pm/c3-demo-data.js +560 -0
- package/workspace/public/pm/c3-demo-init.js +373 -0
- package/workspace/public/pm/c3-demo-life.js +77 -0
- package/workspace/public/pm/c3-demo-misc2.js +46 -0
- package/workspace/public/pm/c3-demo-plan2.js +30 -0
- package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
- package/workspace/public/pm/c3-demo-rep2.js +92 -0
- package/workspace/public/pm/c3-demo-term.js +153 -0
- package/workspace/public/pm/c3-demo-time.js +49 -0
- package/workspace/public/pm/c3-demo-work-ix.js +80 -0
- package/workspace/public/pm/c3-demo-work2.js +13 -0
- package/workspace/public/pm/c3-demo.js +113 -0
- package/workspace/public/pm/c3-doc-sheet.js +314 -0
- package/workspace/public/pm/c3-dock.js +180 -0
- package/workspace/public/pm/c3-docs.js +290 -0
- package/workspace/public/pm/c3-expand.js +96 -0
- package/workspace/public/pm/c3-init-extras.js +170 -0
- package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
- package/workspace/public/pm/c3-misc-panels.js +89 -0
- package/workspace/public/pm/c3-plan-panels.js +163 -0
- package/workspace/public/pm/c3-plan.js +192 -0
- package/workspace/public/pm/c3-roadmap.js +249 -0
- package/workspace/public/pm/c3-sheet.js +107 -0
- package/workspace/public/pm/c3-shell.js +637 -0
- package/workspace/public/pm/c3-signals.js +196 -0
- package/workspace/public/pm/c3-strategy-panels.js +45 -0
- package/workspace/public/pm/c3-swipe.js +138 -0
- package/workspace/public/pm/c3-terminal-data.js +26 -0
- package/workspace/public/pm/c3-terminal-edge.js +91 -0
- package/workspace/public/pm/c3-terminal-icons.js +41 -0
- package/workspace/public/pm/c3-terminal-results.js +141 -0
- package/workspace/public/pm/c3-terminal.js +52 -0
- package/workspace/public/pm/c3-tickets.js +163 -0
- package/workspace/public/pm/c3-timeline.js +201 -0
- package/workspace/public/pm/c3-toast.js +16 -0
- package/workspace/public/pm/calendar-views.js +1015 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1480 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- package/workspace/public/pm/dest-notes.js +25 -3
- package/workspace/public/pm/dest-reports.js +47 -1
- package/workspace/public/pm/dest-today.js +55 -1
- package/workspace/public/pm/doc-health-panel.js +110 -0
- package/workspace/public/pm/doc-rail.js +9 -3
- package/workspace/public/pm/lifecycle-lens-model.js +228 -0
- package/workspace/public/pm/lifecycle-lens.js +261 -0
- package/workspace/public/pm/new-initiative-sheet.js +116 -21
- package/workspace/public/pm/outbox-panel.js +125 -0
- package/workspace/public/pm/pm-work-view.js +919 -0
- package/workspace/public/pm/releases-view.js +248 -0
- package/workspace/public/pm/report-families-view.js +503 -0
- package/workspace/public/pm/run-card.js +244 -0
- package/workspace/public/pm/sessions-panel.js +149 -0
- package/workspace/public/pm/shell-bell.js +162 -0
- package/workspace/public/pm/shell-model.js +83 -0
- package/workspace/public/pm/shell-phone.js +124 -0
- package/workspace/public/pm/shell-readaloud.js +53 -0
- package/workspace/public/pm/shell-recording.js +110 -0
- package/workspace/public/pm/shell-ticker.js +90 -0
- package/workspace/public/pm/slide-confirm.js +163 -0
- package/workspace/public/pm/timeline-forecast-view.js +164 -0
- package/workspace/public/pm/today-day.js +8 -2
- package/workspace/public/pm/tokens.css +1 -0
- package/workspace/server.js +5 -2
- package/workspace/test-files.json +8 -0
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -25,6 +25,9 @@ import { openSheet } from './initiative-sheet.js';
|
|
|
25
25
|
import { openNewInitiative } from './new-initiative-sheet.js';
|
|
26
26
|
import './cycle-review.js';
|
|
27
27
|
import './triage-card.js';
|
|
28
|
+
import './releases-view.js';
|
|
29
|
+
import './lifecycle-lens.js';
|
|
30
|
+
import './timeline-forecast-view.js';
|
|
28
31
|
|
|
29
32
|
setInitiativeOpener(openSheet);
|
|
30
33
|
|
|
@@ -93,21 +96,31 @@ function draw(host, r, ctx, repo, ws) {
|
|
|
93
96
|
if (r.extra && r.extra.dispose) { try { r.extra.dispose(r.body); } catch { /* the view is gone */ } }
|
|
94
97
|
r.handle = null; r.extra = null;
|
|
95
98
|
|
|
99
|
+
// The Roadmap's control budget is fixed (first-paint-initiatives: PRD Q5, 24 plus the top bar), so only Timeline keeps its own
|
|
100
|
+
// tab; every other registered view (Releases, Lifecycle, Forecast, ...) sits behind one 'More' tab with a second row.
|
|
101
|
+
// Only the console3 views are grouped; any other view a later run registers keeps its own tab (the View-control contract).
|
|
102
|
+
const MORE_IDS = ['releases', 'lifecycle', 'forecast'];
|
|
103
|
+
const extras = initiativesViews().filter((v) => !BUILT_IN.includes(v.id)).map((v) => ({ id: v.id, label: v.label || v.id }));
|
|
104
|
+
const ownTab = extras.filter((v) => !MORE_IDS.includes(v.id));
|
|
105
|
+
const moreViews = extras.filter((v) => MORE_IDS.includes(v.id));
|
|
96
106
|
const views = [
|
|
97
107
|
{ id: 'roadmap', label: COPY.roadmap },
|
|
98
108
|
{ id: 'list', label: COPY.list },
|
|
99
|
-
...
|
|
109
|
+
...ownTab,
|
|
110
|
+
...(moreViews.length ? [{ id: 'more', label: 'More' }] : []),
|
|
100
111
|
];
|
|
101
|
-
if (!views.some((v) => v.id === r.view)) r.view = 'roadmap';
|
|
112
|
+
if (!views.some((v) => v.id === r.view) && !moreViews.some((v) => v.id === r.view)) r.view = 'roadmap';
|
|
102
113
|
// A phone shows Roadmap or List only, and has no view switch (pm/mobile.css): its screen is one job, to see where the
|
|
103
114
|
// initiatives stand and open one. ⌘K changes between the two. Any other view falls back to Roadmap here without being
|
|
104
115
|
// saved, so the desktop keeps its choice.
|
|
105
116
|
if (phone() && !BUILT_IN.includes(r.view)) r.view = 'roadmap';
|
|
117
|
+
const inMore = moreViews.some((v) => v.id === r.view);
|
|
106
118
|
r.key = keyOf(r, repo, ws);
|
|
107
119
|
|
|
108
120
|
const head = el('div', { class: 'in-head' },
|
|
109
121
|
el('h2', { class: 'in-head__title' }, COPY.title),
|
|
110
|
-
tabs({ variant: 'segmented', role: 'tablist', ariaLabel: COPY.views, items: views, value: r.view, attrs: { 'data-in-views': '' }, onChange: (id) => { r.view = id; saveView(
|
|
122
|
+
tabs({ variant: 'segmented', role: 'tablist', ariaLabel: COPY.views, items: views, value: inMore ? 'more' : r.view, attrs: { 'data-in-views': '' }, onChange: (id) => { r.view = id === 'more' ? moreViews[0].id : id; saveView(r.view); r.draw(r.ctx); } }),
|
|
123
|
+
inMore ? tabs({ variant: 'segmented', role: 'tablist', ariaLabel: 'More views', items: moreViews, value: r.view, attrs: { 'data-in-more': '' }, onChange: (id) => { r.view = id; saveView(id); r.draw(r.ctx); } }) : null,
|
|
111
124
|
// Scan repository (scan-documents S7): the visible form of the sheet menu's and ⌘K's re-scan. It needs a workspace.
|
|
112
125
|
ws ? button({ label: COPY.rescan, icon: 'refresh-cw', attrs: { 'data-in-scan': '', title: COPY.rescanHint }, onClick: () => rescanRepo(ws) }) : null,
|
|
113
126
|
button({ label: COPY.newInitiative, icon: 'plus', variant: 'primary', attrs: { 'data-in-new': '' }, onClick: () => r.openNew() }));
|
|
@@ -19,6 +19,8 @@ import { icon, ensureSprite } from './icons.js';
|
|
|
19
19
|
import { pmPost } from './http.js';
|
|
20
20
|
import { wsOf } from './data.js';
|
|
21
21
|
import { registerCommand, openDestination } from './registry.js';
|
|
22
|
+
import { mountPmWork, disposePmWork } from './pm-work-view.js';
|
|
23
|
+
import { mountSessions, disposeSessions } from './sessions-panel.js';
|
|
22
24
|
import { myWork, loadMyWork, onMyWork } from './my-work-data.js';
|
|
23
25
|
|
|
24
26
|
export const COPY = Object.freeze({
|
|
@@ -112,12 +114,16 @@ export function mountDestination(host, ctx) {
|
|
|
112
114
|
ensureSprite();
|
|
113
115
|
loadMyWork();
|
|
114
116
|
draw(inst);
|
|
117
|
+
if (!inst.items) { inst.items = el('div', { 'data-pm-items-host': '' }); inst.root.appendChild(inst.items); mountPmWork(inst.items, ctx); }
|
|
118
|
+
if (!inst.sessions) { inst.sessions = el('div', { 'data-sessions-host': '' }); inst.root.appendChild(inst.sessions); mountSessions(inst.sessions, ctx); }
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
export function disposeDestination(host) {
|
|
118
122
|
const inst = insts.get(host);
|
|
119
123
|
if (!inst) return;
|
|
120
124
|
if (inst.off) inst.off();
|
|
125
|
+
if (inst.items) disposePmWork(inst.items);
|
|
126
|
+
if (inst.sessions) disposeSessions(inst.sessions);
|
|
121
127
|
inst.root.remove();
|
|
122
128
|
insts.delete(host);
|
|
123
129
|
}
|
|
@@ -277,7 +283,8 @@ function draw(inst) {
|
|
|
277
283
|
const root = inst.root;
|
|
278
284
|
const d = myWork();
|
|
279
285
|
const head = header(inst, d);
|
|
280
|
-
|
|
286
|
+
// The PM items panel lives in the root too; a redraw of head and body must keep it.
|
|
287
|
+
if (head.parentNode !== root || inst.body.parentNode !== root) root.replaceChildren(head, inst.body, ...(inst.items ? [inst.items] : []), ...(inst.sessions ? [inst.sessions] : []));
|
|
281
288
|
const fp = JSON.stringify([d, inst.tab, inst.open, inst.details, inst.busy]);
|
|
282
289
|
if (fp === inst.last) return;
|
|
283
290
|
// Focus stays in an instruction being typed: the redraw waits until the field is left.
|
|
@@ -207,7 +207,7 @@ async function refresh(inst) {
|
|
|
207
207
|
if (seq !== inst.seq) return;
|
|
208
208
|
if (r && r.ok) { inst.rows = r.notes || []; inst.error = null; } else { inst.rows = inst.rows || []; inst.error = (r && r.error) || COPY.readError; }
|
|
209
209
|
if (!inst.open && inst.rows.length) inst.open = inst.rows[0].id;
|
|
210
|
-
if (one && one.ok && one.note && one.note.id === inst.open) inst.note = one.note;
|
|
210
|
+
if (one && one.ok && one.note && one.note.id === inst.open) { inst.note = one.note; inst.rec = one.recording || null; }
|
|
211
211
|
else if (inst.open && !one) loadOpen(inst);
|
|
212
212
|
drawLeft(inst);
|
|
213
213
|
drawRight(inst);
|
|
@@ -217,8 +217,25 @@ async function loadOpen(inst) {
|
|
|
217
217
|
const r = await pmGet('/api/notes/one?id=' + encodeURIComponent(id));
|
|
218
218
|
if (inst.open !== id) return;
|
|
219
219
|
inst.note = r && r.ok ? r.note : null;
|
|
220
|
+
inst.rec = r && r.ok ? r.recording || null : null;
|
|
220
221
|
drawRight(inst);
|
|
221
222
|
}
|
|
223
|
+
/** The replay of the note's kept audio: one <audio> per note, kept across redraws so playback goes on. Null when no audio is kept. */
|
|
224
|
+
function replayFor(inst, n) {
|
|
225
|
+
const rec = inst.rec;
|
|
226
|
+
if (!rec || n.private) { inst.audio = null; return null; }
|
|
227
|
+
const key = n.id + ':' + rec.bytes + ':' + (rec.cleaned ? 1 : 0);
|
|
228
|
+
if (inst.audio && inst.audio.key === key) return inst.audio.el;
|
|
229
|
+
if (inst.audio && !inst.audio.el.paused) return inst.audio.el; // never cut a replay that is playing
|
|
230
|
+
const a = el('audio', { controls: true, preload: 'metadata', class: 'nt-audio', 'data-ntm-audio': '', src: '/api/notes/recording?id=' + encodeURIComponent(n.id) + '&b=' + rec.bytes + (rec.cleaned ? '&c=1' : '') });
|
|
231
|
+
inst.audio = { key, el: a };
|
|
232
|
+
return a;
|
|
233
|
+
}
|
|
234
|
+
function seekReplay(inst, ms) {
|
|
235
|
+
const a = inst.audio && inst.audio.el;
|
|
236
|
+
if (!a) return;
|
|
237
|
+
try { a.currentTime = Math.max(0, ms / 1000); a.play(); } catch { /* the audio is not ready */ }
|
|
238
|
+
}
|
|
222
239
|
function choose(inst, id) {
|
|
223
240
|
if (inst.open === id) return;
|
|
224
241
|
inst.open = id; inst.note = null; inst.ctxOpen = null; inst.info = false;
|
|
@@ -259,7 +276,7 @@ function drawRight(inst, force) {
|
|
|
259
276
|
const n = inst.note;
|
|
260
277
|
const live = dock.recordState();
|
|
261
278
|
const held = !!n && live.mode !== 'idle' && live.id === n.id;
|
|
262
|
-
const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
|
|
279
|
+
const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], inst.rec && [inst.rec.bytes, inst.rec.cleaned], live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
|
|
263
280
|
// a held note draws again on every render: the clock, the lines and the paused minutes move. Nothing else draws without a change.
|
|
264
281
|
if (!force && !held && sig === inst.rightSig) return;
|
|
265
282
|
// a field being typed in is not redrawn under the cursor, except in the live block, whose fields are kept across redraws
|
|
@@ -387,6 +404,11 @@ function noteParts(inst, n, live) {
|
|
|
387
404
|
if (V.renderLive(liveHost, { state: inst.ctx.state, setState: inst.ctx.setState, FIXTURE: inst.ctx.FIXTURE }, n.id)) out.push(liveHost);
|
|
388
405
|
}
|
|
389
406
|
|
|
407
|
+
const replay = held ? null : replayFor(inst, n);
|
|
408
|
+
if (replay) out.push(el('section', { class: 'nt-pg-sec nt-pg-sec--first nt-pg-replay', 'data-ntm': 'replay' },
|
|
409
|
+
el('h3', { class: 'nt-pg-sec__h' }, el('span', null, 'Replay')), replay,
|
|
410
|
+
el('p', { class: 'nt-hint' }, 'Kept on this machine. Press a time in the discussion to play from there.')));
|
|
411
|
+
|
|
390
412
|
const cf = conflictsOf(n);
|
|
391
413
|
if (cf.length) {
|
|
392
414
|
out.push(el('section', { class: 'nt-pg-conflicts', 'data-ntm': 'conflicts' },
|
|
@@ -438,7 +460,7 @@ function noteParts(inst, n, live) {
|
|
|
438
460
|
out.push(el('details', { class: 'nt-pg-sec nt-pg-fold', 'data-ntm': 'discussion' },
|
|
439
461
|
el('summary', { class: 'nt-pg-fold__sum' }, COPY.discussion + ' · ' + bits),
|
|
440
462
|
p.topics.map((tp) => el('div', { class: 'nt-pg-topic' }, el('h4', { class: 'nt-pg-topic__h' }, tp.title), (tp.points || []).map((pt) => el('p', { class: 'nt-pg-topic__p' }, pt)))),
|
|
441
|
-
tr.length ? el('div', { class: 'nt-pg-trans' }, tr.map((x) => el('div', { class: 'nt-pg-li' }, el('span', { class: 'nt-pg-li__at' }, V.fmtTime(x.t || 0)), el('span', { class: 'nt-pg-li__text' }, x.text)))) : null));
|
|
463
|
+
tr.length ? el('div', { class: 'nt-pg-trans' }, tr.map((x) => el('div', { class: 'nt-pg-li' }, inst.audio && !held ? button({ label: V.fmtTime(x.t || 0), variant: 'ghost', size: 'sm', onClick: () => seekReplay(inst, x.t || 0), attrs: { 'data-ntm-seek': '', title: 'Play from here' } }) : el('span', { class: 'nt-pg-li__at' }, V.fmtTime(x.t || 0)), el('span', { class: 'nt-pg-li__text' }, x.text)))) : null));
|
|
442
464
|
}
|
|
443
465
|
return out.filter(Boolean);
|
|
444
466
|
}
|
|
@@ -25,6 +25,11 @@ import { wsOf } from './data.js';
|
|
|
25
25
|
import { COPY, cadenceName } from './reports-copy.js';
|
|
26
26
|
import { openIssueSheet, openNotesSheet, draftNow, printReport, copyMarkdown, clearPrint, ensurePrintCss } from './report-view.js';
|
|
27
27
|
import { openSlides, slideCards } from './report-slides.js';
|
|
28
|
+
/* The report families and the outbox load on demand: they are only drawn when the PM opens the toggle, and their module tree (the c3
|
|
29
|
+
* panels) must not weigh on the first paint of the Reports page. `lazy()` resolves both modules once. */
|
|
30
|
+
let lazyMods = null;
|
|
31
|
+
const lazy = () => lazyMods || (lazyMods = Promise.all([import('./report-families-view.js'), import('./outbox-panel.js')]).then(([f, o]) => ({ ...f, ...o })));
|
|
32
|
+
const disposeLazy = (fn, host) => { if (lazyMods) lazyMods.then((m) => m[fn](host)); };
|
|
28
33
|
|
|
29
34
|
const SHOWN = 4; // the reports listed before "Show older"
|
|
30
35
|
const insts = new WeakMap(); // host -> instance
|
|
@@ -49,6 +54,7 @@ function make(host) {
|
|
|
49
54
|
const inst = {
|
|
50
55
|
host, root, ws: null, loaded: false, error: null, reports: [], flags: [], sig: null, schedules: [], doc: null, docKey: null,
|
|
51
56
|
sel: null, showAll: false, otherOpen: false, locked: false, stop: null, seq: 0, last: '',
|
|
57
|
+
extra: null, famHost: null, outHost: null, extraWs: null,
|
|
52
58
|
};
|
|
53
59
|
return inst;
|
|
54
60
|
}
|
|
@@ -72,17 +78,56 @@ export function mountDestination(host, ctx) {
|
|
|
72
78
|
refresh(inst);
|
|
73
79
|
}
|
|
74
80
|
draw(inst);
|
|
81
|
+
mountExtras(inst, ctx);
|
|
75
82
|
return;
|
|
76
83
|
}
|
|
77
84
|
if (!inst.last) draw(inst);
|
|
85
|
+
mountExtras(inst, ctx);
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
/* Report families and the outbox sit under the page. They live in one container the page's own redraw never replaces
|
|
89
|
+
* (draw() puts it back last), so a redraw does not lose them. Mounted once per repository: the families read their sections
|
|
90
|
+
* from GET /api/report-families/facts (real initiatives and releases only), then the outbox. */
|
|
91
|
+
function mountExtras(inst, ctx) {
|
|
92
|
+
if (!inst.ws || inst.extraWs === inst.ws) return;
|
|
93
|
+
inst.extraWs = inst.ws;
|
|
94
|
+
if (!inst.extra) {
|
|
95
|
+
inst.famHost = el('div', { 'data-reports-families': '' });
|
|
96
|
+
inst.outHost = el('div', { 'data-reports-outbox': '' });
|
|
97
|
+
inst.panel = el('div', { class: 'rp-extra__panel', hidden: true }, inst.famHost, inst.outHost);
|
|
98
|
+
// Collapsed by default: the page's control budget (first-paint, pm-frame Q5) counts what a first paint shows, so the
|
|
99
|
+
// families and the outbox cost one control until the PM opens them.
|
|
100
|
+
inst.toggle = button({ label: 'Report families and outbox', variant: 'ghost', attrs: { 'data-reports-extra-toggle': '', 'aria-expanded': 'false' }, onClick: () => toggleExtras(inst, ctx) });
|
|
101
|
+
inst.extra = el('div', { class: 'rp-extra', 'data-reports-extra': '' }, inst.toggle, inst.panel);
|
|
102
|
+
}
|
|
103
|
+
keepExtra(inst);
|
|
104
|
+
// A repository change resets to collapsed, with nothing mounted: the families read their own facts when opened.
|
|
105
|
+
inst.extraOpen = false; inst.panel.hidden = true; inst.toggle.setAttribute('aria-expanded', 'false');
|
|
106
|
+
disposeLazy('disposeReportFamilies', inst.famHost); disposeLazy('disposeOutbox', inst.outHost);
|
|
107
|
+
}
|
|
108
|
+
function toggleExtras(inst, ctx) {
|
|
109
|
+
inst.extraOpen = !inst.extraOpen;
|
|
110
|
+
inst.panel.hidden = !inst.extraOpen;
|
|
111
|
+
inst.toggle.setAttribute('aria-expanded', String(inst.extraOpen));
|
|
112
|
+
if (!inst.extraOpen || !inst.ws) return;
|
|
113
|
+
const ws = inst.ws;
|
|
114
|
+
disposeLazy('disposeReportFamilies', inst.famHost);
|
|
115
|
+
pmGet('/api/report-families/facts?ws=' + encodeURIComponent(ws)).then((j) => {
|
|
116
|
+
if (inst.extraWs !== ws || !inst.extraOpen || !insts.has(inst.host)) return;
|
|
117
|
+
lazy().then((m) => { if (inst.extraWs === ws && inst.extraOpen) m.mountReportFamilies(inst.famHost, { ...(ctx || {}), reportFacts: j && j.ok ? j.facts : {} }); });
|
|
118
|
+
});
|
|
119
|
+
lazy().then((m) => { if (inst.extraOpen) m.mountOutbox(inst.outHost, {}); });
|
|
120
|
+
}
|
|
121
|
+
const keepExtra = (inst) => { if (inst.extra && inst.ws && inst.extra.parentNode !== inst.root) inst.root.appendChild(inst.extra); };
|
|
122
|
+
|
|
80
123
|
export function disposeDestination(host) {
|
|
81
124
|
const inst = insts.get(host);
|
|
82
125
|
if (!inst) return;
|
|
83
126
|
if (inst.stop) inst.stop();
|
|
84
127
|
inst.stop = null; inst.seq++;
|
|
85
128
|
clearPrint();
|
|
129
|
+
if (inst.famHost) disposeLazy('disposeReportFamilies', inst.famHost);
|
|
130
|
+
if (inst.outHost) disposeLazy('disposeOutbox', inst.outHost);
|
|
86
131
|
inst.root.remove();
|
|
87
132
|
insts.delete(host);
|
|
88
133
|
}
|
|
@@ -309,7 +354,8 @@ function goFullScreen() {
|
|
|
309
354
|
}
|
|
310
355
|
|
|
311
356
|
/** Rebuild the persistent root from the instance, if what it shows changed; keep the focus on the same control. */
|
|
312
|
-
function draw(inst) {
|
|
357
|
+
function draw(inst) { drawMain(inst); keepExtra(inst); }
|
|
358
|
+
function drawMain(inst) {
|
|
313
359
|
const root = inst.root;
|
|
314
360
|
if (!inst.ws) { root.replaceChildren(empty({ text: COPY.noRepo })); inst.last = 'none'; return; }
|
|
315
361
|
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, inst.locked]);
|
|
@@ -22,6 +22,8 @@ import { state, setState, FIXTURE } from './state.js';
|
|
|
22
22
|
import { COPY } from './today-copy.js';
|
|
23
23
|
import { todayCard, acceptCard, rankNodes, scopeCtx, ensureFeed, feedOf, feedErrorOf, bannerOf, onFeed } from './today-cards.js';
|
|
24
24
|
import { daySection, daySig, dayState, pressPrepare } from './today-day.js';
|
|
25
|
+
import { mountBriefing, disposeBriefing } from './briefing-deck.js';
|
|
26
|
+
import { mountCalendarViews, disposeCalendarViews } from './calendar-views.js';
|
|
25
27
|
|
|
26
28
|
const SHOWN = 3; // cards drawn before "Show more": the first paint holds at most 17 controls (first-paint-today.test.js)
|
|
27
29
|
let expanded = false;
|
|
@@ -205,8 +207,60 @@ export function mountToday(host, ctx) {
|
|
|
205
207
|
refocus(r.root, k);
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
/* The destination: Today's own board, plus Briefing and Calendar (console3 wave D) as two more views of the same page.
|
|
211
|
+
* Each view has its own pane in one wrapper that survives every redraw (the container-survives-redraw rule: dest-mywork.js
|
|
212
|
+
* keeps inst.items the same way). Briefing and Calendar mount once, on first open, into their pane and are disposed with the
|
|
213
|
+
* destination; Today keeps drawing into its pane exactly as before, so its content is unchanged. */
|
|
214
|
+
const VIEWS = [{ id: 'today', label: 'Today' }, { id: 'briefing', label: 'Briefing' }, { id: 'calendar', label: 'Calendar' }];
|
|
215
|
+
const dests = new WeakMap(); // host -> { wrap, bar, panes, view, mounted }
|
|
216
|
+
|
|
217
|
+
function destFor(host, ctx) {
|
|
218
|
+
let d = dests.get(host);
|
|
219
|
+
if (d) return d;
|
|
220
|
+
const panes = {};
|
|
221
|
+
for (const v of VIEWS) panes[v.id] = el('div', { class: 'td-view', 'data-today-view': v.id });
|
|
222
|
+
d = { wrap: null, bar: null, panes, view: 'today', mounted: {} };
|
|
223
|
+
const bar = tabs({
|
|
224
|
+
variant: 'segmented', role: 'radiogroup', ariaLabel: 'Today views', value: d.view,
|
|
225
|
+
items: VIEWS.map((v) => ({ id: v.id, label: v.label })),
|
|
226
|
+
onChange: (id) => { d.view = VIEWS.some((v) => v.id === id) ? id : 'today'; showView(host, d, d.ctx); },
|
|
227
|
+
attrs: { 'data-today': 'views' },
|
|
228
|
+
});
|
|
229
|
+
d.bar = bar;
|
|
230
|
+
d.wrap = el('div', { class: 'td-dest', 'data-today': 'dest' }, bar, ...VIEWS.map((v) => panes[v.id]));
|
|
231
|
+
dests.set(host, d);
|
|
232
|
+
return d;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function showView(host, d, ctx) {
|
|
236
|
+
for (const v of VIEWS) d.panes[v.id].hidden = v.id !== d.view;
|
|
237
|
+
d.bar.querySelectorAll('button[data-tab]').forEach((b) => { // the segmented control does not track its own choice
|
|
238
|
+
const on = b.getAttribute('data-tab') === d.view;
|
|
239
|
+
b.setAttribute('aria-checked', on ? 'true' : 'false'); b.setAttribute('tabindex', on ? '0' : '-1');
|
|
240
|
+
});
|
|
241
|
+
if (d.view === 'today') mountToday(d.panes.today, ctx);
|
|
242
|
+
else if (d.view === 'briefing') { d.mounted.briefing = true; mountBriefing(d.panes.briefing, ctx); }
|
|
243
|
+
else if (d.view === 'calendar') { d.mounted.calendar = true; mountCalendarViews(d.panes.calendar, ctx); }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function mountDestination(host, ctx) {
|
|
247
|
+
if (!host) return;
|
|
248
|
+
const d = destFor(host, ctx);
|
|
249
|
+
d.ctx = ctx;
|
|
250
|
+
if (!d.wrap.isConnected || d.wrap.parentNode !== host) host.replaceChildren(d.wrap);
|
|
251
|
+
showView(host, d, ctx);
|
|
252
|
+
}
|
|
209
253
|
export function disposeDestination(host) {
|
|
254
|
+
const d = dests.get(host);
|
|
255
|
+
if (d) {
|
|
256
|
+
for (const v of VIEWS) disposeToday(d.panes[v.id]);
|
|
257
|
+
if (d.mounted.briefing) disposeBriefing(d.panes.briefing);
|
|
258
|
+
if (d.mounted.calendar) disposeCalendarViews(d.panes.calendar);
|
|
259
|
+
dests.delete(host);
|
|
260
|
+
}
|
|
261
|
+
disposeToday(host);
|
|
262
|
+
}
|
|
263
|
+
function disposeToday(host) {
|
|
210
264
|
const r = roots.get(host);
|
|
211
265
|
if (r && r.off) r.off();
|
|
212
266
|
roots.delete(host);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* pm/doc-health-panel.js — console3 wave B2: the document's Health and History modes, and the Ask-the-agent diff.
|
|
2
|
+
*
|
|
3
|
+
* Health: "N of M checks" and the reason for each. A check with no input is shown as "not enough input" and is not
|
|
4
|
+
* in N or M. The result is recomputed by the server from the text on each read and is never stored here beyond a short hold.
|
|
5
|
+
* Fix with the agent: disabled until the agent session path is built. Ask the agent: the word diff of a before and an
|
|
6
|
+
* after, with Accept disabled (accept by slide needs the outbox and the slide, which are not built).
|
|
7
|
+
* History: the word diff between two revisions of the document, given as a list { id, text, label? }.
|
|
8
|
+
*/
|
|
9
|
+
import { el } from './dom.js';
|
|
10
|
+
import { setState } from './state.js';
|
|
11
|
+
import { ensureC3Css, c3ic } from './pm-work-view.js';
|
|
12
|
+
|
|
13
|
+
/* R4b: the panels are drawn on the console3 mockup's document sheet markup (console3.html docPanel, lines 4090 to 4104; the diff,
|
|
14
|
+
* lines 4075 to 4076; the revision list, line 4102): `.dpn` holds `.chips2` (the shield and "N of M checks pass"), `ul.chgl`
|
|
15
|
+
* rows (`li.ok` or `li.bad` with the check and its reason), `ul.hx` for the revisions, and `.dpn.dfi > .dsec > h4 + p` with
|
|
16
|
+
* `<ins>` and `<del>` for the diff. The styles are pm/console3.css. The document panel mounts in the document rail, which has no
|
|
17
|
+
* `.c3` root, so each panel carries `c3 c3e`. Not lifted: the mockup's Read, Edit and Relations modes (the rail has its own),
|
|
18
|
+
* the per-check "Fix with the agent" chips (one disabled button stays, there is no agent session path), the section flags,
|
|
19
|
+
* and the document graph (no relations data is served here). */
|
|
20
|
+
const wrap = (attrs, kids) => { ensureC3Css(); return el('div', { class: 'c3 c3e', ...attrs }, el('div', { class: 'dpn' }, kids)); };
|
|
21
|
+
const hcol = (h) => (h.total === 0 ? 'var(--muted-foreground)' : h.passed === h.total ? 'var(--success)' : h.total - h.passed === 1 ? 'var(--warning)' : 'var(--destructive)');
|
|
22
|
+
|
|
23
|
+
const held = new Map(); // ws|path -> { state, data, error, at }
|
|
24
|
+
|
|
25
|
+
const NAMES = { fresh: 'Fresh', decisions: 'Matches decisions', data: 'Matches data', complete: 'Complete', traced: 'Traced', readers: 'Readers have the current version' };
|
|
26
|
+
|
|
27
|
+
function readHealth(ws, rel) {
|
|
28
|
+
const k = ws + '|' + rel;
|
|
29
|
+
const cur = held.get(k);
|
|
30
|
+
if (cur && (cur.state === 'loading' || Date.now() - cur.at < 30000)) return cur;
|
|
31
|
+
const next = { state: 'loading', data: cur ? cur.data : null, at: Date.now() };
|
|
32
|
+
held.set(k, next);
|
|
33
|
+
fetch('/api/doc-health/check', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ws, path: rel }) })
|
|
34
|
+
.then((r) => r.json())
|
|
35
|
+
.then((d) => { held.set(k, d && d.ok ? { state: 'ok', data: d.health, at: Date.now() } : { state: 'error', error: (d && d.error) || 'refused', at: Date.now() }); setState({}); },
|
|
36
|
+
(e) => { held.set(k, { state: 'error', error: (e && e.message) || 'could not reach the workspace server', at: Date.now() }); setState({}); });
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the Health mode shows now, for the rail's change key. */
|
|
41
|
+
export function healthStamp(ws, rel) { const g = held.get(ws + '|' + rel); return g ? [g.state, g.error || null, g.data || null] : null; }
|
|
42
|
+
|
|
43
|
+
export function refreshHealth(ws, rel) { held.delete(ws + '|' + rel); }
|
|
44
|
+
|
|
45
|
+
/** The pure view of one healthOf answer. */
|
|
46
|
+
export function healthView(h) {
|
|
47
|
+
const note = h.checks.filter((c) => c.pass === null).length;
|
|
48
|
+
return wrap({ 'data-rail': 'health' }, [
|
|
49
|
+
el('div', { class: 'chips2', 'data-health': 'count' }, [el('span', { class: 'ki', style: 'color:' + hcol(h) }, [c3ic('shield')]), el('b', null, `${h.passed} of ${h.total} checks pass`)]),
|
|
50
|
+
note ? el('p', { class: 'dim', style: 'font-size:var(--fs-2);margin:0', 'data-health': 'unanswered' }, `${note} check${note === 1 ? '' : 's'} not counted: not enough input.`) : null,
|
|
51
|
+
el('ul', { class: 'chgl' }, h.checks.map((c) => el('li', { class: c.pass === null ? '' : c.pass ? 'ok' : 'bad', 'data-check': c.id, 'data-pass': c.pass === null ? 'none' : String(c.pass) }, [
|
|
52
|
+
c3ic(c.pass === null ? 'info' : c.pass ? 'check' : 'alert', 'sm'),
|
|
53
|
+
el('span', null, (NAMES[c.id] || c.id) + ': ' + c.reason),
|
|
54
|
+
el('span', { class: 'tag' }, c.pass === null ? 'no input' : c.pass ? 'pass' : 'fail'),
|
|
55
|
+
]))),
|
|
56
|
+
el('div', { class: 'chips2' }, [el('button', { type: 'button', class: 'pill', disabled: true, 'aria-disabled': 'true', 'data-health': 'fix',
|
|
57
|
+
title: 'Needs the agent session path, which is not built yet.' }, [c3ic('spark', 'sm'), 'Fix with the agent'])]),
|
|
58
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Health answers one question: can I trust this document without rereading it? Checks are recomputed from the text every time it changes.'),
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The Health mode for one document. */
|
|
63
|
+
export function healthPanel(o) {
|
|
64
|
+
const g = readHealth(o.ws, o.rel);
|
|
65
|
+
if (g.state === 'loading' && !g.data) return wrap({ 'data-rail': 'status' }, [el('div', { class: 'fold a' }, [el('span', { class: 'c3spin' }, [c3ic('spark', 'sm')]), 'Checking this document…'])]);
|
|
66
|
+
if (g.state === 'error') return wrap({ 'data-rail': 'status' }, [el('div', { class: 'fold w' }, [c3ic('alert', 'sm'), 'The checks could not run: ' + g.error + '. Nothing is shown.'])]);
|
|
67
|
+
return healthView(g.data);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const diffNode = (diff) => el('div', { class: 'dpn dfi', 'data-rail': 'diff' }, el('div', { class: 'dsec' + (diff.some((d) => d.op !== 'same') ? ' warn' : '') }, el('p', null, diff.map((d, i) => {
|
|
71
|
+
const tag = d.op === 'add' ? 'ins' : d.op === 'del' ? 'del' : 'span';
|
|
72
|
+
return [i ? ' ' : '', el(tag, { 'data-op': d.op }, d.text)];
|
|
73
|
+
}).flat())));
|
|
74
|
+
|
|
75
|
+
// The same plain LCS as scripts/doc-health.js, kept here because the page cannot load a CommonJS file.
|
|
76
|
+
export function wordDiff(before, after) {
|
|
77
|
+
const a = String(before || '').split(/\s+/).filter(Boolean), b = String(after || '').split(/\s+/).filter(Boolean);
|
|
78
|
+
const L = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
79
|
+
for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--) L[i][j] = a[i] === b[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
|
|
80
|
+
const out = [];
|
|
81
|
+
const push = (op, text) => { const l = out[out.length - 1]; if (l && l.op === op) l.text += ' ' + text; else out.push({ op, text }); };
|
|
82
|
+
let i = 0, j = 0;
|
|
83
|
+
while (i < a.length && j < b.length) {
|
|
84
|
+
if (a[i] === b[j]) { push('same', a[i]); i++; j++; } else if (L[i + 1][j] >= L[i][j + 1]) push('del', a[i++]); else push('add', b[j++]);
|
|
85
|
+
}
|
|
86
|
+
while (i < a.length) push('del', a[i++]);
|
|
87
|
+
while (j < b.length) push('add', b[j++]);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** History mode: compare the newest revision with the one before it (or the pair the PM picked in `o.pick`). */
|
|
92
|
+
export function historyPanel(o) {
|
|
93
|
+
const revs = Array.isArray(o.revisions) ? o.revisions : [];
|
|
94
|
+
if (revs.length < 2) return wrap({ 'data-rail': 'history' }, [el('div', { class: 'fold' }, [c3ic('info', 'sm'), 'Fewer than two revisions are known for this document, so there is nothing to compare.'])]);
|
|
95
|
+
const [from, to] = o.pick || [revs[revs.length - 2].id, revs[revs.length - 1].id];
|
|
96
|
+
const a = revs.find((r) => r.id === from), b = revs.find((r) => r.id === to);
|
|
97
|
+
if (!a || !b) return wrap({ 'data-rail': 'history' }, [el('div', { class: 'fold w' }, [c3ic('alert', 'sm'), 'That revision is not in the list.'])]);
|
|
98
|
+
return wrap({ 'data-rail': 'history' }, [
|
|
99
|
+
el('ul', { class: 'hx' }, revs.slice().reverse().map((r) => el('li', null, [el('span', { class: 'chip2' }, [c3ic('doc', 'sm'), r.id]), el('span', null, [el('b', null, r.label || r.id)]), el('span', { class: 'w' }, r.id === to ? 'newest compared' : r.id === from ? 'compared with' : '')]))),
|
|
100
|
+
el('h4', { style: 'margin:0;font-size:var(--fs-3)' }, `${a.label || a.id} to ${b.label || b.id}`),
|
|
101
|
+
diffNode(wordDiff(a.text, b.text)),
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Ask the agent: the word diff of a supplied before and after. Accept needs the outbox and the slide. */
|
|
106
|
+
export function askPanel({ before, after }) {
|
|
107
|
+
return wrap({ 'data-rail': 'ask' }, [diffNode(wordDiff(before, after)),
|
|
108
|
+
el('div', { class: 'chips2' }, [el('button', { type: 'button', class: 'pill', disabled: true, 'aria-disabled': 'true', 'data-ask': 'accept',
|
|
109
|
+
title: 'Accept by slide needs the outbox and the slide, which are not built yet.' }, [c3ic('check', 'sm'), 'Accept'])])]);
|
|
110
|
+
}
|
|
@@ -14,11 +14,12 @@ import { el, setChildren } from './dom.js';
|
|
|
14
14
|
import { tabs, row } from './primitives.js';
|
|
15
15
|
import { setState } from './state.js';
|
|
16
16
|
import { copiesOf, wordsOf, openCopy } from './doc-remote.js';
|
|
17
|
+
import { healthPanel, historyPanel, healthStamp } from './doc-health-panel.js';
|
|
17
18
|
|
|
18
19
|
const SVG = 'http://www.w3.org/2000/svg';
|
|
19
20
|
const graphs = new Map(); // ws|path -> { state, data, at }
|
|
20
21
|
const rails = new Map(); // tab id -> { node, key }
|
|
21
|
-
const modes = new Map(); // tab id -> 'outline' | 'graph'
|
|
22
|
+
const modes = new Map(); // tab id -> 'outline' | 'graph' | 'health' | 'history'
|
|
22
23
|
|
|
23
24
|
const VIA = { link: 'Link', frontmatter: 'Frontmatter', initiative: 'Initiative', anchor: 'Anchor', cites: 'Decision',
|
|
24
25
|
supersedes: 'Supersedes', amended_by: 'Amended by' };
|
|
@@ -51,7 +52,7 @@ export function railNode(tab, o) {
|
|
|
51
52
|
const ties = g.state === 'ok' && g.data ? g.data.ties || [] : [];
|
|
52
53
|
const flagged = new Set(o.findings.map((f) => f.rel).filter(Boolean));
|
|
53
54
|
const mode = modes.get(tab.id) || (o.heads.length > 1 ? 'outline' : 'graph');
|
|
54
|
-
const key = JSON.stringify([mode, o.heads, g.state, ties, copies.map((c) => [c.kind, wordsOf(c, o.ws, o.rel).text]), o.findings]);
|
|
55
|
+
const key = JSON.stringify([mode, mode === 'health' ? healthStamp(o.ws, o.rel) : 0, o.revisions, o.heads, g.state, ties, copies.map((c) => [c.kind, wordsOf(c, o.ws, o.rel).text]), o.findings]);
|
|
55
56
|
let r = rails.get(tab.id);
|
|
56
57
|
if (!r) { r = { node: el('aside', { class: 'fr-rail', 'data-rail': '' }), key: null }; rails.set(tab.id, r); }
|
|
57
58
|
if (r.key === key) return r.node;
|
|
@@ -59,7 +60,8 @@ export function railNode(tab, o) {
|
|
|
59
60
|
const n = ties.length + copies.length;
|
|
60
61
|
const kids = [tabs({
|
|
61
62
|
variant: 'segmented', ariaLabel: 'Beside the document', value: mode, full: true,
|
|
62
|
-
items: [{ id: 'outline', label: 'Outline' }, { id: 'graph', label: n ? 'Connections ' + n : 'Connections' }
|
|
63
|
+
items: [{ id: 'outline', label: 'Outline' }, { id: 'graph', label: n ? 'Connections ' + n : 'Connections' },
|
|
64
|
+
{ id: 'health', label: 'Health' }, { id: 'history', label: 'History' }],
|
|
63
65
|
onChange: (m) => { modes.set(tab.id, m); setState({}); },
|
|
64
66
|
attrs: { class: 'fr-rail__tabs', 'data-rail': 'tabs' },
|
|
65
67
|
})];
|
|
@@ -69,6 +71,10 @@ export function railNode(tab, o) {
|
|
|
69
71
|
o.heads.map((h) => row({ title: h.text, compact: true, onOpen: () => o.onGoto(h.i), openLabel: 'Go to ' + h.text,
|
|
70
72
|
attrs: { class: 'fr-rail__head', 'data-level': String(h.level), 'data-rail': 'head' } })))
|
|
71
73
|
: el('p', { class: 'fr-rail__note' }, 'No headings yet. A line that starts with # or ## is a heading.'));
|
|
74
|
+
} else if (mode === 'health') {
|
|
75
|
+
kids.push(healthPanel(o));
|
|
76
|
+
} else if (mode === 'history') {
|
|
77
|
+
kids.push(historyPanel(o));
|
|
72
78
|
} else if (g.state === 'loading' && !g.data) {
|
|
73
79
|
kids.push(el('p', { class: 'fr-rail__note', 'data-rail': 'status' }, 'Reading the ties of this file…'));
|
|
74
80
|
} else if (g.state === 'error') {
|