create-pmos 0.51.0 → 0.53.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.
Files changed (212) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/README.md +13 -0
  4. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
  6. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/plan-items-stage.js +271 -0
  11. package/payload/scripts/pmos-log.sh +18 -7
  12. package/payload/scripts/workflow-state.js +28 -2
  13. package/payload/state/discoveries.md +1 -1
  14. package/payload/state/log.md +1 -1
  15. package/workspace/bin/pmos.js +15 -1
  16. package/workspace/cut-stub.js +40 -0
  17. package/workspace/first-paint-audit.js +145 -0
  18. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  19. package/workspace/fixtures/initiatives-api/list.json +176 -0
  20. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  21. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  22. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  23. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  24. package/workspace/fixtures/report-api/draft.json +177 -0
  25. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  26. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  27. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  28. package/workspace/guide/board.md +81 -0
  29. package/workspace/guide/breakdown.md +154 -0
  30. package/workspace/guide/command-palette.md +75 -0
  31. package/workspace/guide/index.md +27 -17
  32. package/workspace/guide/initiatives-roadmap.md +130 -0
  33. package/workspace/guide/initiatives-screen.md +89 -0
  34. package/workspace/guide/initiatives-timeline.md +57 -0
  35. package/workspace/guide/integrated-work.md +94 -0
  36. package/workspace/guide/knowledge-graph.md +3 -68
  37. package/workspace/guide/layout.md +98 -32
  38. package/workspace/guide/live-notes.md +3 -3
  39. package/workspace/guide/pm-only-acts.md +89 -0
  40. package/workspace/guide/pm-only-lines.md +61 -0
  41. package/workspace/guide/reports-page.md +80 -0
  42. package/workspace/guide/reports.md +73 -0
  43. package/workspace/guide/timeline.md +70 -0
  44. package/workspace/guide/today.md +85 -0
  45. package/workspace/guide/validate-with-typesafe.md +1 -1
  46. package/workspace/lib/actor.js +91 -0
  47. package/workspace/lib/board-cache.js +32 -2
  48. package/workspace/lib/board-cli.js +108 -0
  49. package/workspace/lib/board-fold.js +232 -0
  50. package/workspace/lib/breakdown-export.js +80 -0
  51. package/workspace/lib/capture.js +98 -0
  52. package/workspace/lib/engine/plan-items-stage.js +271 -0
  53. package/workspace/lib/engine/workflow-state.js +28 -2
  54. package/workspace/lib/factory-join.js +158 -0
  55. package/workspace/lib/grant-mint.js +113 -0
  56. package/workspace/lib/health-rules-factory.js +92 -0
  57. package/workspace/lib/health-rules-plan.js +164 -0
  58. package/workspace/lib/health.js +79 -0
  59. package/workspace/lib/initiative-cli.js +222 -0
  60. package/workspace/lib/initiative-display.js +183 -0
  61. package/workspace/lib/initiative-needs.js +127 -0
  62. package/workspace/lib/initiative-rows.js +206 -0
  63. package/workspace/lib/initiative-writer.js +141 -0
  64. package/workspace/lib/issues.js +206 -0
  65. package/workspace/lib/item-cli.js +195 -0
  66. package/workspace/lib/item-spans.js +137 -0
  67. package/workspace/lib/measure-cli.js +100 -0
  68. package/workspace/lib/measurements.js +189 -0
  69. package/workspace/lib/mobile-nav.js +57 -53
  70. package/workspace/lib/needs-providers.js +120 -0
  71. package/workspace/lib/placements.js +440 -0
  72. package/workspace/lib/plan-items.js +922 -0
  73. package/workspace/lib/plan-ledgers.js +85 -0
  74. package/workspace/lib/plan-rules.js +72 -0
  75. package/workspace/lib/plan-tree.js +367 -0
  76. package/workspace/lib/pm-guard.js +174 -0
  77. package/workspace/lib/pm-lines-needs.js +331 -0
  78. package/workspace/lib/pmos-verbs.js +66 -0
  79. package/workspace/lib/proactive.js +7 -4
  80. package/workspace/lib/report-cli.js +152 -0
  81. package/workspace/lib/report-draft.js +579 -0
  82. package/workspace/lib/report-needs.js +213 -0
  83. package/workspace/lib/route-modules.js +128 -0
  84. package/workspace/lib/routes-board.js +91 -0
  85. package/workspace/lib/routes-capture.js +52 -0
  86. package/workspace/lib/routes-initiatives.js +371 -0
  87. package/workspace/lib/routes-reports.js +163 -0
  88. package/workspace/lib/routes-timeline.js +197 -0
  89. package/workspace/lib/routes-today.js +32 -0
  90. package/workspace/lib/today-cli.js +99 -0
  91. package/workspace/lib/today.js +333 -0
  92. package/workspace/lib/triage.js +163 -0
  93. package/workspace/lib/verify-work-cli.js +81 -0
  94. package/workspace/lib/work-check-needs.js +99 -0
  95. package/workspace/package-lock.json +1 -1
  96. package/workspace/package.json +5 -5
  97. package/workspace/public/index.html +29 -19
  98. package/workspace/public/pm/app.js +299 -30
  99. package/workspace/public/pm/bd-board.js +113 -0
  100. package/workspace/public/pm/bd-drag.js +59 -0
  101. package/workspace/public/pm/bd-model.js +152 -0
  102. package/workspace/public/pm/bd-timeline.js +233 -0
  103. package/workspace/public/pm/bd-tree.js +244 -0
  104. package/workspace/public/pm/board-copy.js +57 -0
  105. package/workspace/public/pm/breakdown-api.js +58 -0
  106. package/workspace/public/pm/breakdown-copy.js +138 -0
  107. package/workspace/public/pm/components.css +3054 -0
  108. package/workspace/public/pm/cycle-review.js +125 -0
  109. package/workspace/public/pm/data.js +40 -187
  110. package/workspace/public/pm/dest-board.js +438 -0
  111. package/workspace/public/pm/dest-initiatives.js +134 -0
  112. package/workspace/public/pm/dest-reports.js +270 -0
  113. package/workspace/public/pm/dest-settings.js +217 -0
  114. package/workspace/public/pm/dest-today.js +176 -0
  115. package/workspace/public/pm/dialogs.js +88 -0
  116. package/workspace/public/pm/display-word.js +56 -0
  117. package/workspace/public/pm/dock.js +105 -0
  118. package/workspace/public/pm/fixture.js +8 -227
  119. package/workspace/public/pm/frame-copy.js +94 -0
  120. package/workspace/public/pm/handoff-box.js +82 -0
  121. package/workspace/public/pm/http.js +88 -0
  122. package/workspace/public/pm/icons.js +53 -0
  123. package/workspace/public/pm/initiative-list.js +218 -0
  124. package/workspace/public/pm/initiative-sheet.js +261 -0
  125. package/workspace/public/pm/initiatives-api.js +59 -0
  126. package/workspace/public/pm/initiatives-copy.js +215 -0
  127. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  128. package/workspace/public/pm/initiatives-timeline.js +410 -0
  129. package/workspace/public/pm/initiatives-ui.js +123 -0
  130. package/workspace/public/pm/item-sheet.js +251 -0
  131. package/workspace/public/pm/keep-card.js +204 -0
  132. package/workspace/public/pm/lifecycle.js +7 -1
  133. package/workspace/public/pm/mobile.css +49 -20
  134. package/workspace/public/pm/mobile.js +157 -29
  135. package/workspace/public/pm/modes-live.js +7 -883
  136. package/workspace/public/pm/modes.js +32 -1024
  137. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  138. package/workspace/public/pm/next-step-ui.js +3 -2
  139. package/workspace/public/pm/palette-copy.js +51 -0
  140. package/workspace/public/pm/palette.js +312 -0
  141. package/workspace/public/pm/panel-breakdown.js +702 -0
  142. package/workspace/public/pm/panel-cockpit.js +63 -295
  143. package/workspace/public/pm/panel-detail.js +25 -172
  144. package/workspace/public/pm/panel-initiatives.js +1 -19
  145. package/workspace/public/pm/panel-repos.js +31 -81
  146. package/workspace/public/pm/panel-tabs.js +197 -110
  147. package/workspace/public/pm/panel-trees.js +5 -2
  148. package/workspace/public/pm/portfolio-report.js +61 -0
  149. package/workspace/public/pm/primitives.js +656 -0
  150. package/workspace/public/pm/print.css +33 -0
  151. package/workspace/public/pm/provider-cards.js +136 -0
  152. package/workspace/public/pm/registry.js +194 -0
  153. package/workspace/public/pm/release-card.js +110 -0
  154. package/workspace/public/pm/report-view.js +309 -0
  155. package/workspace/public/pm/reports-copy.js +114 -0
  156. package/workspace/public/pm/roadmap.js +271 -0
  157. package/workspace/public/pm/sig-watch.js +41 -0
  158. package/workspace/public/pm/state.js +8 -4
  159. package/workspace/public/pm/status-banner.js +13 -5
  160. package/workspace/public/pm/tab-doc.js +2 -4
  161. package/workspace/public/pm/tab-term.js +12 -2
  162. package/workspace/public/pm/tab-web.js +3 -2
  163. package/workspace/public/pm/timeline-copy.js +48 -0
  164. package/workspace/public/pm/today-cards.js +244 -0
  165. package/workspace/public/pm/today-copy.js +97 -0
  166. package/workspace/public/pm/tokens.css +16 -18
  167. package/workspace/public/pm/topbar.js +102 -0
  168. package/workspace/public/pm/triage-card.js +60 -0
  169. package/workspace/public/pm/undo-hold.js +103 -0
  170. package/workspace/public/pm/view-addrepo.js +2 -1
  171. package/workspace/public/pm/view-livenotes.js +3 -1
  172. package/workspace/public/pm/view-today.js +7 -162
  173. package/workspace/public/vendor/Sortable.min.js +2 -0
  174. package/workspace/public/vendor/lucide-sprite.js +79 -0
  175. package/workspace/replaced-tests.json +2864 -0
  176. package/workspace/run-tests.js +117 -6
  177. package/workspace/server.js +312 -36
  178. package/workspace/sheet-answer.js +61 -0
  179. package/workspace/skills/measure.skill +41 -0
  180. package/workspace/skills/report-biweekly.skill +23 -25
  181. package/workspace/skills/tracker-refresh.skill +50 -0
  182. package/workspace/test-files.json +20 -4
  183. package/workspace/test-lists/base.json +6 -0
  184. package/workspace/test-lists/run2-a.json +6 -0
  185. package/workspace/test-lists/run2-b.json +13 -0
  186. package/workspace/test-lists/run2-c.json +19 -0
  187. package/workspace/test-lists/run2-d.json +22 -0
  188. package/workspace/test-lists/run3-0.json +5 -0
  189. package/workspace/test-lists/run3-a.json +5 -0
  190. package/workspace/test-lists/run3-b.json +6 -0
  191. package/workspace/test-lists/run3-c.json +15 -0
  192. package/workspace/test-lists/run3-d.json +6 -0
  193. package/workspace/test-lists/run4-a.json +8 -0
  194. package/workspace/test-lists/run4-b.json +6 -0
  195. package/workspace/test-lists/run4-c.json +13 -0
  196. package/workspace/test-lists/run4-d.json +9 -0
  197. package/workspace/test-lists/run5-a.json +6 -0
  198. package/workspace/test-lists/run5-b.json +6 -0
  199. package/workspace/test-lists/run5-c.json +15 -0
  200. package/workspace/test-run.js +27 -1
  201. package/workspace/third-party/README.md +44 -0
  202. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  203. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  204. package/workspace/design-parity.js +0 -673
  205. package/workspace/public/pm/tab-task.js +0 -86
  206. package/workspace/public/pm/view-guide.js +0 -141
  207. package/workspace/public/pm/view-knowledge.js +0 -225
  208. package/workspace/public/pm/view-memory.js +0 -122
  209. package/workspace/public/pm/view-portfolio.js +0 -153
  210. package/workspace/public/pm/view-reports.js +0 -190
  211. package/workspace/public/pm/view-scheduled.js +0 -161
  212. package/workspace/public/pm/view-skills.js +0 -216
@@ -1,68 +1,53 @@
1
- /* pm/panel-cockpit.js — the cockpit spine: the repository rail, the work list, the menu and the view tabs.
1
+ /* pm/panel-cockpit.js — the cockpit spine of the four-panel shell: the repository rail, the work list and the `view` tab kind.
2
2
  *
3
- * This is PRD workspace-cockpit-nav, Run 1, from the PM's mockup (planning/design/cockpit-mockup/, option A). On a desktop the shell
4
- * is four hosts in a row: a 64px rail of repositories, a list of the selected repository's initiatives, the detail, and the
5
- * tabs. This module draws the first two, the menu that hangs off the rail, and the `view` tab kind. Nothing here owns the
6
- * detail or the tabs, and nothing here touches a terminal.
3
+ * The four-panel shell is what fixture mode and `?layout=legacy` draw; the default shell is the frame (app.js, topbar.js,
4
+ * dock.js). After the R29 cuts (pm-os-absorb Run 3, slice 3D) this module draws the rail of repositories, the list of the
5
+ * selected repository's initiatives, the breadcrumb, the palette's rows and the `view` tab kind. Nothing here owns the detail or
6
+ * the tabs, and nothing here touches a terminal.
7
7
  *
8
8
  * ── The one rule ────────────────────────────────────────────────────────────────────────────────
9
- * A tool never replaces the list. Knowledge, memory, skills, scheduled tasks, validate, the guide, today and all repos
10
- * open as a `view` tab beside the sessions and are opened once, never twice. The list and the detail stay where they are.
11
- * `VIEWS` is the table of those tools, and a list mode that is neither in it nor in `NOT_A_VIEW` fails a test, so a new
12
- * mode has to say where it opens.
13
- *
14
- * ── What is not here yet ─────────────────────────────────────────────────────────────────────────
15
- * The palette and Focus (Run 2), the redrawn views and the in-context items (Run 3), and the phone (Run 4). On a phone the
16
- * four hosts are still drawn by the older modules, and `state.mode` still drives the phone's lists; only a desktop is
17
- * handled here. Grants and Jira status are drawn in the mockup and hidden here until each has a backing (assumption 3).
9
+ * A tool never replaces the list. A tool (Today, Validate, Add repository, Live notes) opens as a `view` tab beside the sessions
10
+ * and is opened once, never twice. `VIEWS` is the table of those tools, and a list mode that is neither in it nor `product` fails a
11
+ * test, so a new mode has to say where it opens. Focus, Portfolio, Memory, Knowledge, the guide, Skills, Scheduled, Agents and
12
+ * Connect are gone (R29): Memory, Knowledge and the guide are ⌘K scopes, Skills start from + Session, the schedules are in
13
+ * Reports, and "All repositories" is a choice in Repository ▾.
18
14
  */
19
15
 
20
16
  import { el, setChildren } from './dom.js';
21
17
  import { state, setState, FIXTURE } from './state.js';
22
18
  import { currentProduct } from './board-read.js';
23
- import { repoRow, targetStepIndex } from './fixture.js';
19
+ import { repoRow } from './fixture.js';
24
20
  import {
25
- repoTuples, activeProduct, selectRepo, onAddProduct, manageParts, repoInitial, treesOf, UNBACKED_MODES, stateHealthBanner, renameRepo,
21
+ repoTuples, activeProduct, selectRepo, manageParts, repoInitial, treesOf, stateHealthBanner,
26
22
  } from './panel-repos.js';
27
23
  import {
28
- openTab, groupsFor, selectedSlug, attribution, workingSet, leadOf, orphanRows, searchHits, hitNode, fileHitsBlock,
24
+ openTab, openDoc, groupsFor, selectedSlug, attribution, workingSet, leadOf, orphanRows, searchHits, hitNode, fileHitsBlock,
29
25
  } from './panel-initiatives.js';
30
26
  import { records } from './board-read.js';
27
+ import { wordOf, toneOf, stageOf } from './display-word.js';
28
+ import './keep-card.js'; // registers the plan-rows Today source
29
+ import './portfolio-report.js'; // registers the three portfolio-report commands (R29: the Portfolio mode is cut)
30
+ import { scopeCtx } from './today-cards.js';
31
31
  import { filesToggle, renderFiles } from './view-files.js';
32
32
  import { openAddRepo } from './view-addrepo.js';
33
+ import { todayCount, commands, destinations, openDestination } from './registry.js'; // pm-os-absorb Runs 2–5
34
+ import { paintPalette } from './palette.js';
33
35
 
34
36
  /* ── the views: every tool that is a tab ──────────────────────────────────────────────────────── */
35
37
 
36
- /** The label is what the tab says after `view ·`. `scoped` views show one repository's data and are one tab per
37
- * repository; the others are the same everywhere. `menu` is the group a view is offered in, or null when the menu does not
38
- * list it (today and all repos are the switch above the list; the focus list is part of Today since Run 2). */
38
+ /** The label is what the tab says after `tool ·`. `scoped` views show one repository's data and are one tab per repository; the
39
+ * others are the same everywhere. `palette` marks the one ⌘K lists as a row of its own; the others are reached from the place that
40
+ * owns them (Validate and Add repository from Settings, Today from the frame's Today place). */
39
41
  export const VIEWS = {
40
- today: { label: 'today', mode: 'today', scoped: false, menu: null },
41
- portfolio: { label: 'all repos', mode: 'portfolio', scoped: false, menu: null },
42
- knowledge: { label: 'knowledge', mode: 'knowledge', scoped: true, menu: 'Library', hint: 'decisions, graph' },
43
- memory: { label: 'memory', mode: 'memory', scoped: true, menu: 'Library', hint: 'learned notes' },
44
- validate: { label: 'validate', mode: 'validate', scoped: true, menu: 'Library', hint: 'document health' },
45
- skills: { label: 'skills', mode: 'skills', scoped: false, menu: 'Automate', hint: 'approve, create' },
46
- scheduled: { label: 'scheduled', mode: 'scheduled', scoped: false, menu: 'Automate', hint: 'cadence tasks' },
47
- guide: { label: 'guide', mode: 'guide', scoped: false, menu: 'Help and settings', hint: 'the user guide' },
48
- // Run 3: views with no list mode — they draw only through their board module
49
- reports: { label: 'reports', mode: null, scoped: true, menu: 'Library', hint: 'run reports' },
50
- addrepo: { label: 'add repository', mode: null, scoped: false, menu: null },
42
+ today: { label: 'today', mode: null, scoped: false },
43
+ validate: { label: 'validate', mode: 'validate', scoped: true },
44
+ addrepo: { label: 'add repository', mode: null, scoped: false },
51
45
  // jev-live-notes Run 6: confirm each talking point as it arrives
52
- livenotes: { label: 'live notes', mode: null, scoped: true, menu: 'Library', hint: 'confirm talking points' },
53
- };
54
-
55
- /** Modes that exist in the code and are deliberately not views, with the reason. `product` is the list itself. */
56
- export const NOT_A_VIEW = {
57
- focus: 'folded into Today (Run 2): the mode opens the today view',
58
- product: 'the work list itself',
59
- workflow: 'decided by the agent at runtime, never configured by hand',
60
- agent: 'no server behind it yet, so it is not offered',
61
- connect: 'no server behind it yet, so it is not offered',
46
+ livenotes: { label: 'live notes', mode: null, scoped: true, palette: true, hint: 'confirm talking points' },
62
47
  };
63
48
 
64
- /** The view a list mode opens as, or null when the mode is not a view. */
65
- export const viewOfMode = (mode) => (mode === 'focus' ? 'today' : Object.prototype.hasOwnProperty.call(VIEWS, mode) ? mode : null);
49
+ /** The view a list mode opens as, or null when the mode is not a view. `product` is the list itself. */
50
+ export const viewOfMode = (mode) => (Object.prototype.hasOwnProperty.call(VIEWS, mode) ? mode : null);
66
51
 
67
52
  const viewId = (view, repo) => 'view:' + view + (VIEWS[view] && VIEWS[view].scoped && repo ? ':' + repo : '');
68
53
 
@@ -80,16 +65,13 @@ export function openView(view) {
80
65
  });
81
66
  }
82
67
 
83
- /** Is `view` offered here? In the live app a view whose mode has no server behind it is not. */
84
- const offered = (view) => FIXTURE || !UNBACKED_MODES.includes(VIEWS[view].mode);
85
-
86
- /** A desktop never shows a list mode in the list. Code that still says `setState({ mode: 'skills' })` is read here as
87
- * "open the skills view": the mode is put back to `product` and the view opens. Returns true when it did something. */
68
+ /** A desktop never shows a list mode in the list. Code that still says `setState({ mode: 'validate' })` is read here as
69
+ * "open the Validate view": the mode is put back to `product` and the view opens. Returns true when it did something. */
88
70
  export function absorbMode() {
89
71
  const m = state.mode;
90
72
  if (!m || m === 'product') return false;
91
73
  const view = viewOfMode(m);
92
- setState({ mode: 'product', settingsOpen: false, menuOpen: false });
74
+ setState({ mode: 'product' });
93
75
  if (view) openView(view);
94
76
  return true;
95
77
  }
@@ -110,9 +92,7 @@ function modes() {
110
92
  * `renderBoard(inner, ctx)`. `ctx.renderMode(host)` draws the view's existing list into `host`, so a board keeps every live read and
111
93
  * every `data-*` hook the suites use, and only adds the board's frame around it. A view with no board module draws as before. */
112
94
  export const BOARD_MODULES = {
113
- today: './view-today.js', portfolio: './view-portfolio.js', knowledge: './view-knowledge.js', memory: './view-memory.js',
114
- skills: './view-skills.js', scheduled: './view-scheduled.js', validate: './view-validate.js', guide: './view-guide.js',
115
- reports: './view-reports.js', addrepo: './view-addrepo.js', livenotes: './view-livenotes.js',
95
+ today: './view-today.js', validate: './view-validate.js', addrepo: './view-addrepo.js', livenotes: './view-livenotes.js',
116
96
  };
117
97
  const boardMods = {}; // view -> undefined (not tried) · null (loading or absent) · module
118
98
  function boardOf(view) {
@@ -144,7 +124,6 @@ export function mountView(host, tab) {
144
124
  } else if (boardMods[tab.view] === null) {
145
125
  // the board module is still loading: draw nothing rather than flash the old list
146
126
  } else if (m && typeof m.renderMode === 'function') {
147
- if (tab.view === 'today') inner.appendChild(todayBoard());
148
127
  const rest = el('div', { style: 'display:flex;flex-direction:column;gap:2px' });
149
128
  inner.appendChild(rest);
150
129
  renderMode(rest);
@@ -153,16 +132,13 @@ export function mountView(host, tab) {
153
132
  return host;
154
133
  }
155
134
 
156
- /** Run 2's Today board, exported so the Run 3 Today module can reuse its derived parts. */
157
- export { todayBoard };
158
-
159
135
  /* ── small shared pieces ──────────────────────────────────────────────────────────────────────── */
160
136
 
161
137
  const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;color:var(--foreground);white-space:nowrap;flex:none';
162
138
  const BADGE_TONE = {
163
139
  d: 'border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
164
140
  w: 'border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
165
- s: 'border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
141
+ s: 'border-color:transparent;background:var(--secondary);color:var(--foreground)',
166
142
  };
167
143
  const badge = (text, tone) => el('span', { style: BADGE + (tone ? ';' + BADGE_TONE[tone] : '') }, text);
168
144
  const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
@@ -179,13 +155,15 @@ function statusKey(repo, slug) {
179
155
  /** What a PM must close: an initiative that is blocked, or delivered and waiting for acceptance. */
180
156
  const NEEDS = ['needs-you', 'awaiting-acceptance'];
181
157
 
182
- /** How many initiatives, across every repository, are waiting on the person. Derived on each read, never stored. */
158
+ /**
159
+ * The number on the nav's Today button: the cards Today lists for the scope Today is in (the repository, or every one).
160
+ * Every source adds its own cards through todayCount() and Today's header adds the same sources over the same scope, so the
161
+ * two cannot disagree (F8). The breakdown rows are read once the first sessions poll has answered (the plan-rows source
162
+ * primes them), so the number does not wait for Today to open. Derived on each read, never stored.
163
+ */
183
164
  export function needsCount() {
184
- let n = 0;
185
- for (const repo of Object.keys(state.statuses || {})) {
186
- for (const slug of Object.keys(state.statuses[repo] || {})) if (NEEDS.includes(statusKey(repo, slug))) n++;
187
- }
188
- return n;
165
+ const sc = scopeCtx(state, repoTuples(), currentProduct());
166
+ return todayCount({ all: sc.all, repos: sc.repos, setState });
189
167
  }
190
168
 
191
169
  /** Does anything in `repo` need the person: a blocked or delivered initiative, or a session that asked for them. */
@@ -201,9 +179,8 @@ function repoNeeds(repo) {
201
179
  /* ── the rail ─────────────────────────────────────────────────────────────────────────────────── */
202
180
 
203
181
  const TILE = 'box-sizing:content-box;position:relative;flex:none;width:36px;height:36px;border-radius:10px;display:grid;grid-template:36px / 36px;place-items:center;font:600 13px var(--sans)';
204
- const SVG_MENU = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg>';
205
182
 
206
- /** The 64px column of repositories. One tile per repository, a dashed tile to add one, and the menu at the bottom. */
183
+ /** The 64px column of repositories. One tile per repository and a dashed tile to add one. */
207
184
  export function renderRail(host) {
208
185
  const tuples = repoTuples();
209
186
  const product = activeProduct(tuples);
@@ -215,14 +192,12 @@ export function renderRail(host) {
215
192
  const said = rp.name + ' — ' + rp.kindLabel + ' repository' + (needs ? ' · something in it needs you' : '');
216
193
  return el('button', {
217
194
  onClick: () => selectRepo(rp.name),
218
- // right-click renames from the rail, where the abbreviation is shown (overhaul-r15 R2)
219
- onContextmenu: (e) => { if (e && e.preventDefault) e.preventDefault(); renameRepo(rp.name); },
220
- title: said + ' · right-click to rename', 'aria-label': said, 'aria-pressed': on ? 'true' : 'false',
195
+ title: said, 'aria-label': said, 'aria-pressed': on ? 'true' : 'false',
221
196
  'data-rail': 'repo', 'data-r1': 'rail-glyph', 'data-ini': repoInitial(rp.name), 'data-repo': rp.name, 'data-active': on ? '1' : '0',
222
- style: TILE + ';' + (on ? 'background:var(--brand);color:#fff' : 'background:var(--secondary);color:var(--foreground)'),
197
+ class: 'lg-rail-tile',
223
198
  },
224
199
  repoInitial(rp.name),
225
- needs ? el('i', { 'data-rail': 'needs-dot', style: 'position:absolute;right:-3px;top:-3px;width:9px;height:9px;border-radius:50%;background:var(--warning);border:2px solid var(--sidebar)' }) : null,
200
+ needs ? el('i', { 'data-rail': 'needs-dot', class: 'lg-rail-tile__dot' }) : null,
226
201
  );
227
202
  });
228
203
  const hb = stateHealthBanner();
@@ -232,59 +207,9 @@ export function renderRail(host) {
232
207
  style: TILE + ';background:transparent;border:1px dashed var(--input);color:var(--muted-foreground)',
233
208
  hover: 'color:var(--foreground);background:var(--accent)',
234
209
  }, '+');
235
- const menu = el('button', {
236
- onClick: () => setState((s) => ({ menuOpen: !s.menuOpen })),
237
- title: 'menu', 'aria-label': 'menu', 'aria-expanded': state.menuOpen ? 'true' : 'false', 'data-rail': 'menu',
238
- style: TILE + ';margin-top:auto;background:' + (state.menuOpen ? 'var(--accent)' : 'transparent') + ';color:var(--foreground)',
239
- hover: 'background:var(--accent)',
240
- html: SVG_MENU,
241
- });
242
- /* The host is the mockup's rail (app.js gives it the centred column, 12px top and bottom, 10px between tiles), with the menu at the
243
- * foot. The tiles and the add tile scroll in their own column when there are more than fit, so the menu stays put; that column
244
- * starts 3px higher with 3px of padding, so a needs dot on the first tile is not clipped and the first tile still sits 12px down. */
245
- setChildren(host, [el('div', { 'data-rail': 'column', style: 'flex:0 1 auto;min-height:0;align-self:stretch;margin-top:-3px;padding-top:3px;display:flex;flex-direction:column;align-items:center;gap:10px;overflow-y:auto' }, stateHealth, tiles, add), el('div', { style: 'flex:none;margin-top:auto;display:flex;justify-content:center' }, menu)]);
246
- return host;
247
- }
248
-
249
- /* ── the menu ─────────────────────────────────────────────────────────────────────────────────── */
250
-
251
- const MENU_STYLE = 'position:fixed;left:72px;bottom:40px;width:330px;max-height:calc(100vh - 120px);overflow-y:auto;z-index:50;background:var(--card);border:1px solid var(--input);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.5);padding:6px';
252
- const MI = 'display:flex;align-items:center;gap:10px;width:100%;min-height:34px;padding:0 10px;border-radius:6px;font:500 13px var(--sans);text-align:left';
253
-
254
- /** The entries of the menu, in its three groups. A view with nothing behind it is left out, not greyed. */
255
- export function menuGroups() {
256
- const groups = [];
257
- for (const g of ['Library', 'Automate', 'Help and settings']) {
258
- const items = Object.keys(VIEWS).filter((k) => VIEWS[k].menu === g && offered(k)).map((k) => ({ view: k, label: VIEWS[k].label, hint: VIEWS[k].hint }));
259
- if (g === 'Help and settings') { items.push({ action: 'profile', label: 'claude profile', hint: '' }); items.push({ action: 'theme', label: 'theme', hint: state.theme }); }
260
- if (items.length) groups.push({ label: g + (g === 'Library' ? ' · ' + currentProduct() : g === 'Automate' ? ' · all repos' : ''), items });
261
- }
262
- return groups;
263
- }
264
-
265
- /** The menu and its click-away layer, drawn into the shell's own overlay host. Empty when the menu is shut. */
266
- export function renderMenu(host) {
267
- if (!state.menuOpen) { setChildren(host, []); return host; }
268
- const close = () => setState({ menuOpen: false });
269
- const parts = [];
270
- for (const g of menuGroups()) {
271
- parts.push(el('div', { style: 'padding:8px 10px 6px;' + LBL }, g.label));
272
- for (const it of g.items) {
273
- parts.push(el('button', {
274
- 'data-menu': it.view || it.action,
275
- onClick: () => {
276
- if (it.view) { setState({ menuOpen: false }); openView(it.view); }
277
- else if (it.action === 'theme') setState((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark', menuOpen: false }));
278
- else if (it.action === 'profile') setState({ menuOpen: false, detail: { kind: 'prof' }, col2: true });
279
- },
280
- style: MI + ';color:var(--foreground)', hover: 'background:var(--accent)',
281
- }, el('span', null, it.label), it.hint ? el('span', { style: 'margin-left:auto;font:11px var(--mono);color:var(--muted-foreground)' }, it.hint) : null));
282
- }
283
- }
284
- setChildren(host, [
285
- el('div', { 'data-menu': 'away', onClick: close, style: 'position:fixed;inset:0;z-index:49' }),
286
- el('div', { 'data-menu': 'panel', role: 'menu', style: MENU_STYLE }, parts),
287
- ]);
210
+ /* The host is the mockup's rail (app.js gives it the centred column, 12px top and bottom, 10px between tiles). The tiles and the
211
+ * add tile scroll in their own column when there are more than fit; that column starts 3px higher with 3px of padding, so a needs dot on the first tile is not clipped and the first tile still sits 12px down. */
212
+ setChildren(host, [el('div', { 'data-rail': 'column', class: 'lg-rail-col' }, stateHealth, tiles, add)]);
288
213
  return host;
289
214
  }
290
215
 
@@ -315,33 +240,17 @@ const GROUP_OF = {
315
240
  const GROUPS = [
316
241
  ['needs', 'Needs you'], ['progress', 'In progress'], ['done', 'Done'], ['hold', 'On hold'], ['unknown', 'Unknown'],
317
242
  ];
318
- const WORD = {
319
- 'needs-you': ['needs you', 'd'], 'awaiting-acceptance': ['accept', 'w'], 'in-progress': ['build', null], planning: ['plan', null],
320
- accepted: [null, null], closed: ['closed', null], cancelled: ['cancelled', null], 'on-hold': ['hold', null], unknown: ['unknown', 'd'],
321
- };
322
- /* Run 4: the badge names the RUNG the initiative is at, in the PM's word for it — the mockup's "review" on an initiative blocked in
323
- * eval, "accept" on one waiting at acceptance, "build" on one building. The rung is the same pointer the breadcrumb and the detail
324
- * use (`targetStepIndex`: the first blocked step, else the current one). An initiative whose ladder is not read yet keeps the status
325
- * word above, so a badge never names a rung nobody read. */
326
- const RUNG_WORD = { eval: 'review', accepted: 'accept' };
327
- function rungWord(slug) {
328
- const rec = records()[slug];
329
- const steps = rec && Array.isArray(rec.steps) ? rec.steps : null;
330
- const ti = steps ? targetStepIndex(steps) : -1;
331
- const k = ti >= 0 && steps[ti] ? String(steps[ti].k || '') : '';
332
- if (!k) return null;
333
- return RUNG_WORD[k] || (/^[a-z]+$/.test(k) ? k : null);
334
- }
335
243
  const DOT = {
336
- needs: 'var(--destructive)', progress: 'var(--info)', done: 'var(--success)', hold: 'var(--muted-foreground)', unknown: 'var(--destructive)',
244
+ needs: 'var(--destructive)', progress: 'var(--foreground)', done: 'var(--muted-foreground)', hold: 'var(--muted-foreground)', unknown: 'var(--destructive)',
337
245
  };
338
246
 
339
247
  const ITEM = 'display:flex;align-items:center;gap:10px;width:100%;min-height:40px;padding:0 10px;border-radius:8px;font:500 13px var(--sans);text-align:left';
340
248
 
341
249
  function row(slug, product, group, live, sel, detKind) {
342
250
  const key = statusKey(product, slug);
343
- const [word0, tone] = WORD[key] || WORD.unknown;
344
- const word = word0 && (group === 'needs' || group === 'progress') ? (rungWord(slug) || word0) : word0;
251
+ const srow = (state.statuses && state.statuses[product] && state.statuses[product][slug]) || null;
252
+ const tone = toneOf(srow);
253
+ const word = key === 'accepted' ? null : wordOf(srow, records()[slug]);
345
254
  const on = slug === sel && detKind === 'init';
346
255
  const [glyph, glyphColor, glyphTitle] = leadOf(slug, live);
347
256
  const liveNow = glyph !== '#';
@@ -362,8 +271,6 @@ function row(slug, product, group, live, sel, detKind) {
362
271
  );
363
272
  }
364
273
 
365
- const SEG = 'flex:1 1 0;text-align:center;padding:4px 0;font:13px var(--sans);line-height:1.45;';
366
-
367
274
  /** The list column: the repository, the three-way switch, the filter, the initiatives by state, and the way to add one. */
368
275
  export function renderWorkList(host) {
369
276
  const product = currentProduct();
@@ -388,18 +295,6 @@ export function renderWorkList(host) {
388
295
  el('span', { 'data-list': 'repo', style: 'font-weight:600;font-size:15px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, product || ''),
389
296
  rp ? badge(rp.kindLabel + ' · ' + all.length) : null);
390
297
 
391
- const act = (state.tabs || []).find((t) => t.id === state.active);
392
- const activeView = act && act.kind === 'view' && (act.view === 'today' || act.view === 'portfolio') ? act.view : null;
393
- const segBtn = (label, on, onClick, extra, attrName) => el('button', {
394
- onClick, 'data-switch': attrName, 'aria-pressed': on ? 'true' : 'false',
395
- style: SEG + (on ? 'border-radius:6px;background:var(--background);font-weight:500;color:var(--foreground)' : 'color:var(--muted-foreground)'),
396
- }, label, extra);
397
- const n = needsCount();
398
- const switcher = el('div', { 'data-list': 'switch', style: 'display:flex;gap:2px;background:var(--accent);border-radius:8px;padding:3px' },
399
- segBtn('Work', !activeView, () => { if (activeView) setState({ active: lastNonView() }); }, null, 'work'),
400
- segBtn(n ? 'Today ' : 'Today', activeView === 'today', () => openView('today'), n ? el('span', { style: 'color:var(--warning)' }, String(n)) : null, 'today'),
401
- segBtn('All repos', activeView === 'portfolio', () => openView('portfolio'), null, 'all'));
402
-
403
298
  const manage = manageParts(product, product);
404
299
  const manageRow = manage.tools.length || manage.below.length
405
300
  ? el('div', { 'data-list': 'manage', style: 'display:flex;flex-direction:column;margin:0 -12px' },
@@ -448,7 +343,7 @@ export function renderWorkList(host) {
448
343
  if (filesOn) renderFiles(filesHost);
449
344
  host.style.position = 'relative'; // anchors the fold control (foldList)
450
345
  setChildren(host, [
451
- el('div', { style: 'flex:none;display:flex;flex-direction:column;gap:12px' }, stateHealthBanner(), header, manageRow, switcher, FIXTURE ? null : filesToggle(), filesOn ? null : filterBox),
346
+ el('div', { class: 'lg-stack' }, stateHealthBanner(), header, manageRow, FIXTURE ? null : filesToggle(), filesOn ? null : filterBox),
452
347
  // the rows take their own height and scroll only when they outgrow the column; "New initiative" sits at the bottom either way
453
348
  filesOn ? filesHost : el('div', { 'data-list': 'rows', style: 'flex:0 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:2px' }, groupsNodes),
454
349
  el('div', { style: 'flex:none;margin-top:auto;display:flex;flex-direction:column;gap:8px' }, el('div', { style: 'height:1px;background:var(--border)' }), addInit),
@@ -469,12 +364,6 @@ function foldList() {
469
364
  }, '⟨');
470
365
  }
471
366
 
472
- /** The tab to show when the PM goes back to Work from a view: the last one that is not a view, else none. */
473
- function lastNonView() {
474
- const t = [...(state.tabs || [])].reverse().find((x) => x.kind !== 'view');
475
- return t ? t.id : null;
476
- }
477
-
478
367
  /* ── the breadcrumb ───────────────────────────────────────────────────────────────────────────── */
479
368
 
480
369
  /** repository › initiative › the active tab, as the nodes of the top bar. */
@@ -485,9 +374,8 @@ export function breadcrumb() {
485
374
  const slug = selectedSlug();
486
375
  if (slug && state.detail && state.detail.kind === 'init') parts.push(slug);
487
376
  // the step the initiative is at: the first blocked one, else the current one — the same pointer the detail's answer line uses
488
- const rec = slug && state.detail && state.detail.kind === 'init' ? records()[slug] : null;
489
- const ti = rec && Array.isArray(rec.steps) ? targetStepIndex(rec.steps) : -1;
490
- if (ti >= 0 && rec.steps[ti] && rec.steps[ti].k) parts.push(rec.steps[ti].k);
377
+ const stage = slug && state.detail && state.detail.kind === 'init' ? stageOf(records()[slug]) : null;
378
+ if (stage) parts.push(stage);
491
379
  const act = (state.tabs || []).find((t) => t.id === state.active);
492
380
  if (act && act.title) parts.push(act.title);
493
381
  const nodes = [];
@@ -496,28 +384,14 @@ export function breadcrumb() {
496
384
  const last = i === parts.length - 1;
497
385
  nodes.push(el('span', { style: last ? 'color:var(--foreground)' : '', 'data-crumb': last && act && act.title === p ? 'tab' : null }, p));
498
386
  });
499
- if (state.layoutFocus) {
500
- nodes.push(badge('Focus', 'w'));
501
- nodes.push(el('button', { 'data-crumb': 'navigate', onClick: leaveFocus, title: 'back to the rail and the list (Esc)',
502
- style: 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:6px;padding:0 8px;line-height:18px;color:var(--foreground)', hover: 'background:var(--accent)' }, 'Navigate'));
503
- }
504
387
  return nodes;
505
388
  }
506
389
 
507
390
 
508
- /* ── Focus (Run 2, R7) ────────────────────────────────────────────────────────────────────────── */
509
-
510
- /** Focus is a layout, not a record: it is never stored, and no host is created, moved or re-parented to enter it. */
511
- export function enterFocus() { if (!state.layoutFocus) setState({ layoutFocus: true, menuOpen: false, paletteOpen: false }); }
512
- export function leaveFocus() { if (state.layoutFocus) setState({ layoutFocus: false }); }
513
- export const toggleFocus = () => (state.layoutFocus ? leaveFocus() : enterFocus());
514
-
515
- /* ── the palette (Run 2, R6) ──────────────────────────────────────────────────────────────────── */
391
+ /* ── the palette (Run 2, R6; rebuilt in Run 3, slice 3C, as pm/palette.js) ────────────────────────
392
+ * This file owns what the palette can jump to. The rows, the scopes, Capture as note and Ask are pm/palette.js's. */
516
393
 
517
- let palQ = '';
518
- let palSel = 0;
519
-
520
- /** Everything the palette can jump to, in the order it lists them. Derived on each open, never stored. */
394
+ /** Everything the palette can jump to, in the order the base lists it. Derived on each open, never stored. */
521
395
  export function paletteItems() {
522
396
  const items = [];
523
397
  const tuples = repoTuples();
@@ -534,119 +408,13 @@ export function paletteItems() {
534
408
  items.push({ kind: t.kind === 'term' ? 'session' : 'tab', label: String(t.title || t.id).replace(/^tool · /, ''), hint: t.kind === 'term' ? 'session' : 'open tab',
535
409
  run: () => setState({ active: t.id }) });
536
410
  }
537
- for (const k of Object.keys(VIEWS)) if (offered(k)) items.push({ kind: 'view', label: VIEWS[k].label, hint: VIEWS[k].hint || 'view', run: () => openView(k) });
411
+ for (const k of Object.keys(VIEWS)) if (VIEWS[k].palette) items.push({ kind: 'view', label: VIEWS[k].label, hint: VIEWS[k].hint || 'view', run: () => openView(k) });
412
+ // pm-os-absorb Runs 2–5: the destinations that are built, then the commands each run registers (pm/registry.js)
413
+ for (const d of destinations()) items.push({ kind: 'destination', label: d.label, hint: d.key || 'open', run: () => openDestination(d.id) });
414
+ for (const c of commands()) items.push({ kind: 'command', label: c.label, hint: c.hint || (c.cli ? '' : 'command'), cli: c.cli || '', run: c.run });
538
415
  return items;
539
416
  }
540
417
 
541
- const palFilter = (items, q) => {
542
- const w = q.toLowerCase().split(/\s+/).filter(Boolean);
543
- // with nothing typed, every kind shows: a long list of initiatives must not push the views off the list
544
- if (!w.length) { const seen = {}; return items.filter((it) => (seen[it.kind] = (seen[it.kind] || 0) + 1) <= 6); }
545
- return items.filter((it) => w.every((x) => (it.label + ' ' + it.hint + ' ' + it.kind).toLowerCase().includes(x))).slice(0, 40);
546
- };
547
-
548
- /* The input is made once and kept: a repaint replaces the list under it, never the box being typed in, so focus and the caret survive. */
549
- let palItems = [];
550
- const palClose = () => setState({ paletteOpen: false });
551
- const palChoose = (it) => { if (!it) return; setState({ paletteOpen: false }); it.run(); };
552
- const palInput = el('input', {
553
- type: 'text', 'data-palette': 'input', placeholder: 'Jump to anything…', autocomplete: 'off', spellcheck: 'false',
554
- style: 'width:100%;box-sizing:border-box;font:14px var(--sans);padding:12px 14px;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--foreground);outline:none',
555
- onInput: (e) => { palQ = e.target.value; palSel = 0; setState({}); },
556
- onKeydown: (e) => {
557
- if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); palClose(); }
558
- else if (e.key === 'ArrowDown') { e.preventDefault(); palSel = Math.min(palItems.length - 1, palSel + 1); setState({}); }
559
- else if (e.key === 'ArrowUp') { e.preventDefault(); palSel = Math.max(0, palSel - 1); setState({}); }
560
- else if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); palChoose(palItems[palSel]); }
561
- },
562
- });
563
-
564
418
  export function renderPalette(host) {
565
- if (!state.paletteOpen) { palQ = ''; palSel = 0; palInput.value = ''; setChildren(host, []); return host; }
566
- const items = palItems = palFilter(paletteItems(), palQ);
567
- if (palSel >= items.length) palSel = Math.max(0, items.length - 1);
568
- if (palInput.value !== palQ) palInput.value = palQ;
569
- const rows = items.length ? items.map((it, i) => el('button', {
570
- 'data-palette': 'item', 'data-kind': it.kind, 'aria-selected': i === palSel ? 'true' : 'false', onClick: () => palChoose(it),
571
- style: 'display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:7px 14px;font-size:13px;background:' + (i === palSel ? 'var(--accent)' : 'transparent'),
572
- hover: 'background:var(--accent)',
573
- }, el('span', { style: LBL + ';width:78px;flex:none' }, it.kind), el('span', { style: 'flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, it.label),
574
- el('span', { style: 'color:var(--muted-foreground);font-size:11px;white-space:nowrap' }, it.hint)))
575
- : [el('div', { 'data-palette': 'empty', style: 'padding:14px;color:var(--muted-foreground);font-size:12px' }, 'Nothing matches "' + palQ + '".')];
576
- const fresh = !host.firstChild;
577
- setChildren(host, [el('div', { 'data-palette': 'overlay', onClick: (e) => { if (e.target === e.currentTarget) palClose(); },
578
- style: 'position:fixed;inset:0;z-index:60;background:oklch(0 0 0 / 45%);display:flex;justify-content:center;align-items:flex-start;padding-top:12vh' },
579
- el('div', { role: 'dialog', 'aria-label': 'Jump to anything', style: 'width:560px;max-width:calc(100vw - 32px);background:var(--popover, var(--card));color:var(--foreground);border:1px solid var(--border);border-radius:10px;overflow:hidden;box-shadow:0 20px 50px oklch(0 0 0 / 50%)' },
580
- palInput, el('div', { 'data-palette': 'list', style: 'max-height:50vh;overflow-y:auto;padding:4px 0' }, rows)))]);
581
- // the box is in the document now, so focus it at once: a key pressed straight after ⌘K must land in it
582
- if (fresh || document.activeElement !== palInput) { try { palInput.focus(); } catch { /* not mounted */ } }
583
- return host;
584
- }
585
-
586
- /* ── Today's board (Run 2, R9) ────────────────────────────────────────────────────────────────── */
587
-
588
- const DISMISS_REASONS = ['not now', 'wrong priority', 'already handled'];
589
-
590
- /** The initiatives waiting on the person, from derived status. `all` widens it from the current repository to every one. */
591
- export function needsCards(all) {
592
- const repos = all ? Object.keys(state.statuses || {}) : [currentProduct()].filter(Boolean);
593
- const out = [];
594
- for (const repo of repos) {
595
- for (const slug of Object.keys((state.statuses && state.statuses[repo]) || {})) {
596
- if (!NEEDS.includes(statusKey(repo, slug))) continue;
597
- if ((state.todayDismissed || {})[repo + '/' + slug]) continue;
598
- out.push({ repo, slug, key: statusKey(repo, slug) });
599
- }
600
- }
601
- return out;
602
- }
603
-
604
- function todayBoard() {
605
- const all = state.todayScope === 'all';
606
- const cards = needsCards(all);
607
- const repos = repoTuples();
608
- const hour = new Date().getHours();
609
- const greet = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
610
- const prepared = [];
611
- for (const t of repos) {
612
- if (!all && t[0] !== currentProduct()) continue;
613
- for (const slug of Object.keys((state.statuses && state.statuses[t[0]]) || {})) {
614
- if (statusKey(t[0], slug) === 'in-progress') prepared.push({ repo: t[0], slug });
615
- }
616
- }
617
- const toggle = el('div', { 'data-today': 'scope', style: 'display:flex;gap:2px;background:var(--accent);border-radius:8px;padding:3px' },
618
- ...[['repo', 'Repository'], ['all', 'All']].map(([k, label]) => el('button', {
619
- 'data-today-scope': k, 'aria-pressed': (k === 'all') === all ? 'true' : 'false',
620
- onClick: () => setState({ todayScope: k === 'all' ? 'all' : 'repo' }),
621
- style: 'font:12px var(--sans);padding:2px 10px;border-radius:6px;' + ((k === 'all') === all ? 'background:var(--background);font-weight:500;color:var(--foreground)' : 'color:var(--muted-foreground)'),
622
- }, label)));
623
- const card = (c) => el('div', { 'data-today': 'card', 'data-slug': c.slug, style: 'border:1px solid var(--border);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;background:var(--card)' },
624
- el('div', { style: 'display:flex;gap:8px;align-items:baseline' },
625
- el('span', { style: 'font-weight:600;font-size:13px;min-width:0;overflow:hidden;text-overflow:ellipsis' }, c.slug),
626
- el('span', { style: 'margin-left:auto;color:var(--muted-foreground);font-size:11px' }, c.repo)),
627
- badge(c.key === 'needs-you' ? 'needs you' : 'review', c.key === 'needs-you' ? 'd' : 'w'),
628
- el('div', { style: 'display:flex;gap:6px;flex-wrap:wrap;align-items:center' },
629
- el('button', { 'data-today': 'open', onClick: () => { selectRepo(c.repo); setState({ sel: c.slug, detail: { kind: 'init' }, col2: true }); },
630
- style: 'font:500 12px var(--sans);padding:3px 10px;border-radius:6px;background:var(--primary);color:var(--primary-foreground)' }, c.key === 'needs-you' ? 'Unblock' : 'Review'),
631
- el('span', { style: 'color:var(--muted-foreground);font-size:11px' }, 'dismiss:'),
632
- ...DISMISS_REASONS.map((r) => el('button', { 'data-today': 'dismiss', 'data-reason': r,
633
- onClick: () => setState((s) => ({ todayDismissed: { ...(s.todayDismissed || {}), [c.repo + '/' + c.slug]: r } })),
634
- style: 'font:11px var(--sans);padding:1px 8px;border:1px solid var(--border);border-radius:999px;color:var(--muted-foreground)', hover: 'color:var(--foreground)' }, r))));
635
- const col = (title, kids, attr) => el('div', { 'data-today': attr, style: 'flex:1;min-width:260px;display:flex;flex-direction:column;gap:8px' },
636
- el('div', { style: LBL }, title), ...kids);
637
- const empty = (t) => el('div', { style: 'color:var(--muted-foreground);font-size:12px' }, t);
638
- return el('div', { 'data-today': 'board', style: 'display:flex;flex-direction:column;gap:14px;padding-bottom:14px;margin-bottom:8px;border-bottom:1px solid var(--border)' },
639
- el('div', { style: 'display:flex;align-items:center;gap:12px' },
640
- el('div', { 'data-today': 'greeting', style: 'font-size:18px;font-weight:600' }, greet), toggle),
641
- el('div', { 'data-today': 'count', style: 'color:var(--muted-foreground);font-size:12px' },
642
- cards.length + (cards.length === 1 ? ' thing needs you' : ' things need you') + (all ? ' across ' + repos.length + ' repositories' : ' in ' + (currentProduct() || 'this repository'))
643
- + ' · ' + prepared.length + ' in progress'),
644
- el('div', { 'data-today': 'sources', style: 'color:var(--muted-foreground);font-size:11px' },
645
- 'Sources reached: ' + (FIXTURE ? 'the fixture records' : 'derived status of ' + Object.keys(state.statuses || {}).length + ' repositories') + '. No mail or calendar is read here.'),
646
- el('div', { style: 'display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start' },
647
- col('Needs you', cards.length ? cards.map(card) : [empty('Nothing is waiting on you.')], 'needs'),
648
- col('Prepared for you', prepared.length ? prepared.map((c) => el('button', { 'data-today': 'prepared', 'data-slug': c.slug,
649
- onClick: () => { selectRepo(c.repo); setState({ sel: c.slug, detail: { kind: 'init' }, col2: true }); },
650
- style: 'text-align:left;border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:13px;background:var(--card)', hover: 'background:var(--accent)' }, c.slug + ' · ' + c.repo)) : [empty('Nothing is being prepared.')], 'prepared'),
651
- col('Today', [empty('Events come from the morning brief; none was read into this view.')], 'events')));
419
+ return paintPalette(host, { rows: paletteItems, openView, openDoc, openTab });
652
420
  }