create-pmos 0.85.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.85.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@2d5583a
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:18:36Z"}
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:18:36Z"}
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:18:36Z"}
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
 
@@ -10,6 +10,12 @@ Open My work from the top bar, from ⌘K (**My work**), or press `g w`.
10
10
 
11
11
  My work lists every Jira ticket and every breakdown (WBS) item assigned to you, in all your registered repositories. It ranks them, shows what each one asks, and lets you give an agent an instruction for it. The agent's draft then waits for you in Agenda.
12
12
 
13
+ ## The screen
14
+
15
+ The page has the same layout as Agenda. The title says how many items wait for a brief. At the right of the title, four buttons switch the list: **To brief**, **Agents**, **Ready** and **All**, each with its count. A strip below the title shows when Jira was last read and how many tickets and plan items the read holds. **Read again** is at the right of the strip.
16
+
17
+ Two cards fill the rest of the page. The left card lists the items. Each row has a dot for its tier (red for P1, amber for P2, grey for the rest). A badge shows when the item is due, and a **Decision** badge marks an item that looks like a decision only you can make. The right card, **Brief**, shows the open item. On a phone, the two cards stack.
18
+
13
19
  ## Where the items come from
14
20
 
15
21
  - **Jira tickets.** The workspace does not read Jira. The **my-work-read** skill reads it in an agent session, with your own Atlassian connection. It reads the open issues assigned to you, with their comments and links, and writes a context card for each: who asked, what they ask, a short summary of the thread, the questions still open, and a suggested instruction for an agent. It does not copy the comments. Press **Read again** in My work to run it.
@@ -38,7 +44,7 @@ Being assigned a ticket does not mean an agent must resolve it. Every new item s
38
44
  2. Change the instruction. It starts as the agent's suggestion.
39
45
  3. Press **Run agent**. Or press **I'll handle it** to keep the item yourself.
40
46
 
41
- Run agent adds one job to today's day run, with your instruction. The job runs like every Agenda job: at most 5 at once, one draft, and nothing is sent. A hand icon marks an item that looks like a decision only you can make. An agent can still prepare it.
47
+ Run agent adds one job to today's day run, with your instruction. The job runs like every Agenda job: at most 5 at once, one draft, and nothing is sent. The **Decision** badge marks an item that looks like a decision only you can make. An agent can still prepare it.
42
48
 
43
49
  **Back to brief** returns an item to To brief, unless an agent is working on it.
44
50
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.85.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.85.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.85.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;
@@ -5928,12 +5995,51 @@
5928
5995
  }
5929
5996
  .rp-mywork-top {
5930
5997
  display: flex;
5931
- flex-wrap: wrap;
5998
+ flex-direction: column;
5999
+ gap: 12px;
6000
+ }
6001
+ .rp-mywork-header {
6002
+ display: flex;
6003
+ justify-content: space-between;
5932
6004
  align-items: center;
5933
6005
  gap: 12px;
5934
6006
  }
5935
- .rp-mywork-top > .rp-head__title {
5936
- margin-right: auto;
6007
+ .rp-mywork-header h2 {
6008
+ flex: 1;
6009
+ }
6010
+ .rp-mywork-strip {
6011
+ display: flex;
6012
+ align-items: center;
6013
+ gap: 10px;
6014
+ padding: 8px 12px;
6015
+ border: 1px solid var(--border);
6016
+ border-radius: 8px;
6017
+ background: var(--card);
6018
+ }
6019
+ .rp-mywork-title {
6020
+ flex: 1;
6021
+ margin: 0;
6022
+ font-size: 20px;
6023
+ font-weight: 600;
6024
+ }
6025
+ .rp-mywork-strip > .rp-mywork-hint:first-child {
6026
+ color: var(--foreground);
6027
+ }
6028
+ .rp-mywork-row {
6029
+ --background: transparent;
6030
+ }
6031
+ .rp-mywork-spacer {
6032
+ flex: 1;
6033
+ }
6034
+ .rp-mywork-dot--p1 {
6035
+ background: var(--destructive);
6036
+ }
6037
+ .rp-mywork-dot--p2 {
6038
+ background: var(--warning);
6039
+ }
6040
+ .rp-mywork-dot--p3,
6041
+ .rp-mywork-dot--p4 {
6042
+ background: var(--muted-foreground);
5937
6043
  }
5938
6044
  .rp-mywork-body {
5939
6045
  display: flex;
@@ -5942,32 +6048,69 @@
5942
6048
  }
5943
6049
  .rp-mywork-grid {
5944
6050
  display: grid;
5945
- grid-template-columns: minmax(280px, 3fr) 2fr;
5946
- gap: 24px;
5947
- align-items: start;
6051
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
6052
+ gap: 12px;
6053
+ align-items: stretch;
5948
6054
  }
5949
6055
  .rp-mywork-list {
5950
6056
  display: flex;
5951
6057
  flex-direction: column;
5952
- gap: 8px;
6058
+ gap: 0;
5953
6059
  min-width: 0;
5954
6060
  }
5955
6061
  .rp-mywork-views {
5956
6062
  border-bottom: 1px solid var(--border);
6063
+ margin-bottom: 8px;
5957
6064
  }
5958
6065
  .rp-mywork-row {
5959
- border-top: 1px solid var(--border);
6066
+ border: 1px solid transparent;
6067
+ border-radius: 6px;
5960
6068
  }
5961
- .rp-mywork-row:last-child {
5962
- border-bottom: 1px solid var(--border);
6069
+ .rp-mywork-grid > .td-day__panel,
6070
+ .rp-mywork-grid > .td-day__detail {
6071
+ align-content: start;
6072
+ }
6073
+ .rp-mywork-badge {
6074
+ gap: 4px;
6075
+ }
6076
+ .rp-mywork-row__inner {
6077
+ display: flex;
6078
+ align-items: center;
6079
+ gap: 10px;
6080
+ flex: 1;
6081
+ min-width: 0;
6082
+ }
6083
+ .rp-mywork-row__content {
6084
+ flex: 1;
6085
+ min-width: 0;
6086
+ }
6087
+ .rp-mywork-row__title {
6088
+ font-weight: 500;
6089
+ font-size: 14px;
6090
+ }
6091
+ .rp-mywork-row__meta {
6092
+ font-size: 12px;
6093
+ color: var(--muted-foreground);
5963
6094
  }
5964
6095
  .rp-mywork-row--open {
5965
- box-shadow: inset 2px 0 0 var(--foreground);
6096
+ background: var(--accent);
6097
+ border-color: var(--ring);
6098
+ }
6099
+ .rp-mywork-dot {
6100
+ width: 8px;
6101
+ height: 8px;
6102
+ border-radius: 50%;
6103
+ flex: none;
6104
+ display: inline-block;
6105
+ }
6106
+ .rp-mywork-badge {
6107
+ flex: none;
5966
6108
  }
5967
6109
  .rp-mywork-state {
5968
6110
  display: inline-flex;
5969
6111
  align-items: center;
5970
6112
  color: var(--muted-foreground);
6113
+ flex: none;
5971
6114
  }
5972
6115
  .rp-mywork-page {
5973
6116
  display: flex;
@@ -6035,6 +6178,10 @@
6035
6178
  .rp-mywork-grid {
6036
6179
  grid-template-columns: 1fr;
6037
6180
  }
6181
+ .rp-mywork-header {
6182
+ flex-direction: column;
6183
+ align-items: flex-start;
6184
+ }
6038
6185
  }
6039
6186
 
6040
6187
  /* -- layouts · .nt- Notes: one meeting on one screen — notes and what was caught beside a quiet transcript (meeting-notes Runs 1 and 2) -- */
@@ -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
  }
@@ -4,15 +4,13 @@
4
4
  * A DESTINATION MODULE (registry.js): mountDestination(host, ctx) on every render with the same host, disposeDestination(host).
5
5
  * It keeps ONE persistent root per host and rebuilds the body only when its own data or a choice changed.
6
6
  *
7
- * What it shows:
8
- * - the tabs To brief (the default) · Agents · Ready · All, each with its count, and Read again;
9
- * - the list, in WSJF order: the tier, the title, a meta line (Jira or WBS, the key, who asked, the due date) and a
10
- * state icon;
11
- * - the chosen item: What the agent found (the ask, the thread, the open questions), the instruction box prefilled
12
- * with the agent's suggestion, Run agent and I'll handle it; Back to brief when the item is the PM's or an agent's;
13
- * Open Agenda when its draft waits there; Details with each part of the score.
14
- * Every press is a page press to POST /api/my-work/act or /api/my-work/refresh. Approve, revise and retry are Agenda's
15
- * (R7): there is no approve button here. In fixture mode it reads nothing.
7
+ * Layout (matching Agenda's two-column design):
8
+ * - header: "N to brief in <repo>" with segmented control (To brief/Agents/Ready/All with counts);
9
+ * - strip: Jira read line and Read again button;
10
+ * - body: two-column grid (desktop) or single column (mobile)
11
+ * - left: .td-day__panel card with list items (status dot, title, meta, trailing badge);
12
+ * - right: .td-day__detail card with the chosen item's full view.
13
+ * Every press is a page press to POST /api/my-work/act or /api/my-work/refresh. In fixture mode it reads nothing.
16
14
  */
17
15
 
18
16
  import { el } from './dom.js';
@@ -71,6 +69,27 @@ const stateIcon = (x) => {
71
69
  return el('span', { class: 'rp-mywork-state', role: 'img', 'aria-label': stateWord(x), title: stateWord(x), 'data-my-work-state': x.state }, node);
72
70
  };
73
71
 
72
+ // Helper: create status dot colored by tier
73
+ const statusDot = (tier) => {
74
+ return el('span', { class: 'rp-mywork-dot rp-mywork-dot--' + String(tier).toLowerCase(), 'data-my-work-tier': tier, title: COPY.score({ tier }) });
75
+ };
76
+
77
+ // Trailing badges: a decision marker and a due date when one is set (soon = warning tone).
78
+ const dueLabel = (due) => {
79
+ const days = Math.round((new Date(due + 'T00:00:00') - new Date(new Date().toDateString())) / 86400000);
80
+ if (Number.isNaN(days)) return { text: 'Due ' + due, soon: false };
81
+ if (days < 0) return { text: 'Overdue', soon: true };
82
+ if (days === 0) return { text: 'Due today', soon: true };
83
+ if (days === 1) return { text: 'Due tomorrow', soon: true };
84
+ return { text: 'Due ' + new Date(due + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), soon: false };
85
+ };
86
+ const trailingBadges = (x) => {
87
+ const out = [];
88
+ if (x.kind === 'decide' && x.state === 'brief') out.push(badge({ text: 'Decision', tone: 'outline', attrs: { title: COPY.decide, class: 'rp-mywork-badge' } }));
89
+ if (x.due) { const d = dueLabel(x.due); out.push(badge({ text: d.text, tone: d.soon ? 'warning' : 'outline', attrs: { class: 'rp-mywork-badge' } })); }
90
+ return out;
91
+ };
92
+
74
93
  /* ── mount ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
75
94
 
76
95
  const insts = new WeakMap();
@@ -128,12 +147,12 @@ function itemRow(inst, x) {
128
147
  const meta = [x.source === 'jira' ? 'Jira ' + x.key : 'WBS ' + (x.repo ? x.repo + ' · ' : '') + x.key];
129
148
  if (x.from) meta.push(COPY.from(x.from));
130
149
  if (x.due) meta.push(COPY.due(x.due));
131
- return el('div', { class: 'rp-mywork-row' + (open ? ' rp-mywork-row--open' : ''), role: 'listitem', 'data-my-work-item': x.id },
150
+ return el('div', { class: 'rp-mywork-row td-day__r' + (open ? ' rp-mywork-row--open' : ''), role: 'listitem', 'data-my-work-item': x.id, 'data-cur': open ? 'true' : null },
132
151
  row({
133
152
  title: x.title,
134
153
  meta,
135
- lead: [badge({ text: x.wsjf.tier, tone: x.wsjf.tier === 'P1' ? 'warning' : 'outline', attrs: { 'data-my-work-tier': x.wsjf.tier, title: COPY.score(x.wsjf) } })],
136
- trail: [x.kind === 'decide' && x.state === 'brief' ? el('span', { class: 'rp-mywork-state', role: 'img', 'aria-label': COPY.decide, title: COPY.decide }, icon('diamond')) : null, stateIcon(x)].filter(Boolean),
154
+ lead: [statusDot(x.wsjf.tier)],
155
+ trail: [...trailingBadges(x), x.state !== 'brief' ? stateIcon(x) : el('span', { 'data-my-work-state': x.state, hidden: '' })].filter(Boolean),
137
156
  onOpen: () => { inst.open = x.id; inst.details = false; inst.last = ''; draw(inst); },
138
157
  openLabel: `${x.title}, ${x.wsjf.tier}, ${stateWord(x)}`,
139
158
  openAttrs: { 'aria-current': open ? 'true' : null, 'data-my-work-open': x.id },
@@ -143,14 +162,19 @@ function itemRow(inst, x) {
143
162
  function listPane(inst, d) {
144
163
  const shown = d.items.filter((x) => TAB_STATES[inst.tab].includes(x.state));
145
164
  const counts = { brief: d.counts.brief, agents: d.counts.agents, ready: d.counts.ready, all: d.counts.all };
146
- const controls = el('div', { class: 'rp-mywork-views' },
147
- tabs({ variant: 'line', value: inst.tab, ariaLabel: 'Show', attrs: { 'data-my-work-tabs': '' },
148
- items: Object.keys(COPY.tabs).map((id) => ({ id, label: COPY.tabs[id], count: counts[id] })),
149
- onChange: (v) => { inst.tab = v; inst.open = null; inst.last = ''; draw(inst); } }));
150
- const rows = shown.length
151
- ? el('div', { role: 'list', 'data-my-work-list': inst.tab }, shown.map((x) => itemRow(inst, x)))
152
- : el('p', { class: 'rp-section__empty' }, COPY.none[inst.tab]);
153
- return el('div', { class: 'rp-mywork-list' }, controls, rows);
165
+ const panelChildren = [
166
+ el('h2', { class: 'td-day__h2' }, COPY.tabs[inst.tab], el('span', null, counts[inst.tab] + (counts[inst.tab] === 1 ? ' item' : ' items')))
167
+ ];
168
+
169
+ if (shown.length) {
170
+ const rowsContainer = el('div', { role: 'list', 'data-my-work-list': inst.tab });
171
+ shown.forEach((x) => rowsContainer.appendChild(itemRow(inst, x)));
172
+ panelChildren.push(rowsContainer);
173
+ } else {
174
+ panelChildren.push(el('p', { class: 'rp-section__empty' }, COPY.none[inst.tab]));
175
+ }
176
+
177
+ return el('section', { class: 'td-day__panel' }, ...panelChildren);
154
178
  }
155
179
 
156
180
  /* ── the item ────────────────────────────────────────────────────────────────────────────────────────────────────── */
@@ -178,7 +202,7 @@ function briefSec(inst, x) {
178
202
  const r = await act(inst, { action: 'brief', item: x.id, instruction }, COPY.started);
179
203
  if (r) inst.drafts.delete(x.id);
180
204
  } });
181
- const mine = button({ label: COPY.handle, variant: 'outline', size: 'sm', disabled: inst.busy, attrs: { 'data-my-work-act': 'handle' },
205
+ const mine = button({ label: COPY.handle, variant: 'outline', size: 'sm', icon: 'check', disabled: inst.busy, attrs: { 'data-my-work-act': 'handle' },
182
206
  onClick: () => act(inst, { action: 'handle', item: x.id }, COPY.taken) });
183
207
  return el('div', { class: 'rp-mywork-brief' }, f, el('div', { class: 'rp-mywork-acts' }, run, mine));
184
208
  }
@@ -198,14 +222,18 @@ function statusSec(inst, x) {
198
222
 
199
223
  function itemPane(inst, d) {
200
224
  const x = d.items.find((y) => y.id === inst.open);
201
- if (!x) return el('div', { class: 'rp-mywork-page' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
225
+ if (!x) return el('section', { class: 'td-day__detail' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
226
+
202
227
  const head = el('header', { class: 'rp-mywork-who' },
203
228
  el('h3', { class: 'rp-mywork-who__title' }, x.title),
204
229
  el('p', { class: 'rp-mywork-hint' }, [x.source === 'jira' ? 'Jira' : 'WBS', x.key, x.repo, x.status, x.due ? COPY.due(x.due) : null, x.from ? COPY.from(x.from) : null].filter(Boolean).join(' · ')),
205
230
  x.url ? el('a', { class: 'rp-mywork-link', href: x.url, target: '_blank', rel: 'noopener noreferrer' }, icon('link'), ' ', COPY.source) : null);
231
+
206
232
  const details = button({ label: inst.details ? COPY.hide : COPY.details, variant: 'ghost', size: 'sm', iconEnd: inst.details ? 'chevron-down' : 'chevron-right',
207
233
  attrs: { 'aria-expanded': inst.details ? 'true' : 'false', 'data-my-work-details': '' }, onClick: () => { inst.details = !inst.details; inst.last = ''; draw(inst); } });
208
- return el('article', { class: 'rp-mywork-page', 'data-my-work-page': x.id },
234
+
235
+ return el('article', { class: 'rp-mywork-page td-day__detail', 'data-my-work-page': x.id },
236
+ el('h2', { class: 'td-day__h2' }, 'Brief', el('span', null, x.key)),
209
237
  head, foundSec(x),
210
238
  x.state === 'brief' ? section(COPY.instruction, [briefSec(inst, x)], { 'data-my-work-sec': 'brief' }) : statusSec(inst, x),
211
239
  el('div', { class: 'rp-mywork-acts' }, details), inst.details ? el('p', { class: 'rp-mywork-hint', 'data-my-work-score': '' }, COPY.score(x.wsjf)) : null);
@@ -213,18 +241,42 @@ function itemPane(inst, d) {
213
241
 
214
242
  /* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
215
243
 
216
- function header(inst) {
217
- if (inst.head) return inst.head;
244
+ function header(inst, d) {
245
+ const key = JSON.stringify([d && d.counts, inst.tab]);
246
+ if (inst.head && inst.headData === key) return inst.head;
247
+ inst.headData = key;
248
+
249
+ const counts = d && d.counts ? d.counts : { brief: 0, agents: 0, ready: 0, all: 0 };
250
+ const briefCount = counts.brief || 0;
251
+ const nounPhrase = briefCount === 0 ? 'Nothing to brief' : `${briefCount} to brief`;
252
+
253
+ const segmentedTabs = tabs({ variant: 'segmented', role: 'radiogroup', value: inst.tab, ariaLabel: 'Filter', attrs: { 'data-my-work-tabs': '' },
254
+ items: Object.keys(COPY.tabs).map((id) => ({ id, label: COPY.tabs[id], count: counts[id] })),
255
+ onChange: (v) => { inst.tab = v; inst.open = null; inst.last = ''; draw(inst); } });
256
+ const btns = segmentedTabs.querySelectorAll('button');
257
+ Object.keys(COPY.tabs).forEach((id, i) => { if (btns[i]) btns[i].setAttribute('data-tab', id); });
258
+
218
259
  inst.readLine = el('p', { class: 'rp-mywork-hint', 'data-my-work-read': '' });
219
- inst.head = el('div', { class: 'rp-mywork-top' }, el('h2', { class: 'rp-head__title' }, COPY.title), inst.readLine,
220
- button({ label: COPY.readAgain, variant: 'outline', size: 'sm', icon: 'refresh-cw', attrs: { 'data-my-work-act': 'refresh' }, onClick: () => readAgain(inst) }));
260
+ const readAgainBtn = button({ label: COPY.readAgain, variant: 'outline', size: 'sm', icon: 'refresh-cw', attrs: { 'data-my-work-act': 'refresh', 'data-my-work-read': '' }, onClick: () => readAgain(inst) });
261
+
262
+ const headerRow = el('div', { class: 'rp-mywork-header' },
263
+ el('h2', { class: 'rp-mywork-title' }, nounPhrase),
264
+ segmentedTabs);
265
+
266
+ const stripCard = el('div', { class: 'rp-mywork-strip' },
267
+ inst.readLine,
268
+ el('span', { class: 'rp-mywork-spacer' }),
269
+ inst.countLine = el('span', { class: 'rp-mywork-hint' }),
270
+ readAgainBtn);
271
+
272
+ inst.head = el('div', { class: 'rp-mywork-top' }, headerRow, stripCard);
221
273
  return inst.head;
222
274
  }
223
275
 
224
276
  function draw(inst) {
225
277
  const root = inst.root;
226
278
  const d = myWork();
227
- const head = header(inst);
279
+ const head = header(inst, d);
228
280
  if (head.parentNode !== root || inst.body.parentNode !== root) root.replaceChildren(head, inst.body);
229
281
  const fp = JSON.stringify([d, inst.tab, inst.open, inst.details, inst.busy]);
230
282
  if (fp === inst.last) return;
@@ -235,8 +287,14 @@ function draw(inst) {
235
287
  if (!d) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty' }, COPY.loading)); return; }
236
288
  if (!d.ok) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, d.error || COPY.readError)); return; }
237
289
  inst.readLine.textContent = COPY.readAt(d.read_at ? new Date(d.read_at).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : null);
290
+ const nJira = d.items.filter((x) => x.source === 'jira').length;
291
+ const nPlan = d.items.length - nJira;
292
+ inst.countLine.textContent = `${nJira} ticket${nJira === 1 ? '' : 's'} · ${nPlan} plan item${nPlan === 1 ? '' : 's'}`;
238
293
  const shown = d.items.filter((x) => TAB_STATES[inst.tab].includes(x.state));
239
294
  if (!shown.some((x) => x.id === inst.open)) inst.open = shown.length ? shown[0].id : null;
240
295
  const banner = (d.banner || []).length ? el('p', { class: 'rp-mywork-hint', role: 'status', 'data-my-work-banner': '' }, d.banner.map((b) => b.source + ': ' + b.error).join(' · ')) : null;
241
- inst.body.replaceChildren(...[banner, el('div', { class: 'rp-mywork-grid' }, listPane(inst, d), itemPane(inst, d))].filter(Boolean));
296
+ const grid = el('div', { class: 'rp-mywork-grid td-day__cols' },
297
+ listPane(inst, d),
298
+ itemPane(inst, d));
299
+ inst.body.replaceChildren(...[banner, grid].filter(Boolean));
242
300
  }
@@ -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,