create-pmos 0.86.0 → 0.87.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.86.0",
3
+ "version": "0.87.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@f91d32d
1
+ kernel-source: pmos@6a237b2
2
2
  assembled: 2026-10-07
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:27:16Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T16:37:01Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:27:16Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T16:37:01Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:27:16Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T16:37:01Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,13 +2,15 @@
2
2
  type: Guide
3
3
  title: "Board: the items of every initiative"
4
4
  description: "How to read and use the Board destination, filter it, add an item, move a card, release a batch of work items, and print the same Board with pmos board."
5
- timestamp: 2026-10-05
5
+ timestamp: 2026-10-07
6
6
  ---
7
7
 
8
8
  # Board: the items of every initiative
9
9
 
10
10
  The Board shows the items of all your work initiatives in one place. It has four columns: Todo, Doing, Review and Done. A card is one item. The Board is a view of the same items that the Breakdown of each initiative shows, so a card is always in the column that its row has in the Tree.
11
11
 
12
+ The Board looks like the Agenda. The title at the top counts the items, for example "43 items in code". The filters and **Add item** sit on the right of the title. Each column heading has a status dot and a count. A column shows its first 3 cards. Press **Show 14 more** under a column to see all of its cards. An owner icon on a card shows who owns it: a robot for an agent, or "You".
13
+
12
14
  A document initiative has no items, so it has no card and no place in the filter.
13
15
 
14
16
  ## Open the Board
@@ -33,7 +35,7 @@ Press **Add item**, the one main button of the Board. Type a title, choose the i
33
35
 
34
36
  - **On a wide screen,** drag a card to another column. A drop on Done is held for 8 seconds, so you can press **Undo**.
35
37
  - **On a phone,** press the card to open its sheet and press a state button. A card moves in 2 taps. From another column it takes 3: first press the column name, then the card, then the state.
36
- - **From the menu.** Press the three dots on a card, then choose Move to a state.
38
+ - **From the menu.** Press the three dots on a card, then choose Move to a state. Every card shows its three dots.
37
39
 
38
40
  When you move a card that an agent owns, the card reads "moved by you". The agent's own next move can change it again.
39
41
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Docs: every repository file by category, the files each one links to, and whether each copy in Google Docs or Confluence is the same"
4
- timestamp: 2026-10-06
4
+ timestamp: 2026-10-07
5
5
  ---
6
6
 
7
7
  # Docs
@@ -14,6 +14,8 @@ Nothing syncs by itself. A copy changes only when you press a button for that co
14
14
 
15
15
  ## The list
16
16
 
17
+ The title at the top says how many copies need you, for example "2 need you in pmos". The three views are a segmented control at the top right, each with a count. Under the title, one bar shows when the copies were last compared, a search box and the **Compare every copy again** button. The list is in one card with a small label that names the view.
18
+
17
19
  Docs has three views:
18
20
 
19
21
  - **Needs you** shows the copies that are not the same.
@@ -117,7 +119,7 @@ A sync never creates, moves or deletes a page or a file. When the copy's text is
117
119
 
118
120
  ## Compare
119
121
 
120
- PMOS cannot read Google Docs or Confluence itself. **↻** next to "Compared … ago" starts a session that reads every copy with your own connectors. The session reads only. The sentences change when it finishes.
122
+ PMOS cannot read Google Docs or Confluence itself. **Compare every copy again** in the bar, next to "Compared … ago", starts a session that reads every copy with your own connectors. The session reads only. The sentences change when it finishes.
121
123
 
122
124
  A compare runs only when you press it. There is no schedule. An edit to a repository file changes its sentence at once, without a compare.
123
125
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.86.0",
3
+ "version": "0.87.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.86.0",
9
+ "version": "0.87.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.86.0",
3
+ "version": "0.87.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -10,10 +10,11 @@
10
10
  */
11
11
 
12
12
  import { el } from './dom.js';
13
- import { card, tabs, dragHandle, iconButton, empty } from './primitives.js';
13
+ import { card, tabs, dragHandle, iconButton, empty, button, badge } from './primitives.js';
14
14
  import { COPY, STATES, shortDate, ownerLabel } from './breakdown-copy.js';
15
15
  import { stageLabel } from './bd-model.js';
16
16
  import { signalsFor, byPart } from './bd-tree.js';
17
+ import { icon } from './icons.js';
17
18
 
18
19
  /** The width below which the Board shows one column at a time. Mirrors components.css's container query. */
19
20
  export const WIDE_BOARD = 600;
@@ -21,14 +22,25 @@ export const WIDE_BOARD = 600;
21
22
  function boardCard(ctx, id, wide) {
22
23
  const { L, A } = ctx;
23
24
  const it = L.items[id];
24
- const meta = [stageLabel(L, it.stage), ownerLabel(it.owner), it.due ? COPY.due(shortDate(it.due)) : null, byPart(it)];
25
- const trail = ctx.cardMenus
26
- ? [iconButton({
25
+ const ag = !!ctx.agenda;
26
+ // the Board destination shows the Agenda's card: initiative and stage only, the owner as an icon; the Breakdown keeps the long meta
27
+ const meta = ag ? [stageLabel(L, it.stage)] : [stageLabel(L, it.stage), ownerLabel(it.owner), it.due ? COPY.due(shortDate(it.due)) : null, byPart(it)];
28
+
29
+ const ownerIcon = !ag ? null : it.owner === 'you'
30
+ ? badge({ text: 'You', tone: 'outline', attrs: { 'data-bd-card-owner': 'you' } })
31
+ : it.owner === 'agent'
32
+ ? el('span', { class: 'bd-card__owner-icon', 'data-bd-card-owner': 'agent' }, icon('bot'))
33
+ : null;
34
+
35
+ const trail = !ag && !ctx.cardMenus ? null : [
36
+ ownerIcon,
37
+ iconButton({
27
38
  icon: 'ellipsis', size: 'sm', ariaLabel: COPY.aria.more(it.title),
28
39
  onClick: (e) => { e.stopPropagation(); A.menu(e.currentTarget, id, 'board'); },
29
- attrs: { 'aria-haspopup': 'menu', 'data-fk': 'more:' + id, 'data-bd-more': '' },
30
- })]
31
- : null;
40
+ attrs: { 'aria-haspopup': 'menu', 'data-fk': 'more:' + id, 'data-bd-more': '', 'data-bd-card-menu': '' },
41
+ }),
42
+ ].filter(Boolean);
43
+
32
44
  // Without the ⋯ button the card's menu is a right-click away, and Shift+F10 (or the ContextMenu key) for the keyboard.
33
45
  const menuKeys = ctx.cardMenus ? {} : {
34
46
  'aria-keyshortcuts': 'Shift+F10',
@@ -48,8 +60,9 @@ function boardCard(ctx, id, wide) {
48
60
  };
49
61
  return card({
50
62
  title: it.title, titleIcon: it.milestone ? 'diamond' : null, meta, signals: signalsFor(it, 'board'),
51
- lead: wide ? [dragHandle({ label: COPY.aria.reorder(it.title) })] : null, trail,
63
+ lead: wide ? [dragHandle({ label: COPY.aria.reorder(it.title), })] : null, trail,
52
64
  muted: it.proposed, onOpen: () => A.openSheet(id),
65
+ center: ag,
53
66
  attrs: { class: 'bd-card', 'data-item': id, 'data-state': it.state },
54
67
  openAttrs: { 'data-fk': 'card:' + id, ...menuKeys },
55
68
  });
@@ -68,14 +81,47 @@ export function renderBoard(ctx) {
68
81
  attrs: { class: 'bd-switch', 'data-bd-switch': '' },
69
82
  });
70
83
  for (const b of sw.querySelectorAll('[data-tab]')) b.setAttribute('data-fk', 'col:' + b.getAttribute('data-tab'));
71
- const cols = STATES.map((s) => el('div', { class: 'bd-col' + (s === shown ? ' bd-col--shown' : ''), 'data-col-wrap': s },
72
- el('div', { class: 'bd-col__head' }, el('span', null, COPY.states[s]), el('span', { class: 'bd-col__n' }, '· ' + counts[s])),
73
- s === 'done' && L.board.doneOlder > 0 ? el('div', { class: 'bd-col__older', 'data-bd-older': String(L.board.doneOlder) }, COPY.olderInTree(L.board.doneOlder)) : null,
74
- // an empty column says so, so a blank area never reads as a failed load
75
- !counts[s] && !(s === 'done' && L.board.doneOlder > 0) ? empty({ text: COPY.emptyColumn(COPY.states[s]), attrs: { 'data-bd-empty-col': s } }) : null,
76
- // the list fills its column, so the whole column height takes a drop
77
- el('div', { class: 'bd-col__list', 'data-col': s, 'aria-label': COPY.states[s] }, (L.board[s] || []).map((id) => boardCard(ctx, id, wide)))));
78
- return [sw, el('div', { class: 'bd-board', 'data-bd-board': wide ? 'wide' : 'narrow' }, cols)];
84
+
85
+ const cols = STATES.map((s) => {
86
+ const allIds = L.board[s] || [];
87
+ const expanded = P.expandedCols || (P.expandedCols = new Set());
88
+ const isExpanded = expanded.has(s);
89
+ const showLimit = ctx.agenda ? 3 : Infinity;
90
+ const shownIds = isExpanded ? allIds : allIds.slice(0, showLimit);
91
+ const hasMore = allIds.length > showLimit;
92
+ const moreCount = allIds.length - showLimit;
93
+
94
+ const statusDot = el('span', { class: 'bd-col__dot bd-col__dot--' + s });
95
+ const colName = COPY.states[s];
96
+ const countBadge = badge({ text: String(counts[s]), tone: 'outline', attrs: { 'data-bd-col-count': s } });
97
+
98
+ const cards = shownIds.map((id) => boardCard(ctx, id, wide));
99
+
100
+ const moreButton = hasMore && !isExpanded
101
+ ? button({
102
+ label: `Show ${moreCount} more`,
103
+ variant: 'ghost',
104
+ icon: 'chevron-down',
105
+ size: 'sm',
106
+ onClick: () => {
107
+ expanded.add(s);
108
+ A.rerender();
109
+ },
110
+ attrs: { 'data-bd-col-more': s, 'data-bd-show-more': '' },
111
+ })
112
+ : null;
113
+
114
+ return el('div', { class: 'bd-col' + (s === shown ? ' bd-col--shown' : ''), 'data-col-wrap': s },
115
+ ctx.agenda
116
+ ? el('div', { class: 'bd-col__head' }, el('div', { class: 'bd-col__name' }, statusDot, el('span', null, colName)), countBadge)
117
+ : el('div', { class: 'bd-col__head' }, el('span', null, COPY.states[s]), el('span', { class: 'bd-col__n' }, '· ' + counts[s])),
118
+ s === 'done' && L.board.doneOlder > 0 ? el('div', { class: 'bd-col__older', 'data-bd-older': String(L.board.doneOlder) }, COPY.olderInTree(L.board.doneOlder)) : null,
119
+ // an empty column says so, so a blank area never reads as a failed load
120
+ !counts[s] && !(s === 'done' && L.board.doneOlder > 0) ? empty({ text: COPY.emptyColumn(COPY.states[s]), attrs: { 'data-bd-empty-col': s } }) : null,
121
+ // the list fills its column, so the whole column height takes a drop
122
+ el('div', { class: 'bd-col__list', 'data-col': s, 'aria-label': COPY.states[s] }, ...cards, moreButton));
123
+ });
124
+ return [sw, el('div', { class: 'bd-board' + (ctx.agenda ? ' bd-board--agenda' : ''), 'data-bd-board': wide ? 'wide' : 'narrow' }, cols)];
79
125
  }
80
126
 
81
127
  /**
@@ -16,6 +16,7 @@ export const COPY = {
16
16
  initiative: 'Initiative',
17
17
  anyInitiative: 'All initiatives',
18
18
  addItem: 'Add item',
19
+ title: (n, productName) => `${n} ${n === 1 ? 'item' : 'items'} in ${productName || 'code'}`,
19
20
  adding: {
20
21
  title: 'Add item',
21
22
  sub: 'The item goes to its initiative. Enter adds it.',
@@ -34,6 +35,7 @@ export const COPY = {
34
35
  stageOf: (initiative, stage) => `${initiative} · ${stage}`,
35
36
  notMerged: 'Done · your call · not merged',
36
37
  summary: (n) => (n === 1 ? '1 item' : `${n} items`),
38
+ showMore: (n) => `Show ${n} more`,
37
39
  release: {
38
40
  head: 'Release',
39
41
  title: (initiative) => `Release · ${initiative}`,
@@ -497,6 +497,13 @@
497
497
  position: absolute;
498
498
  inset: 0;
499
499
  }
500
+ .pmc-card--center .pmc-card__top {
501
+ align-items: center;
502
+ }
503
+ .pmc-card--center .pmc-card__trail {
504
+ align-items: center;
505
+ gap: 4px;
506
+ }
500
507
  .pmc-card__trail {
501
508
  position: relative;
502
509
  z-index: 1;
@@ -1313,6 +1320,11 @@
1313
1320
  padding: 16px;
1314
1321
  align-items: stretch;
1315
1322
  }
1323
+ .bd-board--agenda {
1324
+ flex: 1 1 auto;
1325
+ min-height: 0;
1326
+ overflow-y: auto;
1327
+ }
1316
1328
  .bd-switch {
1317
1329
  display: none;
1318
1330
  }
@@ -1339,6 +1351,32 @@
1339
1351
  font-variant-numeric: tabular-nums;
1340
1352
  letter-spacing: 0;
1341
1353
  }
1354
+ /* the Board destination (Agenda look): sentence-case head with a status dot and a count badge */
1355
+ .bd-board--agenda .bd-col__head {
1356
+ align-items: center;
1357
+ justify-content: space-between;
1358
+ padding-bottom: 0;
1359
+ border-bottom: 0;
1360
+ font-size: 14px;
1361
+ line-height: 20px;
1362
+ letter-spacing: 0;
1363
+ text-transform: none;
1364
+ }
1365
+ .bd-col__name {
1366
+ display: flex;
1367
+ align-items: center;
1368
+ gap: 8px;
1369
+ }
1370
+ .bd-col__dot {
1371
+ display: inline-block;
1372
+ width: 8px;
1373
+ height: 8px;
1374
+ border-radius: 50%;
1375
+ background: var(--muted-foreground);
1376
+ }
1377
+ .bd-col__dot--doing { background: var(--primary); }
1378
+ .bd-col__dot--review { background: var(--warning); }
1379
+ .bd-col__dot--done { background: var(--primary); }
1342
1380
  .bd-col__list {
1343
1381
  display: flex;
1344
1382
  flex: 1 1 auto;
@@ -1357,6 +1395,13 @@
1357
1395
  .bd-card {
1358
1396
  cursor: pointer;
1359
1397
  }
1398
+ .bd-card__owner-icon {
1399
+ display: inline-flex;
1400
+ align-items: center;
1401
+ justify-content: center;
1402
+ width: 20px;
1403
+ height: 20px;
1404
+ }
1360
1405
  @container bd-pane (max-width: 599px) {
1361
1406
  .bd-switch {
1362
1407
  display: flex;
@@ -4209,7 +4254,8 @@
4209
4254
 
4210
4255
  /* -- layouts · .bo- Board (Run 4, slice 4D) -- */
4211
4256
  .bo-head {
4212
- flex-wrap: wrap;
4257
+ display: block;
4258
+ padding: 16px 16px 0;
4213
4259
  }
4214
4260
  .bo-head__filters {
4215
4261
  display: flex;
@@ -4221,46 +4267,35 @@
4221
4267
  .bo-head__pick {
4222
4268
  min-width: 160px;
4223
4269
  }
4270
+
4224
4271
  .bo-head__end {
4225
4272
  display: flex;
4226
4273
  align-items: center;
4227
4274
  gap: 12px;
4228
4275
  margin-left: auto;
4229
4276
  }
4230
- .bo-head__n {
4231
- color: var(--muted-foreground);
4232
- font-size: 12px;
4233
- line-height: 16px;
4234
- font-variant-numeric: tabular-nums;
4235
- }
4236
4277
  .bo-releases {
4237
4278
  display: flex;
4238
4279
  flex-direction: column;
4239
- gap: 4px;
4280
+ gap: 8px;
4240
4281
  margin: 16px 16px 0;
4241
- padding: 12px;
4242
- border: 1px solid var(--border);
4243
- border-radius: 8px;
4282
+ padding: 0;
4244
4283
  }
4245
4284
  .bo-releases__head {
4246
- font-size: 12px;
4247
- line-height: 16px;
4248
- font-weight: 600;
4249
- letter-spacing: 0.04em;
4250
- text-transform: uppercase;
4285
+ display: none;
4251
4286
  }
4252
4287
  .bo-releases__note {
4253
- color: var(--muted-foreground);
4254
- font-size: 12px;
4255
- line-height: 16px;
4288
+ display: none;
4256
4289
  }
4257
4290
  .bo-rel {
4258
4291
  display: flex;
4259
4292
  align-items: center;
4260
4293
  justify-content: space-between;
4261
4294
  gap: 12px;
4262
- padding: 8px 0;
4263
- border-top: 1px solid var(--border);
4295
+ padding: 10px 12px;
4296
+ border: 1px solid var(--border);
4297
+ border-radius: 8px;
4298
+ background: var(--card);
4264
4299
  }
4265
4300
  .bo-rel--today {
4266
4301
  padding: 10px 12px;
@@ -4273,7 +4308,8 @@
4273
4308
  min-width: 0;
4274
4309
  }
4275
4310
  .bo-rel__title {
4276
- font-weight: 600;
4311
+ font-size: 14px;
4312
+ font-weight: 500;
4277
4313
  overflow-wrap: anywhere;
4278
4314
  }
4279
4315
  .bo-rel__meta,
@@ -5335,16 +5371,12 @@
5335
5371
  }
5336
5372
  .rp-docs-views {
5337
5373
  display: flex;
5338
- flex-wrap: wrap;
5339
5374
  align-items: center;
5340
- gap: 12px;
5341
- border-bottom: 1px solid var(--border);
5342
5375
  }
5343
5376
  .rp-docs-when {
5344
5377
  display: flex;
5345
5378
  align-items: center;
5346
- gap: 4px;
5347
- margin-left: auto;
5379
+ gap: 6px;
5348
5380
  font-size: 12px;
5349
5381
  color: var(--muted-foreground);
5350
5382
  }
@@ -5356,10 +5388,45 @@
5356
5388
  color: var(--muted-foreground);
5357
5389
  }
5358
5390
  .rp-docs-row {
5359
- border-top: 1px solid var(--border);
5391
+ border-radius: 8px;
5360
5392
  }
5361
- .rp-docs-row:last-child {
5362
- border-bottom: 1px solid var(--border);
5393
+ .rp-docs-row + .rp-docs-row {
5394
+ margin-top: 2px;
5395
+ }
5396
+ .rp-docs-head {
5397
+ display: flex;
5398
+ flex-direction: column;
5399
+ gap: 12px;
5400
+ }
5401
+ .rp-docs-bar {
5402
+ display: flex;
5403
+ flex-wrap: wrap;
5404
+ align-items: center;
5405
+ gap: 8px 10px;
5406
+ padding: 8px 12px;
5407
+ border-radius: 8px;
5408
+ background: var(--card);
5409
+ box-shadow: 0 0 0 1px var(--border);
5410
+ }
5411
+ .rp-docs-bar .rp-docs-find {
5412
+ flex: 1 1 220px;
5413
+ max-width: none;
5414
+ }
5415
+ .rp-docs-panel {
5416
+ display: flex;
5417
+ flex-direction: column;
5418
+ gap: 8px;
5419
+ min-width: 0;
5420
+ padding: 10px 12px;
5421
+ border-radius: 8px;
5422
+ background: var(--card);
5423
+ box-shadow: 0 0 0 1px var(--border);
5424
+ }
5425
+ .rp-docs-panel__eyebrow {
5426
+ font-size: 11px;
5427
+ letter-spacing: 0.04em;
5428
+ text-transform: uppercase;
5429
+ color: var(--muted-foreground);
5363
5430
  }
5364
5431
  .rp-docs-dot {
5365
5432
  display: inline-block;
@@ -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
- const n = out ? (out.counts.todo + out.counts.doing + out.counts.review + out.counts.done) : 0;
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: 'bo-head__filters' }, f, el('div', { class: 'bo-head__pick' }, pick)),
140
- el('div', { class: 'bo-head__end' }, el('span', { class: 'bo-head__n', 'data-bo-count': String(n) }, COPY.summary(n)), add),
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
- // as the Breakdown's Board: a wide Board with a fine pointer draws cards without the ⋯ button (the 33-control budget)
152
- P.cardMenus = !P.wide || P.coarse;
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: P.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) return 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.head = el('div', { class: 'rp-docs-top' }, el('h2', { class: 'rp-head__title' }, COPY.title), el('div', { class: 'rp-docs-find' }, find));
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 = [el('div', { class: 'rp-docs-views' },
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(...body(inst));
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
  }
@@ -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-head__n,
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,