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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/board.md +4 -2
- package/workspace/guide/docs.md +4 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/bd-board.js +62 -16
- package/workspace/public/pm/board-copy.js +2 -0
- package/workspace/public/pm/components.css +97 -30
- package/workspace/public/pm/dest-board.js +15 -9
- package/workspace/public/pm/dest-docs.js +46 -21
- package/workspace/public/pm/mobile.css +1 -1
- package/workspace/public/pm/primitives.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T16:37:01Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/board.md
CHANGED
|
@@ -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-
|
|
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
|
|
package/workspace/guide/docs.md
CHANGED
|
@@ -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-
|
|
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.
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.87.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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:
|
|
4280
|
+
gap: 8px;
|
|
4240
4281
|
margin: 16px 16px 0;
|
|
4241
|
-
padding:
|
|
4242
|
-
border: 1px solid var(--border);
|
|
4243
|
-
border-radius: 8px;
|
|
4282
|
+
padding: 0;
|
|
4244
4283
|
}
|
|
4245
4284
|
.bo-releases__head {
|
|
4246
|
-
|
|
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
|
-
|
|
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:
|
|
4263
|
-
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-
|
|
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:
|
|
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-
|
|
5391
|
+
border-radius: 8px;
|
|
5360
5392
|
}
|
|
5361
|
-
.rp-docs-row
|
|
5362
|
-
|
|
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
|
-
|
|
140
|
+
|
|
141
|
+
const title = el('h2', { class: 'td-count', 'data-bo-count': String(n) }, COPY.title(n, productName));
|
|
142
|
+
|
|
138
143
|
return [
|
|
139
|
-
el('div', { class: '
|
|
140
|
-
|
|
144
|
+
el('div', { class: 'td-head' },
|
|
145
|
+
title,
|
|
146
|
+
el('div', { class: 'bo-head__end' }, f, el('div', { class: 'bo-head__pick' }, pick), add)),
|
|
141
147
|
];
|
|
142
148
|
}
|
|
143
149
|
|
|
@@ -148,14 +154,14 @@ function render(P) {
|
|
|
148
154
|
P.drags.destroy();
|
|
149
155
|
const L = view(P);
|
|
150
156
|
P.wide = (P.root.getBoundingClientRect().width || P.lastWidth || 0) >= WIDE_BOARD;
|
|
151
|
-
//
|
|
152
|
-
P.cardMenus =
|
|
157
|
+
// Always render menu buttons; they will be hidden with CSS opacity until hover
|
|
158
|
+
P.cardMenus = true;
|
|
153
159
|
setChildren(P.headEl, headOf(P));
|
|
154
160
|
let kids;
|
|
155
161
|
if (!P.loaded) kids = [el('div', { class: 'bd-note' }, COPY.reading)];
|
|
156
162
|
else if (!L) kids = [el('div', { class: 'bd-note bd-note--error', role: 'alert' }, COPY.readFailed(P.error || ''))];
|
|
157
163
|
else {
|
|
158
|
-
kids = renderBoard({ P, L, A: P.actions, wide: P.wide, cardMenus:
|
|
164
|
+
kids = renderBoard({ P, L, A: P.actions, wide: P.wide, cardMenus: true, agenda: true });
|
|
159
165
|
const rel = (P.out && P.out.releases) || [];
|
|
160
166
|
if (rel.length) {
|
|
161
167
|
grantScopes(() => bump(P));
|
|
@@ -381,7 +387,7 @@ function createPane(host, ws) {
|
|
|
381
387
|
const P = {
|
|
382
388
|
host, ws, root, headEl, bodyEl, slug: 'board', filter: 'active', init: null, out: null, raw: null, older: 0, error: null, loaded: false,
|
|
383
389
|
loading: false, again: false, boardCol: 'todo', ver: 0, renderedVer: -1, drags: dragSet(), dragging: false, rendering: false,
|
|
384
|
-
disposed: false, sheet: null, wide: false, cardMenus: true, lastWidth: 0,
|
|
390
|
+
disposed: false, sheet: null, wide: false, cardMenus: true, lastWidth: 0, expandedCols: new Set(),
|
|
385
391
|
coarse: typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches,
|
|
386
392
|
};
|
|
387
393
|
P.actions = makeActions(P);
|
|
@@ -178,7 +178,7 @@ export function mountDestination(host, ctx) {
|
|
|
178
178
|
const ws = repo ? wsOf(repo) : null;
|
|
179
179
|
if (ws !== inst.ws) {
|
|
180
180
|
stopPoll(inst);
|
|
181
|
-
Object.assign(inst, { ws, loaded: false, error: null, view: null, last: '', open: null, adding: null, files: [], lib: null, libError: null, graph: null });
|
|
181
|
+
Object.assign(inst, { ws, repo, loaded: false, error: null, view: null, last: '', open: null, adding: null, files: [], lib: null, libError: null, graph: null });
|
|
182
182
|
inst.openCats.clear(); inst.full.clear();
|
|
183
183
|
if (ws) { ensureSprite(); refresh(inst); }
|
|
184
184
|
draw(inst);
|
|
@@ -362,7 +362,7 @@ function fileLine(inst, f, q = '') {
|
|
|
362
362
|
title: marked(f.title, q),
|
|
363
363
|
meta: [ini ? el('span', { class: 'rp-docs-ini', title: ini.title }, marked(ini.short, q)) : el('span', { class: 'rp-docs-path' }, marked(f.path, q)), day(f.date)],
|
|
364
364
|
lead: [el('span', { class: 'rp-docs-dot rp-docs-dot--none', 'aria-hidden': 'true' })],
|
|
365
|
-
trail: adding ? [] : [button({ label: COPY.addCopy, variant: 'outline', size: 'sm', attrs: { class: 'rp-docs-addcopy', 'data-docs-act': 'add-copy', 'data-docs-key': f.path }, onClick: start })],
|
|
365
|
+
trail: adding ? [] : [button({ label: COPY.addCopy, variant: 'outline', size: 'sm', icon: 'plus', attrs: { class: 'rp-docs-addcopy', 'data-docs-act': 'add-copy', 'data-docs-key': f.path }, onClick: start })],
|
|
366
366
|
onOpen: inst.lib ? toggle : null,
|
|
367
367
|
openLabel: `${f.title}: show the files it links to`,
|
|
368
368
|
openAttrs: { 'aria-expanded': open ? 'true' : 'false', title: f.path, 'data-docs-file-toggle': f.path },
|
|
@@ -388,17 +388,48 @@ function fileLine(inst, f, q = '') {
|
|
|
388
388
|
/* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
389
389
|
|
|
390
390
|
function header(inst) {
|
|
391
|
-
if (inst.head)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
391
|
+
if (!inst.head) {
|
|
392
|
+
const find = field({ label: COPY.find, hideLabel: true, placeholder: COPY.find, attrs: { 'data-docs-find': '' } });
|
|
393
|
+
const q = find.control;
|
|
394
|
+
inst.findControl = q;
|
|
395
|
+
q.addEventListener('input', () => { inst.q = q.value; inst.open = null; inst.adding = null; inst.graph = null; inst.full.clear(); searchFiles(inst); draw(inst); });
|
|
396
|
+
// The field restores its last committed text on Escape; here Escape clears the search instead.
|
|
397
|
+
q.addEventListener('keydown', (e) => { if (e.key === 'Escape') { q.resetCommitted(''); inst.q = ''; inst.graph = null; searchFiles(inst); draw(inst); } });
|
|
398
|
+
inst.titleEl = el('h2', { class: 'rp-head__title' }, COPY.title);
|
|
399
|
+
inst.tabsSlot = el('div', { class: 'rp-docs-views' });
|
|
400
|
+
inst.whenEl = el('span', { class: 'rp-docs-when', 'data-docs-summary': '' });
|
|
401
|
+
inst.compareBtn = button({ label: COPY.compareAll, variant: 'primary', size: 'sm', icon: 'refresh-cw', attrs: { 'data-docs-act': 'check' }, onClick: () => compareAll(inst) });
|
|
402
|
+
inst.head = el('div', { class: 'rp-docs-head' },
|
|
403
|
+
el('div', { class: 'rp-docs-top' }, inst.titleEl, inst.tabsSlot),
|
|
404
|
+
el('div', { class: 'rp-docs-bar' }, inst.whenEl, el('div', { class: 'rp-docs-find' }, find), inst.compareBtn));
|
|
405
|
+
}
|
|
399
406
|
return inst.head;
|
|
400
407
|
}
|
|
401
408
|
|
|
409
|
+
/** The parts of the header that follow the data: the title, the view tabs with their counts, when the copies were compared. */
|
|
410
|
+
function updateHead(inst) {
|
|
411
|
+
const v = inst.view;
|
|
412
|
+
const lib = inst.lib;
|
|
413
|
+
const all = v ? v.docs.reduce((n, d) => n + d.links.length, 0) : 0;
|
|
414
|
+
const needs = v ? v.docs.reduce((n, d) => n + d.links.filter((l) => needsYou(inst, d, l)).length, 0) : 0;
|
|
415
|
+
const running = !!v && (!!(v.open || v.session) || inst.busy);
|
|
416
|
+
const repoName = inst.repo ? String(inst.repo).split('/').pop() : '';
|
|
417
|
+
inst.titleEl.textContent = needs > 0 ? COPY.needYou(needs) + (repoName ? ` in ${repoName}` : '') : COPY.title;
|
|
418
|
+
inst.tabsSlot.replaceChildren(tabs({ variant: 'segmented', items: [{ id: 'needs', label: COPY.needs, count: needs }, { id: 'copies', label: COPY.copies, count: all },
|
|
419
|
+
{ id: 'files', label: COPY.files, count: lib ? lib.files.length.toLocaleString('en-US') : null }],
|
|
420
|
+
value: inst.filter, ariaLabel: 'Filter', attrs: { 'data-docs-filter': '' }, onChange: (val) => { inst.filter = val; inst.graph = null; draw(inst); } }));
|
|
421
|
+
inst.whenEl.replaceChildren(icon('refresh-cw'), el('span', null, !v ? '' : running ? COPY.comparing : (v.last ? COPY.compared(v.last.finished_at) : COPY.never)));
|
|
422
|
+
inst.compareBtn.disabled = running || !all;
|
|
423
|
+
inst.compareBtn.setAttribute('aria-label', COPY.compareAll);
|
|
424
|
+
inst.compareBtn.title = COPY.compareAll;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** The body inside one card with a small label, like an Agenda panel. */
|
|
428
|
+
function panel(inst, nodes) {
|
|
429
|
+
const label = inst.q.trim() ? 'Results' : inst.filter === 'files' ? COPY.files : inst.filter === 'copies' ? COPY.copies : COPY.needs;
|
|
430
|
+
return el('section', { class: 'rp-docs-panel' }, el('div', { class: 'rp-docs-panel__eyebrow' }, label), ...nodes);
|
|
431
|
+
}
|
|
432
|
+
|
|
402
433
|
const needsYou = (inst, d, l) => l.signal !== 'same' || !!l.syncing || inst.pending.has(keyOf(d, l));
|
|
403
434
|
|
|
404
435
|
/**
|
|
@@ -467,14 +498,7 @@ function body(inst) {
|
|
|
467
498
|
const all = v.docs.reduce((n, d) => n + d.links.length, 0);
|
|
468
499
|
const needs = v.docs.reduce((n, d) => n + d.links.filter((l) => needsYou(inst, d, l)).length, 0);
|
|
469
500
|
const running = !!(v.open || v.session) || inst.busy;
|
|
470
|
-
const out = [
|
|
471
|
-
tabs({ variant: 'line', items: [{ id: 'needs', label: COPY.needs, count: needs }, { id: 'copies', label: COPY.copies, count: all },
|
|
472
|
-
{ id: 'files', label: COPY.files, count: lib ? lib.files.length.toLocaleString('en-US') : null }],
|
|
473
|
-
value: inst.filter, ariaLabel: 'Filter', attrs: { 'data-docs-filter': '' }, onChange: (val) => { inst.filter = val; inst.graph = null; draw(inst); } }),
|
|
474
|
-
el('span', { class: 'rp-docs-when', 'data-docs-summary': '' },
|
|
475
|
-
running ? COPY.comparing : (v.last ? COPY.compared(v.last.finished_at) : COPY.never),
|
|
476
|
-
button({ label: '↻', variant: 'ghost', size: 'sm', disabled: running || !all,
|
|
477
|
-
attrs: { 'data-docs-act': 'check', 'aria-label': COPY.compareAll, title: COPY.compareAll }, onClick: () => compareAll(inst) })))];
|
|
501
|
+
const out = [];
|
|
478
502
|
if (inst.filter === 'files') {
|
|
479
503
|
if (!lib) { out.push(el('p', { class: 'rp-section__empty' + (inst.libError ? ' rp-section__empty--bad' : '') }, inst.libError || COPY.readingFiles)); return out; }
|
|
480
504
|
out.push(el('div', { class: 'rp-docs-folds' }, byCategory(allEntries(inst)).map(([k, es]) => section(inst, k, es, { foldable: true }))));
|
|
@@ -498,8 +522,9 @@ function draw(inst) {
|
|
|
498
522
|
if (fp === inst.last && root.childNodes.length && root.contains(inst.body)) return;
|
|
499
523
|
inst.last = fp;
|
|
500
524
|
const head = header(inst);
|
|
525
|
+
updateHead(inst);
|
|
501
526
|
if (head.parentNode !== root || inst.body.parentNode !== root) root.replaceChildren(head, inst.body);
|
|
502
|
-
if (!inst.loaded) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty' }, COPY.loading)); return; }
|
|
503
|
-
if (inst.error) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, inst.error)); return; }
|
|
504
|
-
inst.body.replaceChildren(
|
|
527
|
+
if (!inst.loaded) { inst.body.replaceChildren(panel(inst, [el('p', { class: 'rp-section__empty' }, COPY.loading)])); return; }
|
|
528
|
+
if (inst.error) { inst.body.replaceChildren(panel(inst, [el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, inst.error)])); return; }
|
|
529
|
+
inst.body.replaceChildren(panel(inst, body(inst)));
|
|
505
530
|
}
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
body > [data-pm-theme] [data-dest="initiatives"] [data-in-nowline],
|
|
265
265
|
body > [data-pm-theme] [data-dest="initiatives"] [data-in-hint],
|
|
266
266
|
body > [data-pm-theme] [data-dest="initiatives"] .in-grip,
|
|
267
|
-
body > [data-pm-theme] [data-dest="board"] .bo-
|
|
267
|
+
body > [data-pm-theme] [data-dest="board"] .bo-head .td-count,
|
|
268
268
|
body > [data-pm-theme] [data-dest="docs"] .rp-docs-top .rp-head__title,
|
|
269
269
|
body[data-ph-view="cards"] > [data-pm-theme] [data-dest="docs"] .rp-docs-top,
|
|
270
270
|
body[data-ph-view="cards"] > [data-pm-theme] [data-dest="docs"] .rp-docs-views,
|
|
@@ -274,12 +274,12 @@ export function row({ title, titleIcon, meta, signals, lead, trail, onOpen, open
|
|
|
274
274
|
trail && trail.length ? el('span', { class: 'pmc-row__trail' }, trail) : null);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
/** card({ title, titleIcon?, meta?, signals?, lead?, trail?, onOpen, openLabel?, attrs? }): a raised card with one open button. */
|
|
278
|
-
export function card({ title, titleIcon, meta, signals, lead, trail, onOpen, openLabel, muted, attrs, openAttrs } = {}) {
|
|
277
|
+
/** card({ title, titleIcon?, meta?, signals?, lead?, trail?, onOpen, openLabel?, center?, attrs? }): a raised card with one open button. */
|
|
278
|
+
export function card({ title, titleIcon, meta, signals, lead, trail, onOpen, openLabel, muted, center, attrs, openAttrs } = {}) {
|
|
279
279
|
// the open button holds the title and the meta line, as a row's does: one column, and one tall target on a phone
|
|
280
280
|
const open = el('button', { type: 'button', class: 'pmc-card__open', 'aria-label': openLabel || null, onClick: onOpen, ...(openAttrs || {}) },
|
|
281
281
|
titleLine('pmc-card__title', title, titleIcon), metaLine('pmc-card__meta', meta, signals));
|
|
282
|
-
return el('div', withClass(cx('pmc-card', muted && 'pmc-card--muted'), attrs),
|
|
282
|
+
return el('div', withClass(cx('pmc-card', muted && 'pmc-card--muted', center && 'pmc-card--center'), attrs),
|
|
283
283
|
el('div', { class: 'pmc-card__top' },
|
|
284
284
|
lead && lead.length ? el('span', { class: 'pmc-card__lead' }, lead) : null,
|
|
285
285
|
open,
|