create-pmos 0.52.0 → 0.53.1
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 +2 -2
- package/payload/mods/pmos-cockpit/README.md +13 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
- package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/plan-items-stage.js +271 -0
- package/payload/scripts/pmos-log.sh +18 -7
- package/payload/scripts/workflow-state.js +28 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -0
- package/workspace/REMOTE.md +24 -63
- package/workspace/bin/pmos.js +19 -3
- package/workspace/cut-stub.js +40 -0
- package/workspace/first-paint-audit.js +145 -0
- package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
- package/workspace/fixtures/initiatives-api/list.json +176 -0
- package/workspace/fixtures/initiatives-api/post-403.json +5 -0
- package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
- package/workspace/fixtures/initiatives-api/sheet.json +112 -0
- package/workspace/fixtures/report-api/draft.json +177 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
- package/workspace/fixtures/today-api/provider-rows.json +204 -0
- package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
- package/workspace/guide/board.md +81 -0
- package/workspace/guide/breakdown.md +154 -0
- package/workspace/guide/command-palette.md +75 -0
- package/workspace/guide/index.md +28 -17
- package/workspace/guide/initiatives-roadmap.md +130 -0
- package/workspace/guide/initiatives-screen.md +89 -0
- package/workspace/guide/initiatives-timeline.md +57 -0
- package/workspace/guide/integrated-work.md +94 -0
- package/workspace/guide/knowledge-graph.md +3 -68
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +102 -32
- package/workspace/guide/live-notes.md +3 -3
- package/workspace/guide/pm-only-acts.md +89 -0
- package/workspace/guide/pm-only-lines.md +61 -0
- package/workspace/guide/proactive-engine.md +21 -5
- package/workspace/guide/reports-page.md +80 -0
- package/workspace/guide/reports.md +73 -0
- package/workspace/guide/timeline.md +70 -0
- package/workspace/guide/today.md +85 -0
- package/workspace/guide/validate-with-typesafe.md +1 -1
- package/workspace/lib/actor.js +91 -0
- package/workspace/lib/board-cache.js +32 -2
- package/workspace/lib/board-cli.js +108 -0
- package/workspace/lib/board-fold.js +232 -0
- package/workspace/lib/breakdown-export.js +75 -0
- package/workspace/lib/brief.js +20 -2
- package/workspace/lib/capture.js +98 -0
- package/workspace/lib/engine/plan-items-stage.js +271 -0
- package/workspace/lib/engine/workflow-state.js +28 -2
- package/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/goals.js +165 -0
- package/workspace/lib/grant-mint.js +113 -0
- package/workspace/lib/health-rules-factory.js +92 -0
- package/workspace/lib/health-rules-plan.js +164 -0
- package/workspace/lib/health.js +79 -0
- package/workspace/lib/initiative-cli.js +222 -0
- package/workspace/lib/initiative-display.js +183 -0
- package/workspace/lib/initiative-needs.js +127 -0
- package/workspace/lib/initiative-rows.js +210 -0
- package/workspace/lib/initiative-writer.js +141 -0
- package/workspace/lib/issues.js +206 -0
- package/workspace/lib/item-cli.js +195 -0
- package/workspace/lib/item-spans.js +137 -0
- package/workspace/lib/launcher-cli.js +207 -0
- package/workspace/lib/measure-cli.js +100 -0
- package/workspace/lib/measurements.js +189 -0
- package/workspace/lib/mobile-nav.js +57 -53
- package/workspace/lib/needs-providers.js +120 -0
- package/workspace/lib/placements.js +440 -0
- package/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/plan-ledgers.js +85 -0
- package/workspace/lib/plan-rules.js +72 -0
- package/workspace/lib/plan-tree.js +380 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +69 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +9 -6
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +93 -1
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +578 -0
- package/workspace/lib/report-needs.js +213 -0
- package/workspace/lib/route-modules.js +128 -0
- package/workspace/lib/routes-board.js +91 -0
- package/workspace/lib/routes-capture.js +52 -0
- package/workspace/lib/routes-initiatives.js +371 -0
- package/workspace/lib/routes-reports.js +163 -0
- package/workspace/lib/routes-timeline.js +197 -0
- package/workspace/lib/routes-today.js +32 -0
- package/workspace/lib/today-cli.js +99 -0
- package/workspace/lib/today.js +333 -0
- package/workspace/lib/triage.js +163 -0
- package/workspace/lib/verify-work-cli.js +81 -0
- package/workspace/lib/work-check-needs.js +99 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +4 -5
- package/workspace/public/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +40 -19
- package/workspace/public/pm/app.js +330 -30
- package/workspace/public/pm/bd-board.js +113 -0
- package/workspace/public/pm/bd-drag.js +59 -0
- package/workspace/public/pm/bd-model.js +152 -0
- package/workspace/public/pm/bd-timeline.js +233 -0
- package/workspace/public/pm/bd-tree.js +244 -0
- package/workspace/public/pm/board-copy.js +57 -0
- package/workspace/public/pm/breakdown-api.js +58 -0
- package/workspace/public/pm/breakdown-copy.js +138 -0
- package/workspace/public/pm/components.css +3135 -0
- package/workspace/public/pm/cycle-review.js +125 -0
- package/workspace/public/pm/data.js +40 -187
- package/workspace/public/pm/dest-board.js +438 -0
- package/workspace/public/pm/dest-initiatives.js +134 -0
- package/workspace/public/pm/dest-reports.js +270 -0
- package/workspace/public/pm/dest-settings.js +217 -0
- package/workspace/public/pm/dest-today.js +182 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +175 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +99 -0
- package/workspace/public/pm/handoff-box.js +82 -0
- package/workspace/public/pm/http.js +88 -0
- package/workspace/public/pm/icons.js +53 -0
- package/workspace/public/pm/initiative-list.js +218 -0
- package/workspace/public/pm/initiative-sheet.js +261 -0
- package/workspace/public/pm/initiatives-api.js +59 -0
- package/workspace/public/pm/initiatives-copy.js +215 -0
- package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
- package/workspace/public/pm/initiatives-timeline.js +410 -0
- package/workspace/public/pm/initiatives-ui.js +123 -0
- package/workspace/public/pm/item-sheet.js +251 -0
- package/workspace/public/pm/keep-card.js +204 -0
- package/workspace/public/pm/lifecycle.js +7 -1
- package/workspace/public/pm/mobile.css +49 -20
- package/workspace/public/pm/mobile.js +157 -29
- package/workspace/public/pm/modes-live.js +7 -883
- package/workspace/public/pm/modes.js +32 -1024
- package/workspace/public/pm/new-initiative-sheet.js +74 -0
- package/workspace/public/pm/next-step-ui.js +3 -2
- package/workspace/public/pm/palette-copy.js +51 -0
- package/workspace/public/pm/palette.js +312 -0
- package/workspace/public/pm/panel-breakdown.js +702 -0
- package/workspace/public/pm/panel-cockpit.js +63 -295
- package/workspace/public/pm/panel-detail.js +25 -172
- package/workspace/public/pm/panel-initiatives.js +1 -19
- package/workspace/public/pm/panel-repos.js +31 -81
- package/workspace/public/pm/panel-tabs.js +200 -111
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/portfolio-report.js +61 -0
- package/workspace/public/pm/primitives.js +656 -0
- package/workspace/public/pm/print.css +33 -0
- package/workspace/public/pm/provider-cards.js +136 -0
- package/workspace/public/pm/registry.js +194 -0
- package/workspace/public/pm/release-card.js +110 -0
- package/workspace/public/pm/report-view.js +309 -0
- package/workspace/public/pm/reports-copy.js +114 -0
- package/workspace/public/pm/roadmap.js +271 -0
- package/workspace/public/pm/sig-watch.js +41 -0
- package/workspace/public/pm/state.js +8 -4
- package/workspace/public/pm/status-banner.js +13 -5
- package/workspace/public/pm/tab-doc.js +2 -4
- package/workspace/public/pm/tab-term.js +12 -2
- package/workspace/public/pm/tab-web.js +3 -2
- package/workspace/public/pm/timeline-copy.js +48 -0
- package/workspace/public/pm/today-cards.js +244 -0
- package/workspace/public/pm/today-copy.js +126 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/public/pm/tokens.css +16 -18
- package/workspace/public/pm/topbar.js +102 -0
- package/workspace/public/pm/triage-card.js +60 -0
- package/workspace/public/pm/undo-hold.js +103 -0
- package/workspace/public/pm/view-addrepo.js +2 -1
- package/workspace/public/pm/view-livenotes.js +3 -1
- package/workspace/public/pm/view-today.js +7 -162
- package/workspace/public/vendor/Sortable.min.js +2 -0
- package/workspace/public/vendor/lucide-sprite.js +79 -0
- package/workspace/replaced-tests.json +2877 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +446 -40
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +21 -3
- package/workspace/test-lists/base.json +6 -0
- package/workspace/test-lists/run2-a.json +6 -0
- package/workspace/test-lists/run2-b.json +13 -0
- package/workspace/test-lists/run2-c.json +19 -0
- package/workspace/test-lists/run2-d.json +22 -0
- package/workspace/test-lists/run3-0.json +5 -0
- package/workspace/test-lists/run3-a.json +5 -0
- package/workspace/test-lists/run3-b.json +6 -0
- package/workspace/test-lists/run3-c.json +15 -0
- package/workspace/test-lists/run3-d.json +6 -0
- package/workspace/test-lists/run4-a.json +8 -0
- package/workspace/test-lists/run4-b.json +6 -0
- package/workspace/test-lists/run4-c.json +13 -0
- package/workspace/test-lists/run4-d.json +9 -0
- package/workspace/test-lists/run5-a.json +6 -0
- package/workspace/test-lists/run5-b.json +6 -0
- package/workspace/test-lists/run5-c.json +15 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -0
- package/workspace/test-run.js +27 -1
- package/workspace/third-party/README.md +44 -0
- package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
- package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
- package/workspace/design-parity.js +0 -673
- package/workspace/public/pm/tab-task.js +0 -86
- package/workspace/public/pm/view-guide.js +0 -141
- package/workspace/public/pm/view-knowledge.js +0 -225
- package/workspace/public/pm/view-memory.js +0 -122
- package/workspace/public/pm/view-portfolio.js +0 -153
- package/workspace/public/pm/view-reports.js +0 -190
- package/workspace/public/pm/view-scheduled.js +0 -161
- 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
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
19
|
+
import { repoRow } from './fixture.js';
|
|
24
20
|
import {
|
|
25
|
-
repoTuples, activeProduct, selectRepo,
|
|
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 `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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:
|
|
41
|
-
|
|
42
|
-
|
|
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,
|
|
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) => (
|
|
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
|
-
/**
|
|
84
|
-
|
|
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'
|
|
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',
|
|
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:
|
|
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
|
-
/**
|
|
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
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
197
|
+
class: 'lg-rail-tile',
|
|
223
198
|
},
|
|
224
199
|
repoInitial(rp.name),
|
|
225
|
-
needs ? el('i', { 'data-rail': 'needs-dot',
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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(--
|
|
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 [
|
|
344
|
-
const
|
|
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', {
|
|
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
|
|
489
|
-
|
|
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
|
-
/* ──
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
}
|