create-pmos 0.86.0 → 0.88.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 +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/board.md +4 -2
- package/workspace/guide/docs.md +4 -2
- package/workspace/guide/layout.md +1 -1
- package/workspace/guide/people.md +7 -5
- package/workspace/guide/today.md +2 -0
- package/workspace/lib/session-origin.js +13 -7
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/bd-board.js +62 -16
- package/workspace/public/pm/board-copy.js +2 -0
- package/workspace/public/pm/components.css +402 -88
- package/workspace/public/pm/dest-board.js +15 -9
- package/workspace/public/pm/dest-docs.js +46 -21
- package/workspace/public/pm/dest-people.js +114 -76
- package/workspace/public/pm/dock-sessions.js +3 -2
- package/workspace/public/pm/mobile.css +1 -1
- package/workspace/public/pm/primitives.js +3 -3
- package/workspace/public/pm/today-copy.js +3 -1
- package/workspace/public/pm/today-day.js +19 -15
- package/workspace/public/pm/topbar.js +1 -1
- package/workspace/public/vendor/lucide-sprite.js +8 -2
- package/workspace/server.js +1 -1
- package/workspace/third-party/README.md +1 -1
|
@@ -29,8 +29,8 @@ import { currentProduct } from './board-read.js';
|
|
|
29
29
|
import { pmGet } from './http.js';
|
|
30
30
|
import { postEvents } from './breakdown-api.js';
|
|
31
31
|
import { watchSig } from './sig-watch.js';
|
|
32
|
-
import { ensureComponentsCss, layer, tabs, field, button, sheet, toast, empty, menu } from './primitives.js';
|
|
33
|
-
import { ensureSprite } from './icons.js';
|
|
32
|
+
import { ensureComponentsCss, layer, tabs, field, button, sheet, toast, empty, menu, badge } from './primitives.js';
|
|
33
|
+
import { ensureSprite, icon } from './icons.js';
|
|
34
34
|
import { COPY as BD } from './breakdown-copy.js';
|
|
35
35
|
import { COPY } from './board-copy.js';
|
|
36
36
|
import { hold, cancel, isHeld, flush, overlay, anyHeld } from './undo-hold.js';
|
|
@@ -119,6 +119,9 @@ function focusKeyIn(P) {
|
|
|
119
119
|
function headOf(P) {
|
|
120
120
|
const out = P.out;
|
|
121
121
|
const inits = (out && out.initiatives) || [];
|
|
122
|
+
const n = out ? (out.counts.todo + out.counts.doing + out.counts.review + out.counts.done) : 0;
|
|
123
|
+
const productName = currentProduct();
|
|
124
|
+
|
|
122
125
|
const f = tabs({
|
|
123
126
|
variant: 'segmented', ariaLabel: COPY.filter, value: P.filter === 'init' ? null : P.filter,
|
|
124
127
|
items: [{ id: 'active', label: COPY.filters.active }, { id: 'mine', label: COPY.filters.mine }],
|
|
@@ -134,10 +137,13 @@ function headOf(P) {
|
|
|
134
137
|
label: COPY.addItem, variant: 'primary', icon: 'plus', onClick: () => P.actions.openAdd(),
|
|
135
138
|
attrs: { 'data-bo-add': '', 'data-fk': 'add' },
|
|
136
139
|
});
|
|
137
|
-
|
|
140
|
+
|
|
141
|
+
const title = el('h2', { class: 'td-count', 'data-bo-count': String(n) }, COPY.title(n, productName));
|
|
142
|
+
|
|
138
143
|
return [
|
|
139
|
-
el('div', { class: '
|
|
140
|
-
|
|
144
|
+
el('div', { class: 'td-head' },
|
|
145
|
+
title,
|
|
146
|
+
el('div', { class: 'bo-head__end' }, f, el('div', { class: 'bo-head__pick' }, pick), add)),
|
|
141
147
|
];
|
|
142
148
|
}
|
|
143
149
|
|
|
@@ -148,14 +154,14 @@ function render(P) {
|
|
|
148
154
|
P.drags.destroy();
|
|
149
155
|
const L = view(P);
|
|
150
156
|
P.wide = (P.root.getBoundingClientRect().width || P.lastWidth || 0) >= WIDE_BOARD;
|
|
151
|
-
//
|
|
152
|
-
P.cardMenus =
|
|
157
|
+
// Always render menu buttons; they will be hidden with CSS opacity until hover
|
|
158
|
+
P.cardMenus = true;
|
|
153
159
|
setChildren(P.headEl, headOf(P));
|
|
154
160
|
let kids;
|
|
155
161
|
if (!P.loaded) kids = [el('div', { class: 'bd-note' }, COPY.reading)];
|
|
156
162
|
else if (!L) kids = [el('div', { class: 'bd-note bd-note--error', role: 'alert' }, COPY.readFailed(P.error || ''))];
|
|
157
163
|
else {
|
|
158
|
-
kids = renderBoard({ P, L, A: P.actions, wide: P.wide, cardMenus:
|
|
164
|
+
kids = renderBoard({ P, L, A: P.actions, wide: P.wide, cardMenus: true, agenda: true });
|
|
159
165
|
const rel = (P.out && P.out.releases) || [];
|
|
160
166
|
if (rel.length) {
|
|
161
167
|
grantScopes(() => bump(P));
|
|
@@ -381,7 +387,7 @@ function createPane(host, ws) {
|
|
|
381
387
|
const P = {
|
|
382
388
|
host, ws, root, headEl, bodyEl, slug: 'board', filter: 'active', init: null, out: null, raw: null, older: 0, error: null, loaded: false,
|
|
383
389
|
loading: false, again: false, boardCol: 'todo', ver: 0, renderedVer: -1, drags: dragSet(), dragging: false, rendering: false,
|
|
384
|
-
disposed: false, sheet: null, wide: false, cardMenus: true, lastWidth: 0,
|
|
390
|
+
disposed: false, sheet: null, wide: false, cardMenus: true, lastWidth: 0, expandedCols: new Set(),
|
|
385
391
|
coarse: typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches,
|
|
386
392
|
};
|
|
387
393
|
P.actions = makeActions(P);
|
|
@@ -178,7 +178,7 @@ export function mountDestination(host, ctx) {
|
|
|
178
178
|
const ws = repo ? wsOf(repo) : null;
|
|
179
179
|
if (ws !== inst.ws) {
|
|
180
180
|
stopPoll(inst);
|
|
181
|
-
Object.assign(inst, { ws, loaded: false, error: null, view: null, last: '', open: null, adding: null, files: [], lib: null, libError: null, graph: null });
|
|
181
|
+
Object.assign(inst, { ws, repo, loaded: false, error: null, view: null, last: '', open: null, adding: null, files: [], lib: null, libError: null, graph: null });
|
|
182
182
|
inst.openCats.clear(); inst.full.clear();
|
|
183
183
|
if (ws) { ensureSprite(); refresh(inst); }
|
|
184
184
|
draw(inst);
|
|
@@ -362,7 +362,7 @@ function fileLine(inst, f, q = '') {
|
|
|
362
362
|
title: marked(f.title, q),
|
|
363
363
|
meta: [ini ? el('span', { class: 'rp-docs-ini', title: ini.title }, marked(ini.short, q)) : el('span', { class: 'rp-docs-path' }, marked(f.path, q)), day(f.date)],
|
|
364
364
|
lead: [el('span', { class: 'rp-docs-dot rp-docs-dot--none', 'aria-hidden': 'true' })],
|
|
365
|
-
trail: adding ? [] : [button({ label: COPY.addCopy, variant: 'outline', size: 'sm', attrs: { class: 'rp-docs-addcopy', 'data-docs-act': 'add-copy', 'data-docs-key': f.path }, onClick: start })],
|
|
365
|
+
trail: adding ? [] : [button({ label: COPY.addCopy, variant: 'outline', size: 'sm', icon: 'plus', attrs: { class: 'rp-docs-addcopy', 'data-docs-act': 'add-copy', 'data-docs-key': f.path }, onClick: start })],
|
|
366
366
|
onOpen: inst.lib ? toggle : null,
|
|
367
367
|
openLabel: `${f.title}: show the files it links to`,
|
|
368
368
|
openAttrs: { 'aria-expanded': open ? 'true' : 'false', title: f.path, 'data-docs-file-toggle': f.path },
|
|
@@ -388,17 +388,48 @@ function fileLine(inst, f, q = '') {
|
|
|
388
388
|
/* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
389
389
|
|
|
390
390
|
function header(inst) {
|
|
391
|
-
if (inst.head)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
391
|
+
if (!inst.head) {
|
|
392
|
+
const find = field({ label: COPY.find, hideLabel: true, placeholder: COPY.find, attrs: { 'data-docs-find': '' } });
|
|
393
|
+
const q = find.control;
|
|
394
|
+
inst.findControl = q;
|
|
395
|
+
q.addEventListener('input', () => { inst.q = q.value; inst.open = null; inst.adding = null; inst.graph = null; inst.full.clear(); searchFiles(inst); draw(inst); });
|
|
396
|
+
// The field restores its last committed text on Escape; here Escape clears the search instead.
|
|
397
|
+
q.addEventListener('keydown', (e) => { if (e.key === 'Escape') { q.resetCommitted(''); inst.q = ''; inst.graph = null; searchFiles(inst); draw(inst); } });
|
|
398
|
+
inst.titleEl = el('h2', { class: 'rp-head__title' }, COPY.title);
|
|
399
|
+
inst.tabsSlot = el('div', { class: 'rp-docs-views' });
|
|
400
|
+
inst.whenEl = el('span', { class: 'rp-docs-when', 'data-docs-summary': '' });
|
|
401
|
+
inst.compareBtn = button({ label: COPY.compareAll, variant: 'primary', size: 'sm', icon: 'refresh-cw', attrs: { 'data-docs-act': 'check' }, onClick: () => compareAll(inst) });
|
|
402
|
+
inst.head = el('div', { class: 'rp-docs-head' },
|
|
403
|
+
el('div', { class: 'rp-docs-top' }, inst.titleEl, inst.tabsSlot),
|
|
404
|
+
el('div', { class: 'rp-docs-bar' }, inst.whenEl, el('div', { class: 'rp-docs-find' }, find), inst.compareBtn));
|
|
405
|
+
}
|
|
399
406
|
return inst.head;
|
|
400
407
|
}
|
|
401
408
|
|
|
409
|
+
/** The parts of the header that follow the data: the title, the view tabs with their counts, when the copies were compared. */
|
|
410
|
+
function updateHead(inst) {
|
|
411
|
+
const v = inst.view;
|
|
412
|
+
const lib = inst.lib;
|
|
413
|
+
const all = v ? v.docs.reduce((n, d) => n + d.links.length, 0) : 0;
|
|
414
|
+
const needs = v ? v.docs.reduce((n, d) => n + d.links.filter((l) => needsYou(inst, d, l)).length, 0) : 0;
|
|
415
|
+
const running = !!v && (!!(v.open || v.session) || inst.busy);
|
|
416
|
+
const repoName = inst.repo ? String(inst.repo).split('/').pop() : '';
|
|
417
|
+
inst.titleEl.textContent = needs > 0 ? COPY.needYou(needs) + (repoName ? ` in ${repoName}` : '') : COPY.title;
|
|
418
|
+
inst.tabsSlot.replaceChildren(tabs({ variant: 'segmented', items: [{ id: 'needs', label: COPY.needs, count: needs }, { id: 'copies', label: COPY.copies, count: all },
|
|
419
|
+
{ id: 'files', label: COPY.files, count: lib ? lib.files.length.toLocaleString('en-US') : null }],
|
|
420
|
+
value: inst.filter, ariaLabel: 'Filter', attrs: { 'data-docs-filter': '' }, onChange: (val) => { inst.filter = val; inst.graph = null; draw(inst); } }));
|
|
421
|
+
inst.whenEl.replaceChildren(icon('refresh-cw'), el('span', null, !v ? '' : running ? COPY.comparing : (v.last ? COPY.compared(v.last.finished_at) : COPY.never)));
|
|
422
|
+
inst.compareBtn.disabled = running || !all;
|
|
423
|
+
inst.compareBtn.setAttribute('aria-label', COPY.compareAll);
|
|
424
|
+
inst.compareBtn.title = COPY.compareAll;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** The body inside one card with a small label, like an Agenda panel. */
|
|
428
|
+
function panel(inst, nodes) {
|
|
429
|
+
const label = inst.q.trim() ? 'Results' : inst.filter === 'files' ? COPY.files : inst.filter === 'copies' ? COPY.copies : COPY.needs;
|
|
430
|
+
return el('section', { class: 'rp-docs-panel' }, el('div', { class: 'rp-docs-panel__eyebrow' }, label), ...nodes);
|
|
431
|
+
}
|
|
432
|
+
|
|
402
433
|
const needsYou = (inst, d, l) => l.signal !== 'same' || !!l.syncing || inst.pending.has(keyOf(d, l));
|
|
403
434
|
|
|
404
435
|
/**
|
|
@@ -467,14 +498,7 @@ function body(inst) {
|
|
|
467
498
|
const all = v.docs.reduce((n, d) => n + d.links.length, 0);
|
|
468
499
|
const needs = v.docs.reduce((n, d) => n + d.links.filter((l) => needsYou(inst, d, l)).length, 0);
|
|
469
500
|
const running = !!(v.open || v.session) || inst.busy;
|
|
470
|
-
const out = [
|
|
471
|
-
tabs({ variant: 'line', items: [{ id: 'needs', label: COPY.needs, count: needs }, { id: 'copies', label: COPY.copies, count: all },
|
|
472
|
-
{ id: 'files', label: COPY.files, count: lib ? lib.files.length.toLocaleString('en-US') : null }],
|
|
473
|
-
value: inst.filter, ariaLabel: 'Filter', attrs: { 'data-docs-filter': '' }, onChange: (val) => { inst.filter = val; inst.graph = null; draw(inst); } }),
|
|
474
|
-
el('span', { class: 'rp-docs-when', 'data-docs-summary': '' },
|
|
475
|
-
running ? COPY.comparing : (v.last ? COPY.compared(v.last.finished_at) : COPY.never),
|
|
476
|
-
button({ label: '↻', variant: 'ghost', size: 'sm', disabled: running || !all,
|
|
477
|
-
attrs: { 'data-docs-act': 'check', 'aria-label': COPY.compareAll, title: COPY.compareAll }, onClick: () => compareAll(inst) })))];
|
|
501
|
+
const out = [];
|
|
478
502
|
if (inst.filter === 'files') {
|
|
479
503
|
if (!lib) { out.push(el('p', { class: 'rp-section__empty' + (inst.libError ? ' rp-section__empty--bad' : '') }, inst.libError || COPY.readingFiles)); return out; }
|
|
480
504
|
out.push(el('div', { class: 'rp-docs-folds' }, byCategory(allEntries(inst)).map(([k, es]) => section(inst, k, es, { foldable: true }))));
|
|
@@ -498,8 +522,9 @@ function draw(inst) {
|
|
|
498
522
|
if (fp === inst.last && root.childNodes.length && root.contains(inst.body)) return;
|
|
499
523
|
inst.last = fp;
|
|
500
524
|
const head = header(inst);
|
|
525
|
+
updateHead(inst);
|
|
501
526
|
if (head.parentNode !== root || inst.body.parentNode !== root) root.replaceChildren(head, inst.body);
|
|
502
|
-
if (!inst.loaded) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty' }, COPY.loading)); return; }
|
|
503
|
-
if (inst.error) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, inst.error)); return; }
|
|
504
|
-
inst.body.replaceChildren(
|
|
527
|
+
if (!inst.loaded) { inst.body.replaceChildren(panel(inst, [el('p', { class: 'rp-section__empty' }, COPY.loading)])); return; }
|
|
528
|
+
if (inst.error) { inst.body.replaceChildren(panel(inst, [el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, inst.error)])); return; }
|
|
529
|
+
inst.body.replaceChildren(panel(inst, body(inst)));
|
|
505
530
|
}
|
|
@@ -2,24 +2,22 @@
|
|
|
2
2
|
* tone, notes and shared work (initiative people-directory, PRD R5 to R11, proposed D90).
|
|
3
3
|
*
|
|
4
4
|
* A DESTINATION MODULE (registry.js): mountDestination(host, ctx) on every render with the same host, disposeDestination(host).
|
|
5
|
-
* It keeps ONE persistent root per host; the header (title
|
|
6
|
-
*
|
|
5
|
+
* It keeps ONE persistent root per host; the header is built once (h2 title on left, group switch on right), so typing never
|
|
6
|
+
* loses focus. The search field lives in the left card and is built once. Only the body is rebuilt when its own data changed.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the workspace's own records hold, and agent sessions record what they saw through `pmos people record`. A link from another place opens a person through the tab's slug (people-link.js).
|
|
17
|
-
* In fixture mode it reads nothing.
|
|
8
|
+
* Layout (desktop): header row, "Waiting for you" merge candidates as bordered rows, then two cards side-by-side (minmax(300px,2fr) 3fr,
|
|
9
|
+
* gap 16px, fixed height calc(100vh - 220px) min-height 420px, internal scrolling). Left card: "PEOPLE" label + count, search field,
|
|
10
|
+
* full-width sort tabs, list of people. Right card: person detail with avatar, name, meta, Confirm / ⋯ menu, 3 tiles (Interactions,
|
|
11
|
+
* Tone, Shared work), scrollable sections (Interactions, Shared work, Notes, Identity and sources).
|
|
12
|
+
*
|
|
13
|
+
* What it shows: waiting for you (possible merge candidates), the people list (search, filter, sort), and person's page
|
|
14
|
+
* (who, interactions, tone, shared work, notes, identity). Every press is a page press to POST /api/people/decide.
|
|
15
|
+
* A link from another place opens a person through the tab's slug (people-link.js). In fixture mode it reads nothing.
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
18
|
import { el } from './dom.js';
|
|
21
|
-
import { button, row, field, toast, empty, tabs, badge, menu, ensureComponentsCss } from './primitives.js';
|
|
22
|
-
import { ensureSprite } from './icons.js';
|
|
19
|
+
import { button, iconButton, row, field, toast, empty, tabs, badge, menu, ensureComponentsCss } from './primitives.js';
|
|
20
|
+
import { ensureSprite, icon } from './icons.js';
|
|
23
21
|
import { pmGet, pmPost } from './http.js';
|
|
24
22
|
import { wsOf } from './data.js';
|
|
25
23
|
import { registerCommand, openDestination } from './registry.js';
|
|
@@ -174,22 +172,18 @@ function review(inst) {
|
|
|
174
172
|
const rows = [];
|
|
175
173
|
for (const m of r.merges) {
|
|
176
174
|
const key = m.handle.kind + ':' + m.handle.value;
|
|
177
|
-
rows.push(el('div', { class: 'rp-people-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
],
|
|
187
|
-
})));
|
|
175
|
+
rows.push(el('div', { class: 'rp-people-merge', role: 'listitem', 'data-people-merge': key },
|
|
176
|
+
el('div', { class: 'rp-people-merge__dot' }),
|
|
177
|
+
el('div', { class: 'rp-people-merge__text' },
|
|
178
|
+
el('div', { class: 'rp-people-merge__title' }, COPY.same(m.handle.value, m.personName)),
|
|
179
|
+
el('div', { class: 'rp-people-merge__why' }, m.why)),
|
|
180
|
+
button({ label: COPY.merge, icon: 'git-merge', variant: 'outline', size: 'sm', attrs: { 'data-people-act': 'merge', 'data-people-key': key },
|
|
181
|
+
onClick: () => decide(inst, { act: 'merge', person: m.person, handle: m.handle }, COPY.merged) }),
|
|
182
|
+
button({ label: COPY.keep, variant: 'ghost', size: 'sm', attrs: { 'data-people-act': 'keep-separate', 'data-people-key': key },
|
|
183
|
+
onClick: () => decide(inst, { act: 'keep-separate', a: m.a, b: m.b }, COPY.kept) })));
|
|
188
184
|
}
|
|
189
185
|
if (r.tones.length) rows.push(el('p', { class: 'rp-people-hint', 'data-people-tones': String(r.tones.length) }, COPY.tonesWait(r.tones.length)));
|
|
190
|
-
return el('
|
|
191
|
-
el('h3', { class: 'rp-people-sec__title' }, `${COPY.waiting} · ${r.count}`),
|
|
192
|
-
el('div', { role: 'list' }, rows));
|
|
186
|
+
return el('div', { class: 'rp-people-review', 'data-people-review': '' }, ...rows);
|
|
193
187
|
}
|
|
194
188
|
|
|
195
189
|
/* ── the list ────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -212,11 +206,15 @@ function shownPeople(inst) {
|
|
|
212
206
|
function personLine(inst, p) {
|
|
213
207
|
const open = inst.open === p.id;
|
|
214
208
|
const t = toneOf(p);
|
|
215
|
-
|
|
209
|
+
const toneClass = t === 'tense' || t === 'cautious' || t === 'warm' ? t : '';
|
|
210
|
+
const initials = p.name.split(/\s+/).map((w) => w[0]).join('').toUpperCase();
|
|
211
|
+
return el('div', { class: 'rp-people-item', role: 'listitem', 'data-people-person': p.id },
|
|
216
212
|
row({
|
|
217
213
|
title: p.name,
|
|
218
|
-
meta: [who(p), p.
|
|
219
|
-
|
|
214
|
+
meta: [[who(p), p.external ? COPY.external : null].filter(Boolean).join(' · ')],
|
|
215
|
+
lead: [el('span', { class: 'rp-people-row__avatar' }, initials)],
|
|
216
|
+
trail: t ? [el('span', { class: 'rp-people-row__tone' + (p.tone.confirmed ? '' : ' rp-people-row__tone--proposed'), 'data-people-tone': t },
|
|
217
|
+
el('i', { class: 'rp-people-row__tone-dot ' + toneClass }), p.tone.confirmed ? t : t + '?')] : [],
|
|
220
218
|
onOpen: () => { inst.open = p.id; draw(inst); },
|
|
221
219
|
openLabel: `${p.name}, ${who(p)}`,
|
|
222
220
|
openAttrs: { 'aria-current': open ? 'true' : null, 'data-people-open': p.id },
|
|
@@ -226,20 +224,19 @@ function personLine(inst, p) {
|
|
|
226
224
|
function listPane(inst) {
|
|
227
225
|
const v = inst.view;
|
|
228
226
|
const all = v.people.filter((p) => matches(p, inst.q.trim().toLowerCase()));
|
|
229
|
-
const controls = el('div', { class: 'rp-people-views' },
|
|
230
|
-
tabs({ variant: 'line', value: inst.group, ariaLabel: 'Group', attrs: { 'data-people-group': '' },
|
|
231
|
-
items: [{ id: 'all', label: COPY.all, count: all.length }, { id: 'team', label: COPY.team, count: all.filter((p) => !p.external).length },
|
|
232
|
-
{ id: 'external', label: COPY.external, count: all.filter((p) => p.external).length }],
|
|
233
|
-
onChange: (val) => { inst.group = val; draw(inst); } }),
|
|
234
|
-
tabs({ variant: 'segmented', value: inst.sort, ariaLabel: 'Sort', attrs: { 'data-people-sort': '' },
|
|
235
|
-
items: [{ id: 'latest', label: COPY.latest }, { id: 'most', label: COPY.most }, { id: 'name', label: COPY.name }],
|
|
236
|
-
onChange: (val) => { inst.sort = val; draw(inst); } }));
|
|
237
227
|
const list = shownPeople(inst);
|
|
238
228
|
let rows;
|
|
239
229
|
if (!v.people.length) rows = [el('p', { class: 'rp-section__empty' }, COPY.none)];
|
|
240
230
|
else if (!list.length) rows = [el('p', { class: 'rp-section__empty' }, COPY.noMatch(inst.q.trim()))];
|
|
241
231
|
else rows = [el('div', { role: 'list', 'data-people-list': '' }, list.map((p) => personLine(inst, p)))];
|
|
242
|
-
return el('div', { class: 'rp-people-
|
|
232
|
+
return el('div', { class: 'rp-people-card' },
|
|
233
|
+
el('div', { class: 'rp-people-card__header' },
|
|
234
|
+
el('p', { class: 'rp-people-card__label' }, COPY.title, el('span', { class: 'rp-people-card__count' }, all.length))),
|
|
235
|
+
inst.head.find,
|
|
236
|
+
tabs({ variant: 'segmented', full: true, value: inst.sort, ariaLabel: 'Sort', attrs: { 'data-people-sort': '' },
|
|
237
|
+
items: [{ id: 'latest', label: COPY.latest }, { id: 'most', label: COPY.most }, { id: 'name', label: COPY.name }],
|
|
238
|
+
onChange: (val) => { inst.sort = val; draw(inst); } }),
|
|
239
|
+
el('div', { class: 'rp-people-card__list' }, ...rows));
|
|
243
240
|
}
|
|
244
241
|
|
|
245
242
|
/* ── the person's page ───────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -250,11 +247,16 @@ function section(title, kids, attrs) {
|
|
|
250
247
|
|
|
251
248
|
function interactionsSec(p) {
|
|
252
249
|
if (!p.interactions.length) return section(COPY.interactions, [el('p', { class: 'rp-people-hint' }, COPY.noInteractions)], { 'data-people-sec': 'interactions' });
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
el('span', { class: 'rp-people-
|
|
250
|
+
const icons = { mail: 'mail', live: 'message-square', calendar: 'calendar', jira: 'ticket', gather: 'file-text', plan: 'file-text', notes: 'file-text' };
|
|
251
|
+
return section(`${COPY.interactions} · ${p.count}`, [el('div', { class: 'rp-people-ints' }, p.interactions.slice(0, 50).map((i) => {
|
|
252
|
+
const iconName = icons[i.source] || 'file-text';
|
|
253
|
+
return el('div', { class: 'rp-people-int', 'data-people-int': i.date },
|
|
254
|
+
el('span', { class: 'rp-people-int__icon' }, icon(iconName)),
|
|
255
|
+
el('div', {},
|
|
256
|
+
el('div', { class: 'rp-people-int__text' }, i.text),
|
|
257
|
+
el('small', {}, `${day(i.date)} · ${i.kind}`),
|
|
258
|
+
el('small', { class: 'rp-people-int__src' }, [SOURCE[i.source] || i.source, i.pointer, i.tone].filter(Boolean).join(' · '))));
|
|
259
|
+
}))],
|
|
258
260
|
{ 'data-people-sec': 'interactions' });
|
|
259
261
|
}
|
|
260
262
|
|
|
@@ -267,8 +269,6 @@ function toneSec(inst, p) {
|
|
|
267
269
|
else if (t.proposed) kids.push(el('p', { class: 'rp-people-tone', 'data-people-proposed': t.proposed }, COPY.proposed(t.proposed)));
|
|
268
270
|
if (mix) kids.push(el('p', { class: 'rp-people-hint', 'data-people-mix': '' }, mix));
|
|
269
271
|
const acts = [];
|
|
270
|
-
if (!t.confirmed && t.proposed) acts.push(button({ label: COPY.confirm, variant: 'primary', size: 'sm', attrs: { 'data-people-act': 'tone-confirm' },
|
|
271
|
-
onClick: () => decide(inst, { act: 'tone', person: p.id, tone: t.proposed }, COPY.toneSaved) }));
|
|
272
272
|
const another = button({ label: COPY.setAnother, variant: 'outline', size: 'sm', iconEnd: 'chevron-down', attrs: { 'data-people-act': 'tone-set', 'aria-haspopup': 'menu' },
|
|
273
273
|
onClick: () => menu({ anchor: another, ariaLabel: COPY.setAnother,
|
|
274
274
|
items: TONES.map((x) => ({ label: x, onSelect: () => decide(inst, { act: 'tone', person: p.id, tone: x }, COPY.toneSaved) })) }) });
|
|
@@ -281,25 +281,24 @@ function toneSec(inst, p) {
|
|
|
281
281
|
|
|
282
282
|
function sharedSec(p) {
|
|
283
283
|
const s = p.shared;
|
|
284
|
-
const
|
|
285
|
-
for (const g of s.initiatives)
|
|
284
|
+
const chips = [];
|
|
285
|
+
for (const g of s.initiatives) chips.push(el('span', { class: 'rp-people-chip', 'data-people-shared': 'initiative' }, icon('target'), `${g.id} · ${g.count} interaction${g.count === 1 ? '' : 's'}`));
|
|
286
286
|
for (const it of s.items) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
const chip = el('span', { class: 'rp-people-chip', 'data-people-shared': 'item', 'data-people-item': it.id }, icon(it.blocked ? 'ban' : 'target'), `${COPY.owns} ${it.title || it.id}${it.blocked ? ' · ' + COPY.blocked : ''}`);
|
|
288
|
+
if (it.blocked) chip.classList.add('warn');
|
|
289
|
+
chips.push(chip);
|
|
290
290
|
}
|
|
291
291
|
for (const m of s.meetings) {
|
|
292
292
|
const when = new Date(m.start);
|
|
293
|
-
|
|
294
|
-
`${COPY.meeting} · ${when.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} ${when.toTimeString().slice(0, 5)} · ${m.title}`));
|
|
293
|
+
chips.push(el('span', { class: 'rp-people-chip', 'data-people-shared': 'meeting' }, icon('calendar'), `${when.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} · ${m.title}`));
|
|
295
294
|
}
|
|
296
|
-
return section(COPY.shared,
|
|
295
|
+
return section(COPY.shared, chips.length ? [el('div', { class: 'rp-people-chips' }, chips)] : [el('p', { class: 'rp-people-hint' }, COPY.noShared)], { 'data-people-sec': 'shared' });
|
|
297
296
|
}
|
|
298
297
|
|
|
299
298
|
function notesSec(inst, p) {
|
|
300
|
-
const list = p.notes.map((n) => el('
|
|
299
|
+
const list = p.notes.map((n) => el('div', { class: 'rp-people-note', 'data-people-note': n.id },
|
|
301
300
|
el('span', { class: 'rp-people-note__text' }, n.text),
|
|
302
|
-
|
|
301
|
+
iconButton({ icon: 'x', ariaLabel: COPY.remove, size: 'sm', attrs: { 'data-people-act': 'note-remove', 'data-people-key': n.id },
|
|
303
302
|
onClick: () => decide(inst, { act: 'note-remove', person: p.id, id: n.id }, COPY.noteRemoved) })));
|
|
304
303
|
const f = field({ label: COPY.addNote, kind: 'textarea', hideLabel: true, placeholder: COPY.notePlaceholder, attrs: { 'data-people-field': 'note' } });
|
|
305
304
|
const save = async () => {
|
|
@@ -310,8 +309,8 @@ function notesSec(inst, p) {
|
|
|
310
309
|
if (r) f.control.value = '';
|
|
311
310
|
};
|
|
312
311
|
return section(COPY.notes, [
|
|
313
|
-
list.length ? el('
|
|
314
|
-
el('div', { class: 'rp-people-noteadd' }, f,
|
|
312
|
+
list.length ? el('div', { class: 'rp-people-list-plain' }, list) : el('p', { class: 'rp-people-hint' }, COPY.noNotes),
|
|
313
|
+
el('div', { class: 'rp-people-noteadd' }, f, iconButton({ icon: 'plus', ariaLabel: COPY.addNote, size: 'sm', attrs: { 'data-people-act': 'note-add' }, onClick: save })),
|
|
315
314
|
], { 'data-people-sec': 'notes' });
|
|
316
315
|
}
|
|
317
316
|
|
|
@@ -319,25 +318,52 @@ function identitySec(inst, p) {
|
|
|
319
318
|
const by = {};
|
|
320
319
|
for (const i of p.interactions) by[i.source] = (by[i.source] || 0) + 1;
|
|
321
320
|
const file = inst.view.file ? String(inst.view.file).split('/').slice(-3).join('/') : 'people.jsonl';
|
|
322
|
-
|
|
323
|
-
el('
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
321
|
+
const details = el('details', { class: 'rp-people-sec', 'data-people-sec': 'identity' },
|
|
322
|
+
el('summary', {},
|
|
323
|
+
icon('chevron-down'),
|
|
324
|
+
COPY.identity),
|
|
325
|
+
el('div', { class: 'rp-people-list-plain' },
|
|
326
|
+
p.handles.map((h) => el('div', { class: 'rp-people-handle', 'data-people-handle': h.kind + ':' + h.value }, handleText(h))),
|
|
327
|
+
Object.entries(by).map(([s, n]) => el('div', { class: 'rp-people-hint' }, `${SOURCE[s] || s}: ${n} interaction${n === 1 ? '' : 's'}`)),
|
|
328
|
+
(p.sources || []).length ? el('div', { class: 'rp-people-hint', 'data-people-sources': p.sources.join(',') }, COPY.foundIn(p.sources.map((s) => SOURCE[s] || s).join(', '))) : null,
|
|
329
|
+
el('div', { class: 'rp-people-hint rp-people-file' }, COPY.lives(file))));
|
|
330
|
+
return details;
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
function personPane(inst) {
|
|
332
334
|
const p = inst.view.people.find((x) => x.id === inst.open);
|
|
333
|
-
if (!p) return el('div', { class: 'rp-people-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
335
|
+
if (!p) return el('div', { class: 'rp-people-card' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
|
|
336
|
+
const t = p.tone;
|
|
337
|
+
const confirmBtn = !t.confirmed && t.proposed ? button({ label: COPY.confirm + ' ' + t.proposed, icon: 'check', variant: 'primary', size: 'sm',
|
|
338
|
+
attrs: { 'data-people-act': 'tone-confirm' },
|
|
339
|
+
onClick: () => decide(inst, { act: 'tone', person: p.id, tone: t.proposed }, COPY.toneSaved) }) : null;
|
|
340
|
+
const moreBtn = iconButton({ icon: 'ellipsis', ariaLabel: 'More', attrs: { 'data-people-act': 'more' },
|
|
341
|
+
onClick: () => menu({ anchor: moreBtn, ariaLabel: 'Actions', items: [
|
|
342
|
+
{ label: COPY.removeFromPeople, icon: 'x', attrs: { 'data-people-act': 'remove' }, onSelect: async () => { const r = await decide(inst, { act: 'remove', person: p.id }, COPY.removed(p.name)); if (r) { inst.open = null; draw(inst); } } }
|
|
343
|
+
] }) });
|
|
344
|
+
return el('article', { class: 'rp-people-card', 'data-people-page': p.id },
|
|
345
|
+
el('div', { class: 'rp-people-page__header' },
|
|
346
|
+
el('div', { class: 'rp-people-page__info' },
|
|
347
|
+
el('span', { class: 'rp-people-page__avatar' }, p.name.split(/\s+/).map((w) => w[0]).join('').toUpperCase()),
|
|
348
|
+
el('div', { class: 'rp-people-page__text' },
|
|
349
|
+
el('h3', { class: 'rp-people-page__name' }, p.name),
|
|
350
|
+
el('p', { class: 'rp-people-hint' }, [who(p), p.external && (p.role || p.org) ? COPY.external : null].filter(Boolean).join(' · ')))),
|
|
351
|
+
el('div', { class: 'rp-people-page__actions' }, confirmBtn, moreBtn)),
|
|
352
|
+
el('div', { class: 'rp-people-page__tiles' },
|
|
353
|
+
el('div', { class: 'rp-people-tile' },
|
|
354
|
+
el('p', { class: 'rp-people-tile__label' }, COPY.interactions),
|
|
355
|
+
el('b', {}, p.count && p.last ? `${p.count} · ${day(p.last)}` : String(p.count || 0))),
|
|
356
|
+
el('div', { class: 'rp-people-tile' },
|
|
357
|
+
el('p', { class: 'rp-people-tile__label' }, COPY.tone),
|
|
358
|
+
el('b', {}, t.confirmed || t.proposed ? [
|
|
359
|
+
el('i', { class: 'rp-people-tile__dot ' + (t.confirmed ? 'rp-people-tile__dot--confirmed' : 'rp-people-tile__dot--proposed') }),
|
|
360
|
+
' ', t.confirmed ? t.confirmed : (t.proposed + '?')
|
|
361
|
+
] : 'None')),
|
|
362
|
+
el('div', { class: 'rp-people-tile' },
|
|
363
|
+
el('p', { class: 'rp-people-tile__label' }, COPY.shared),
|
|
364
|
+
el('b', {}, p.shared.initiatives.length ? `${p.shared.initiatives.length} ${p.shared.initiatives.length === 1 ? 'initiative' : 'initiatives'}` : 'None'))),
|
|
365
|
+
el('div', { class: 'rp-people-page__scroll' },
|
|
366
|
+
interactionsSec(p), toneSec(inst, p), sharedSec(p), notesSec(inst, p), identitySec(inst, p)));
|
|
341
367
|
}
|
|
342
368
|
|
|
343
369
|
/* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -348,7 +374,15 @@ function header(inst) {
|
|
|
348
374
|
const q = find.control;
|
|
349
375
|
q.addEventListener('input', () => { inst.q = q.value; draw(inst); });
|
|
350
376
|
q.addEventListener('keydown', (e) => { if (e.key === 'Escape') { q.resetCommitted(''); inst.q = ''; draw(inst); } });
|
|
351
|
-
|
|
377
|
+
const v = inst.view || { people: [] };
|
|
378
|
+
const all = v.people ? v.people.filter((p) => matches(p, inst.q.trim().toLowerCase())) : [];
|
|
379
|
+
const titleText = v.people && v.people.length ? `${v.people.length} ${v.people.length === 1 ? 'person' : 'people'}` : COPY.title;
|
|
380
|
+
inst.titleEl = el('h2', { class: 'rp-head__title' }, titleText);
|
|
381
|
+
inst.head = el('div', { class: 'rp-people-top' },
|
|
382
|
+
inst.titleEl,
|
|
383
|
+
tabs({ variant: 'segmented', value: inst.group, ariaLabel: 'Group', attrs: { 'data-people-group': '' },
|
|
384
|
+
items: [{ id: 'all', label: COPY.all }, { id: 'team', label: COPY.team }, { id: 'external', label: COPY.external }],
|
|
385
|
+
onChange: (val) => { inst.group = val; draw(inst); } }));
|
|
352
386
|
inst.head.find = find;
|
|
353
387
|
return inst.head;
|
|
354
388
|
}
|
|
@@ -367,5 +401,9 @@ function draw(inst) {
|
|
|
367
401
|
// Focus stays in a note being typed: a redraw of the page would drop it, so it waits until the field is left.
|
|
368
402
|
const typing = document.activeElement && inst.body.contains(document.activeElement) && document.activeElement.matches('textarea');
|
|
369
403
|
if (typing) { inst.last = ''; return; }
|
|
370
|
-
|
|
404
|
+
const n = inst.view.people.length;
|
|
405
|
+
inst.titleEl.textContent = n ? `${n} ${n === 1 ? 'person' : 'people'}` : COPY.title;
|
|
406
|
+
const rev = review(inst);
|
|
407
|
+
const grid = el('div', { class: 'rp-people-grid' }, listPane(inst), personPane(inst));
|
|
408
|
+
inst.body.replaceChildren(...[rev, grid].filter(Boolean));
|
|
371
409
|
}
|
|
@@ -89,9 +89,10 @@ export function groupsOf(repo, { all = false } = {}) {
|
|
|
89
89
|
const out = [...by.keys()].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b))
|
|
90
90
|
.map((i) => ({ key: 'i:' + i, title: titleOf(i), initiative: i, automatic: false, sessions: by.get(i) }));
|
|
91
91
|
if (none.length) out.push({ key: 'none', title: C().notLinked, initiative: null, automatic: false, sessions: none });
|
|
92
|
-
// hidden by default; the session in front is never hidden, and neither is one
|
|
92
|
+
// hidden by default; the session in front is never hidden, and neither is one held on a permission call (a finished agent reads
|
|
93
|
+
// 'needs you' too, so that state alone does not show it)
|
|
93
94
|
const front = frontSessionId();
|
|
94
|
-
const show = all || state.dsShowAuto || sched.some((s) => s.id === front || s.shown === '
|
|
95
|
+
const show = all || state.dsShowAuto || sched.some((s) => s.id === front || s.shown === 'blocked');
|
|
95
96
|
if (sched.length && show) out.push({ key: 'auto', title: C().automatic, initiative: null, automatic: true, sessions: sched });
|
|
96
97
|
return out;
|
|
97
98
|
}
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
body > [data-pm-theme] [data-dest="initiatives"] [data-in-nowline],
|
|
265
265
|
body > [data-pm-theme] [data-dest="initiatives"] [data-in-hint],
|
|
266
266
|
body > [data-pm-theme] [data-dest="initiatives"] .in-grip,
|
|
267
|
-
body > [data-pm-theme] [data-dest="board"] .bo-
|
|
267
|
+
body > [data-pm-theme] [data-dest="board"] .bo-head .td-count,
|
|
268
268
|
body > [data-pm-theme] [data-dest="docs"] .rp-docs-top .rp-head__title,
|
|
269
269
|
body[data-ph-view="cards"] > [data-pm-theme] [data-dest="docs"] .rp-docs-top,
|
|
270
270
|
body[data-ph-view="cards"] > [data-pm-theme] [data-dest="docs"] .rp-docs-views,
|
|
@@ -274,12 +274,12 @@ export function row({ title, titleIcon, meta, signals, lead, trail, onOpen, open
|
|
|
274
274
|
trail && trail.length ? el('span', { class: 'pmc-row__trail' }, trail) : null);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
/** card({ title, titleIcon?, meta?, signals?, lead?, trail?, onOpen, openLabel?, attrs? }): a raised card with one open button. */
|
|
278
|
-
export function card({ title, titleIcon, meta, signals, lead, trail, onOpen, openLabel, muted, attrs, openAttrs } = {}) {
|
|
277
|
+
/** card({ title, titleIcon?, meta?, signals?, lead?, trail?, onOpen, openLabel?, center?, attrs? }): a raised card with one open button. */
|
|
278
|
+
export function card({ title, titleIcon, meta, signals, lead, trail, onOpen, openLabel, muted, center, attrs, openAttrs } = {}) {
|
|
279
279
|
// the open button holds the title and the meta line, as a row's does: one column, and one tall target on a phone
|
|
280
280
|
const open = el('button', { type: 'button', class: 'pmc-card__open', 'aria-label': openLabel || null, onClick: onOpen, ...(openAttrs || {}) },
|
|
281
281
|
titleLine('pmc-card__title', title, titleIcon), metaLine('pmc-card__meta', meta, signals));
|
|
282
|
-
return el('div', withClass(cx('pmc-card', muted && 'pmc-card--muted'), attrs),
|
|
282
|
+
return el('div', withClass(cx('pmc-card', muted && 'pmc-card--muted', center && 'pmc-card--center'), attrs),
|
|
283
283
|
el('div', { class: 'pmc-card__top' },
|
|
284
284
|
lead && lead.length ? el('span', { class: 'pmc-card__lead' }, lead) : null,
|
|
285
285
|
open,
|
|
@@ -104,13 +104,15 @@ export const COPY = {
|
|
|
104
104
|
toggle: (need) => (need ? `Your day · ${need} ${need === 1 ? 'needs' : 'need'} you` : 'Your day: calendar, jobs, goals and mentions'),
|
|
105
105
|
fold: 'Fold your day',
|
|
106
106
|
hello: { morning: 'Good morning.', afternoon: 'Good afternoon.', evening: 'Good evening.' },
|
|
107
|
+
unfinished: (open, all) => (!all ? 'No goals yet' : open ? `${open} unfinished ${open === 1 ? 'goal' : 'goals'}` : 'All goals done'),
|
|
107
108
|
free: (min) => `${Math.floor(min / 60)} h ${min % 60} min free`,
|
|
108
109
|
steps: ['Read', 'Sort', 'Agents work', 'You approve'],
|
|
109
110
|
stepTitle: ['Read your sources', 'Make the jobs', 'Agents do the work', 'You approve'],
|
|
110
111
|
stepHint: ['The session reads your calendar and your mentions.', 'Each job is tied to a goal or a meeting.', 'One session per job, at most 5 at once.', 'Only what sends or decides waits for you.'],
|
|
111
112
|
stepOf: (n) => `Prepare my day · step ${n} of 4`,
|
|
112
113
|
status: {
|
|
113
|
-
idle: '
|
|
114
|
+
idle: 'Not prepared yet',
|
|
115
|
+
last: (at, n) => (at ? `Last prepared ${at}` : 'Prepared today') + (n ? ` · ${n} ${n === 1 ? 'needs' : 'need'} you` : ''),
|
|
114
116
|
reading: 'Reading your calendar and mentions…',
|
|
115
117
|
readFailed: (why) => `The read stopped: ${why}`,
|
|
116
118
|
working: (w, n) => `${w} ${w === 1 ? 'agent' : 'agents'} working · ${n} ${n === 1 ? 'needs' : 'need'} you`,
|
|
@@ -389,34 +389,38 @@ function select(type, id) {
|
|
|
389
389
|
function topBar(ctx) {
|
|
390
390
|
const { m, ws } = ctx;
|
|
391
391
|
const si = stepIndex(m.stage);
|
|
392
|
-
const
|
|
392
|
+
const press = runs.get(ws);
|
|
393
|
+
const busy = !!press && press.state === 'busy';
|
|
394
|
+
const bs = m.run && m.run.brief_session;
|
|
395
|
+
const reading = !!bs && !bs.ended_at && m.stage === 'reading' && !m.readFailed;
|
|
396
|
+
// the four steps show as a small progress bar only while Prepare my day is running; otherwise the bar says when it last ran
|
|
397
|
+
const inProgress = busy || reading || (!!m.run && m.stage === 'working');
|
|
398
|
+
const steps = inProgress ? el('ol', { class: 'td-day__steps', 'aria-label': D.prepare }, ...D.steps.map((t, i) => {
|
|
393
399
|
const st = si < 0 ? 'todo' : i < si || si === 4 ? 'done' : i === si ? 'now' : 'todo';
|
|
394
|
-
return el('li', null, button({ label: [el('span', { class: 'td-
|
|
395
|
-
attrs: { class: 'td-day__ts', 'data-step-state': st, 'data-day-step': String(i), 'data-day-sel': 'step:' + i, 'aria-current': ctx.cur('step', String(i)), title: D.stepHint[i] } }));
|
|
396
|
-
}));
|
|
400
|
+
return el('li', null, button({ label: [el('span', { class: 'td-day__seg', 'aria-hidden': 'true' })], variant: 'ghost', size: 'sm', onClick: () => select('step', i),
|
|
401
|
+
attrs: { class: 'td-day__ts', 'data-step-state': st, 'data-day-step': String(i), 'data-day-sel': 'step:' + i, 'aria-current': ctx.cur('step', String(i)), 'aria-label': t + ': ' + D.stepHint[i], title: t + ': ' + D.stepHint[i] } }));
|
|
402
|
+
})) : null;
|
|
397
403
|
const working = m.tasks.filter((t) => t.status === 'working' || t.status === 'sending').length;
|
|
398
|
-
const
|
|
399
|
-
|
|
404
|
+
const started = m.run && m.run.started_at ? hhmm(m.run.started_at) : '';
|
|
405
|
+
const status = !m.run ? (busy ? D.starting : D.status.idle) : m.readFailed ? D.status.readFailed(m.readFailed.why) : m.stage === 'reading' ? D.status.reading : m.stage === 'working' ? D.status.working(working, m.needs)
|
|
406
|
+
: D.status.last(started, m.needs);
|
|
400
407
|
let check = null;
|
|
401
408
|
if (m.run && m.check) {
|
|
402
409
|
const every = m.settings && m.settings.dayCheck && typeof m.settings.dayCheck.every === 'number' ? m.settings.dayCheck.every : m.check.every;
|
|
403
410
|
const running = !!m.check.running || ui.busy.has('check');
|
|
404
|
-
check = every === 0 ? el('span', { class: 'td-day__check', 'data-day': 'check' }, D.checkOff)
|
|
405
|
-
: el('span', { class: 'td-day__check', 'data-day': 'check'
|
|
406
|
-
|
|
411
|
+
check = every === 0 ? el('span', { class: 'td-day__check', 'data-day': 'check', title: D.checkOff }, D.checkOff)
|
|
412
|
+
: el('span', { class: 'td-day__check', 'data-day': 'check', title: running ? D.checking : D.checked(hhmm(m.check.last), hhmm(m.check.next)) },
|
|
413
|
+
iconButton({ icon: 'refresh-cw', ariaLabel: D.checkNow, size: 'sm', attrs: { 'data-day': 'check-now', disabled: running ? 'true' : null }, onClick: () => checkNow(ws) }));
|
|
407
414
|
}
|
|
408
|
-
const press = runs.get(ws);
|
|
409
|
-
const busy = !!press && press.state === 'busy';
|
|
410
|
-
const bs = m.run && m.run.brief_session;
|
|
411
|
-
const reading = !!bs && !bs.ended_at && m.stage === 'reading' && !m.readFailed;
|
|
412
415
|
const label = busy ? D.starting : reading ? D.stillReading : m.run ? D.again : D.prepare;
|
|
416
|
+
const open = m.allGoals.filter((g) => g.status === 'open').length;
|
|
413
417
|
return el('header', { class: 'td-day__bar' },
|
|
414
|
-
(() => { const when = [dateLabel(m.now),
|
|
418
|
+
(() => { const when = [dateLabel(m.now), D.unfinished(open, m.allGoals.length)].filter(Boolean).join(' · ');
|
|
415
419
|
return el('div', { class: 'td-day__hello', title: greet(m.now) + ' ' + when }, el('b', null, greet(m.now)), ' ', el('span', { class: 'td-day__dim' }, when)); })(),
|
|
416
420
|
steps,
|
|
417
421
|
el('span', { class: 'td-day__status', role: 'status', 'data-day': 'status', title: status }, status),
|
|
418
422
|
check,
|
|
419
|
-
|
|
423
|
+
iconButton({ icon: 'settings', ariaLabel: D.sources, size: 'sm', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all', 'aria-current': ctx.cur('sources', 'all') }, onClick: () => select('sources', 'all') }),
|
|
420
424
|
button({ label, variant: 'primary', size: 'sm', disabled: busy || reading, attrs: { 'data-day': 'prepare', 'data-goals': 'prepare-day', title: D.prepareHint, 'aria-busy': busy ? 'true' : 'false' }, onClick: () => prepareDay(ws) }),
|
|
421
425
|
iconButton({ icon: 'chevron-down', ariaLabel: D.fold, size: 'sm', expanded: true, attrs: { 'data-day': 'toggle' }, onClick: () => fold(false) }));
|
|
422
426
|
}
|