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.
@@ -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 and search) is built once, so typing never loses focus, and
6
- * only the body is rebuilt when its own data changed.
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
- * What it shows, for the repository the tab names (or the open one):
9
- * - Waiting for you: possible duplicates (Merge · Keep separate) and how many tones wait for a confirm. A new person
10
- * is added, not listed here (the PM, 2026-10-06): Remove from People on their page keeps them out;
11
- * - the list: search over names, handles and initiatives, All · Team · External, sort by latest, most or name;
12
- * - the person's page: who, interactions with where each came from, tone (Confirm · Set another · Change), shared
13
- * work (initiatives, items they own with Blocked shown, coming meetings), notes (Add · Remove), and identity and
14
- * sources (handles, how many interactions each source gave, the file the person lives in).
15
- * Every press is a page press to POST /api/people/decide. Nothing here reads a source: GET /api/people records the names
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-row', role: 'listitem', 'data-people-merge': key },
178
- row({
179
- title: COPY.same(m.handle.value, m.personName),
180
- meta: [m.why],
181
- trail: [
182
- button({ label: COPY.merge, variant: 'outline', size: 'sm', attrs: { 'data-people-act': 'merge', 'data-people-key': key },
183
- onClick: () => decide(inst, { act: 'merge', person: m.person, handle: m.handle }, COPY.merged) }),
184
- button({ label: COPY.keep, variant: 'ghost', size: 'sm', attrs: { 'data-people-act': 'keep-separate', 'data-people-key': key },
185
- onClick: () => decide(inst, { act: 'keep-separate', a: m.a, b: m.b }, COPY.kept) }),
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('section', { class: 'rp-people-review', 'data-people-review': '' },
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
- return el('div', { class: 'rp-people-row' + (open ? ' rp-people-row--open' : ''), role: 'listitem', 'data-people-person': p.id },
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.last ? `${p.count} · ${day(p.last)}` : String(p.count)],
219
- trail: t ? [badge({ text: p.tone.confirmed ? t : t + '?', tone: t === 'tense' ? 'destructive' : t === 'cautious' ? 'warning' : 'outline', attrs: { 'data-people-tone': t } })] : [],
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-list' }, controls, ...rows);
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
- return section(`${COPY.interactions} · ${p.count}`, [el('ol', { class: 'rp-people-ints' }, p.interactions.slice(0, 50).map((i) =>
254
- el('li', { class: 'rp-people-int', 'data-people-int': i.date },
255
- el('span', { class: 'rp-people-int__when' }, `${day(i.date)} · ${i.kind}`),
256
- el('span', { class: 'rp-people-int__text' }, i.text),
257
- el('span', { class: 'rp-people-int__src' }, [SOURCE[i.source] || i.source, i.pointer, i.tone].filter(Boolean).join(' · ')))))],
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 lines = [];
285
- for (const g of s.initiatives) lines.push(el('li', { class: 'rp-people-shared', 'data-people-shared': 'initiative' }, `${g.id} · ${g.count} interaction${g.count === 1 ? '' : 's'}`));
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
- lines.push(el('li', { class: 'rp-people-shared', 'data-people-shared': 'item', 'data-people-item': it.id },
288
- `${COPY.owns} ${it.title || it.id}${it.initiative ? ' · ' + it.initiative : ''} · ${it.state}`,
289
- it.blocked ? badge({ text: COPY.blocked, tone: 'warning', attrs: { title: it.reason || COPY.blocked } }) : null));
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
- lines.push(el('li', { class: 'rp-people-shared', 'data-people-shared': 'meeting' },
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, lines.length ? [el('ul', { class: 'rp-people-list-plain' }, lines)] : [el('p', { class: 'rp-people-hint' }, COPY.noShared)], { 'data-people-sec': '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('li', { class: 'rp-people-note', 'data-people-note': n.id },
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
- button({ label: COPY.remove, variant: 'ghost', size: 'sm', attrs: { 'data-people-act': 'note-remove', 'data-people-key': n.id },
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('ul', { class: 'rp-people-list-plain' }, list) : el('p', { class: 'rp-people-hint' }, COPY.noNotes),
314
- el('div', { class: 'rp-people-noteadd' }, f, el('div', { class: 'rp-people-acts' }, button({ label: COPY.addNote, variant: 'outline', size: 'sm', attrs: { 'data-people-act': 'note-add' }, onClick: save }))),
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
- return section(COPY.identity, [
323
- el('ul', { class: 'rp-people-list-plain' },
324
- p.handles.map((h) => el('li', { class: 'rp-people-handle', 'data-people-handle': h.kind + ':' + h.value }, handleText(h))),
325
- Object.entries(by).map(([s, n]) => el('li', { class: 'rp-people-hint' }, `${SOURCE[s] || s}: ${n} interaction${n === 1 ? '' : 's'}`))),
326
- (p.sources || []).length ? el('p', { class: 'rp-people-hint', 'data-people-sources': p.sources.join(',') }, COPY.foundIn(p.sources.map((s) => SOURCE[s] || s).join(', '))) : null,
327
- el('p', { class: 'rp-people-hint rp-people-file' }, COPY.lives(file)),
328
- ], { 'data-people-sec': 'identity' });
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-page' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
334
- return el('article', { class: 'rp-people-page', 'data-people-page': p.id },
335
- el('header', { class: 'rp-people-who' },
336
- el('h3', { class: 'rp-people-who__name' }, p.name),
337
- el('p', { class: 'rp-people-hint' }, [who(p), p.external && (p.role || p.org) ? COPY.external : null].filter(Boolean).join(' · ')),
338
- el('div', { class: 'rp-people-acts' }, button({ label: COPY.removeFromPeople, variant: 'ghost', size: 'sm', attrs: { 'data-people-act': 'remove' },
339
- onClick: async () => { const r = await decide(inst, { act: 'remove', person: p.id }, COPY.removed(p.name)); if (r) { inst.open = null; draw(inst); } } }))),
340
- interactionsSec(p), toneSec(inst, p), sharedSec(p), notesSec(inst, p), identitySec(inst, p));
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
- inst.head = el('div', { class: 'rp-people-top' }, el('h2', { class: 'rp-head__title' }, COPY.title), el('div', { class: 'rp-people-find' }, find));
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
- inst.body.replaceChildren(...[review(inst), el('div', { class: 'rp-people-grid' }, listPane(inst), personPane(inst))].filter(Boolean));
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 that is waiting on the PM
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 === 'needs' || s.shown === 'blocked');
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
- * focusButton({ toggle }) / paintFocus(b, on)
13
- * Focus: the dock takes the whole window, and Exit focus gives the page back (0.53.1)
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
- /* 0.55.1: icon only (the PM asked for no label): maximize-2 to focus, minimize-2 to leave; the name is the aria-label. */
107
- export function focusButton({ toggle } = {}) {
108
- return iconButton({ icon: 'maximize-2', ariaLabel: COPY.dock.focusTitle, onClick: () => toggle && toggle(),
109
- attrs: { class: 'fr-dock-focus', 'data-fr': 'dock-focus', 'aria-pressed': 'false', 'aria-keyshortcuts': 'Meta+Shift+J Control+Shift+J' } });
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
- /** Show the Focus control's state: pressed while the dock has the whole window. */
112
- export function paintFocus(b, on) {
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 name = on ? 'minimize-2' : 'maximize-2';
155
+ const want = inFocus ? 'minimize-2' : 'dock-' + (size || 'normal');
115
156
  const cur = b.querySelector('[data-icon]');
116
- if (!cur || cur.getAttribute('data-icon') !== name) b.replaceChildren(icon(name));
117
- b.setAttribute('aria-pressed', on ? 'true' : 'false');
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
- focus: dockParts ? dockParts.focus : dockMod.focusButton({ toggle: () => window.pmApp.frame.toggleFocus() }),
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, focus: null, orient: null };
1159
- dockMod.paintFocus(dockParts.focus, !!window.pmApp.frame.dockMax);
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, focus: dockParts.focus, orient: dockOrient };
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.focus : null,
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.focus : null, fr ? fr.fold : null]);
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.focus, fr.fold,
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.focus, fr.fold]);
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: 'One press: the agent reads, makes the jobs and does them. You approve only what sends or decides.',
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 steps = el('ol', { class: 'td-day__steps', 'aria-label': D.prepare }, ...D.steps.map((t, i) => {
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-day__tsdot', 'aria-hidden': 'true' }), t], variant: 'ghost', size: 'sm', onClick: () => select('step', i),
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 status = !m.run ? 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)
399
- : m.stage === 'review' ? D.status.review(m.needs, m.done) : D.status.done(m.done);
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' }, el('span', null, running ? D.checking : D.checked(hhmm(m.check.last), hhmm(m.check.next))),
406
- button({ label: D.checkNow, size: 'sm', disabled: running, attrs: { 'data-day': 'check-now' }, onClick: () => checkNow(ws) }));
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), m.free !== null && m.free !== undefined ? D.free(m.free) : null].filter(Boolean).join(' · ');
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
- button({ label: D.sources, size: 'sm', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all', 'aria-current': ctx.cur('sources', 'all') }, onClick: () => select('sources', 'all') }),
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: 'share-2', settings: 'settings' });
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' });