create-pmos 0.87.0 → 0.89.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/layout.md +3 -3
- 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/app.js +42 -4
- package/workspace/public/pm/components.css +315 -58
- package/workspace/public/pm/dest-people.js +114 -76
- package/workspace/public/pm/dock-sessions.js +3 -2
- package/workspace/public/pm/dock.js +53 -13
- package/workspace/public/pm/frame-copy.js +6 -0
- package/workspace/public/pm/panel-tabs.js +11 -8
- package/workspace/public/pm/primitives.js +2 -2
- 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
|
@@ -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
|
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* createDockRail({ open, resize }) the 44px rail: one control that opens the dock. While the dock is open the rail
|
|
10
10
|
* is a thin grip on the dock's left edge that resizes it (0.53.1)
|
|
11
11
|
* foldButton({ fold }) the control in the open dock's strip that folds it back to the rail
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* dockSizeButton({ setSize, getState }) / paintDockSize(b, size, inFocus)
|
|
13
|
+
* Dock size menu: Normal, Half, Two-thirds, Whole window (focus; 0.53.1, folded into the menu later)
|
|
14
14
|
* sessionMenuButton({ open, ws }) "+ Session": opens the new-session sheet (dock-sessions.js): a blank
|
|
15
15
|
* session or a skill to start from (R10; one list since 0.55.1)
|
|
16
16
|
* sessionDefaults() / setSessionDefaults(patch)
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { el } from './dom.js';
|
|
24
24
|
import { FIXTURE } from './state.js';
|
|
25
25
|
import { pmGet } from './http.js';
|
|
26
|
-
import { button, iconButton } from './primitives.js';
|
|
26
|
+
import { button, iconButton, menu } from './primitives.js';
|
|
27
27
|
import { icon } from './icons.js';
|
|
28
28
|
import { COPY } from './frame-copy.js';
|
|
29
29
|
|
|
@@ -103,19 +103,59 @@ const endDrag = () => { if (drag) { drag.g.removeAttribute('data-dragging'); dra
|
|
|
103
103
|
window.addEventListener('pointerup', endDrag);
|
|
104
104
|
window.addEventListener('pointercancel', endDrag);
|
|
105
105
|
|
|
106
|
-
/*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
/* Dock size menu: Normal, Half, Two-thirds, or Whole window (focus). It replaces the old Focus button; focus is its last row
|
|
107
|
+
* and keeps ⌘⇧J. The button shows the current size; the menu is built on each press, so it always shows the current choice. */
|
|
108
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
109
|
+
const SIZE_SHARE = { normal: 7, half: 9, 'two-thirds': 12 }; // width of the filled part of the glyph, out of 18
|
|
110
|
+
/** A small window glyph with the dock's share filled on the right. Own SVG: the vendored sprite has no such symbol. */
|
|
111
|
+
function sizeGlyph(size) {
|
|
112
|
+
const svg = icon('square'); // the primitive's own <svg> (class, size, aria-hidden); its symbol is replaced below
|
|
113
|
+
svg.replaceChildren();
|
|
114
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
115
|
+
svg.setAttribute('fill', 'none');
|
|
116
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
117
|
+
svg.setAttribute('stroke-width', '2');
|
|
118
|
+
svg.setAttribute('data-icon', 'dock-' + size);
|
|
119
|
+
const frame = document.createElementNS(SVG_NS, 'rect');
|
|
120
|
+
for (const [k, v] of Object.entries({ x: 3, y: 5, width: 18, height: 14, rx: 2 })) frame.setAttribute(k, String(v));
|
|
121
|
+
const fill = document.createElementNS(SVG_NS, 'rect');
|
|
122
|
+
const w = SIZE_SHARE[size] || SIZE_SHARE.normal;
|
|
123
|
+
for (const [k, v] of Object.entries({ x: 21 - w, y: 5, width: w, height: 14 })) fill.setAttribute(k, String(v));
|
|
124
|
+
fill.setAttribute('fill', 'currentColor');
|
|
125
|
+
fill.setAttribute('stroke', 'none');
|
|
126
|
+
svg.append(frame, fill);
|
|
127
|
+
return svg;
|
|
110
128
|
}
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
const sizeNode = (size, inFocus) => (inFocus ? icon('minimize-2') : sizeGlyph(size || 'normal'));
|
|
130
|
+
|
|
131
|
+
export function dockSizeButton({ setSize, getState } = {}) {
|
|
132
|
+
return iconButton({ icon: 'maximize-2', ariaLabel: COPY.dock.dockSize,
|
|
133
|
+
attrs: { class: 'fr-dock-size', 'data-fr': 'dock-size', 'aria-haspopup': 'menu', 'aria-expanded': 'false', 'aria-keyshortcuts': 'Meta+Shift+J Control+Shift+J' },
|
|
134
|
+
onClick: (e) => {
|
|
135
|
+
const b = e.currentTarget;
|
|
136
|
+
const st = (getState && getState()) || {};
|
|
137
|
+
const cur = st.inFocus ? 'whole' : (st.size || 'normal');
|
|
138
|
+
const row = (id, label, hint, node) => ({
|
|
139
|
+
label, iconNode: node, hint, role: 'menuitemradio',
|
|
140
|
+
attrs: { 'data-size': id, 'aria-checked': cur === id ? 'true' : 'false' },
|
|
141
|
+
onSelect: () => setSize && setSize(id),
|
|
142
|
+
});
|
|
143
|
+
menu({ anchor: b, ariaLabel: COPY.dock.dockSizeMenu, items: [
|
|
144
|
+
row('normal', COPY.dock.sizeNormal, '', sizeGlyph('normal')),
|
|
145
|
+
row('half', COPY.dock.sizeHalf, '', sizeGlyph('half')),
|
|
146
|
+
row('two-thirds', COPY.dock.sizeTwoThirds, '', sizeGlyph('two-thirds')),
|
|
147
|
+
row('whole', COPY.dock.sizeWhole, '⌘⇧J', icon('maximize-2')),
|
|
148
|
+
] });
|
|
149
|
+
} });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Show the button's glyph and name for the current size. */
|
|
153
|
+
export function paintDockSize(b, size, inFocus) {
|
|
113
154
|
if (!b) return;
|
|
114
|
-
const
|
|
155
|
+
const want = inFocus ? 'minimize-2' : 'dock-' + (size || 'normal');
|
|
115
156
|
const cur = b.querySelector('[data-icon]');
|
|
116
|
-
if (!cur || cur.getAttribute('data-icon') !==
|
|
117
|
-
|
|
118
|
-
const label = on ? COPY.dock.unfocusTitle : COPY.dock.focusTitle;
|
|
157
|
+
if (!cur || cur.getAttribute('data-icon') !== want) b.replaceChildren(sizeNode(size, inFocus));
|
|
158
|
+
const label = inFocus ? COPY.dock.unfocusTitle : COPY.dock.dockSize;
|
|
119
159
|
b.setAttribute('aria-label', label);
|
|
120
160
|
b.title = label;
|
|
121
161
|
}
|
|
@@ -62,6 +62,12 @@ export const COPY = {
|
|
|
62
62
|
unfocus: 'Exit focus',
|
|
63
63
|
focusTitle: 'Give the dock the whole window (⌘⇧J)',
|
|
64
64
|
unfocusTitle: 'Show the page beside the dock again (⌘⇧J)',
|
|
65
|
+
dockSize: 'Dock size',
|
|
66
|
+
dockSizeMenu: 'Choose dock size',
|
|
67
|
+
sizeNormal: 'Normal',
|
|
68
|
+
sizeHalf: 'Half the window',
|
|
69
|
+
sizeTwoThirds: 'Two-thirds',
|
|
70
|
+
sizeWhole: 'Whole window (focus)',
|
|
65
71
|
resize: 'Resize the agent dock. Drag, or use the left and right arrow keys. Double-click resets the width.',
|
|
66
72
|
session: 'Session',
|
|
67
73
|
sessionMenu: 'Start a session',
|
|
@@ -1152,11 +1152,14 @@ function frameDock() {
|
|
|
1152
1152
|
open: () => (dsMod ? dsMod.openNewSessionSheet(currentProduct(), { initiative: null }) : newSession()),
|
|
1153
1153
|
}),
|
|
1154
1154
|
fold: dockParts ? dockParts.fold : dockMod.foldButton({ fold: () => window.pmApp.frame.foldDock() }),
|
|
1155
|
-
|
|
1155
|
+
size: dockParts ? dockParts.size : dockMod.dockSizeButton({
|
|
1156
|
+
setSize: (sz) => window.pmApp.frame.setDockSize(sz),
|
|
1157
|
+
getState: () => ({ size: window.pmApp.frame.dockSize, inFocus: !!window.pmApp.frame.dockMax }),
|
|
1158
|
+
}),
|
|
1156
1159
|
};
|
|
1157
1160
|
}
|
|
1158
|
-
if (isMobile()) return { session: dockParts.session, fold: null,
|
|
1159
|
-
dockMod.
|
|
1161
|
+
if (isMobile()) return { session: dockParts.session, fold: null, size: null, orient: null };
|
|
1162
|
+
dockMod.paintDockSize(dockParts.size, window.pmApp.frame.dockSize, !!window.pmApp.frame.dockMax);
|
|
1160
1163
|
// the tabs-on-top / tabs-at-side switch, icon only in the frame (0.55.1); drawn per paint because its icon follows the mode
|
|
1161
1164
|
// one element across paints, so the keyboard focus stays on it; only its icon and name follow the mode
|
|
1162
1165
|
const v = !!state.tabsVertical;
|
|
@@ -1170,7 +1173,7 @@ function frameDock() {
|
|
|
1170
1173
|
const olabel = v ? COPY.dock.topTabs : COPY.dock.sideTabs;
|
|
1171
1174
|
dockOrient.setAttribute('aria-label', olabel);
|
|
1172
1175
|
dockOrient.title = olabel;
|
|
1173
|
-
return { session: dockParts.session, fold: dockParts.fold,
|
|
1176
|
+
return { session: dockParts.session, fold: dockParts.fold, size: dockParts.size, orient: dockOrient };
|
|
1174
1177
|
}
|
|
1175
1178
|
|
|
1176
1179
|
/**
|
|
@@ -1395,7 +1398,7 @@ export function renderTabs(host) {
|
|
|
1395
1398
|
newSessionOptsBtn,
|
|
1396
1399
|
canSplit ? splitBtn : null,
|
|
1397
1400
|
orientBtn,
|
|
1398
|
-
fr ? fr.
|
|
1401
|
+
fr ? fr.size : null,
|
|
1399
1402
|
fr ? fr.fold : null),
|
|
1400
1403
|
]);
|
|
1401
1404
|
} else if (ck) {
|
|
@@ -1410,7 +1413,7 @@ export function renderTabs(host) {
|
|
|
1410
1413
|
].filter(Boolean);
|
|
1411
1414
|
extras[0].style.marginLeft = 'auto';
|
|
1412
1415
|
// The switch sits after `+ session`, so the strip still ends the mockup's way in the fixture, which does not draw it.
|
|
1413
|
-
setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, fr ? fr.session : newSessionBtn, FIXTURE ? null : orientBtn, fr ? fr.
|
|
1416
|
+
setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, fr ? fr.session : newSessionBtn, FIXTURE ? null : orientBtn, fr ? fr.size : null, fr ? fr.fold : null]);
|
|
1414
1417
|
} else {
|
|
1415
1418
|
tabBar.setAttribute('style', BAR_STYLE);
|
|
1416
1419
|
newSessionBtn.setAttribute('style', CHIP_STYLE);
|
|
@@ -1437,11 +1440,11 @@ export function renderTabs(host) {
|
|
|
1437
1440
|
...dsMod.railNodes(ds, others.map((t) => dockTabRow(t, (act && act.id === t.id) || (splitTab && splitTab.id === t.id)))),
|
|
1438
1441
|
] : [
|
|
1439
1442
|
...dsMod.stripNodes(ds, others.map((t) => dockTabChip(t, (act && act.id === t.id) || (splitTab && splitTab.id === t.id)))),
|
|
1440
|
-
state.splitId ? splitBtn : null, fr.orient, fr.
|
|
1443
|
+
state.splitId ? splitBtn : null, fr.orient, fr.size, fr.fold,
|
|
1441
1444
|
]);
|
|
1442
1445
|
}
|
|
1443
1446
|
|
|
1444
|
-
if (ck && ds && vertical) setChildren(dockHead, [fr.session, state.splitId ? splitBtn : null, fr.orient, fr.
|
|
1447
|
+
if (ck && ds && vertical) setChildren(dockHead, [fr.session, state.splitId ? splitBtn : null, fr.orient, fr.size, fr.fold]);
|
|
1445
1448
|
if (tabBar.scrollLeft !== keepX) tabBar.scrollLeft = keepX;
|
|
1446
1449
|
if (tabBar.scrollTop !== keepY) tabBar.scrollTop = keepY;
|
|
1447
1450
|
// When the tab in front changes, the bar scrolls just enough to show it, so a tab opened at the far end of a crowded strip or a
|
|
@@ -354,9 +354,9 @@ export function menu({ anchor, items, ariaLabel } = {}) {
|
|
|
354
354
|
if (it.sep) { node.appendChild(el('div', { class: 'pmc-menu__sep', role: 'separator' })); continue; }
|
|
355
355
|
const b = el('button', {
|
|
356
356
|
...(it.attrs || {}),
|
|
357
|
-
type: 'button', role: 'menuitem', tabindex: '-1', class: cx('pmc-menu__item', it.danger && 'pmc-menu__item--danger'),
|
|
357
|
+
type: 'button', role: it.role || 'menuitem', tabindex: '-1', class: cx('pmc-menu__item', it.danger && 'pmc-menu__item--danger'),
|
|
358
358
|
onClick: (e) => { e.stopPropagation(); close(false); if (it.onSelect) it.onSelect(); },
|
|
359
|
-
}, it.icon ? icon(it.icon) : null, el('span', null, it.label));
|
|
359
|
+
}, it.iconNode || (it.icon ? icon(it.icon) : null), el('span', null, it.label), it.hint ? el('kbd', { class: 'pmc-menu__hint' }, it.hint) : null);
|
|
360
360
|
btns.push(b); node.appendChild(b);
|
|
361
361
|
}
|
|
362
362
|
node.addEventListener('toggle', (e) => { if (e.newState === 'closed') close(false); });
|
|
@@ -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
|
}
|
|
@@ -37,7 +37,7 @@ export function unsyncedOf(st) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** The lucide symbol each place wears in the bar (UI comfort, 2026-10-07: a word alone is slow to find; the phone's tab bar already wears these). */
|
|
40
|
-
const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: '
|
|
40
|
+
const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'users', settings: 'settings' });
|
|
41
41
|
|
|
42
42
|
export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
43
43
|
const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
|