rvlib-shipkit 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* the planned page (planned-view.tsx): one quiet list, one font size. Each
|
|
2
|
+
row is a grid on the same three columns (when, project · check, mark), so
|
|
3
|
+
the columns line up without a header. The card on hover takes the pointer,
|
|
4
|
+
unlike .ftip, because it holds the run button. */
|
|
5
|
+
.planned{font-size:13px}
|
|
6
|
+
.plfind{display:block;width:240px;max-width:100%;box-sizing:border-box;margin-bottom:var(--s3);background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s1) var(--s2);font:inherit}
|
|
7
|
+
.plfind:focus{outline:none;border-color:var(--accent)}
|
|
8
|
+
.pllist{max-width:560px}
|
|
9
|
+
.plsec{margin-top:var(--s3);padding:var(--s1) var(--s2);color:var(--dim);text-transform:lowercase}
|
|
10
|
+
.plsec:first-child{margin-top:0}
|
|
11
|
+
.plr{display:grid;grid-template-columns:64px minmax(0,1fr) auto;align-items:center;column-gap:var(--s3);padding:var(--s0) var(--s2);border-radius:3px;white-space:nowrap}
|
|
12
|
+
.plr:hover{background:var(--panel)}
|
|
13
|
+
.plwhen{color:var(--dim);font-variant-numeric:tabular-nums}
|
|
14
|
+
.m-waiting .plwhen{color:var(--dim)}
|
|
15
|
+
.m-running .plwhen{color:var(--accent)}
|
|
16
|
+
.plwhat{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
17
|
+
.plrepo,.plcheck{color:inherit;text-decoration:none}
|
|
18
|
+
.plrepo span{color:var(--pc)}
|
|
19
|
+
.plrepo:hover span,.plcheck:hover{text-decoration:underline}
|
|
20
|
+
.plface{width:14px;height:14px;margin-right:var(--s1);vertical-align:-2px;border-radius:3px;object-fit:contain}
|
|
21
|
+
.pldim{color:var(--dim)}
|
|
22
|
+
.plmark{display:flex;align-items:center;gap:var(--s1);color:var(--dim)}
|
|
23
|
+
.plmark .spin{width:9px;height:9px;border-width:1.5px}
|
|
24
|
+
.plsym{display:inline-block;width:7px;height:7px;border-radius:50%;flex:none;box-sizing:border-box}
|
|
25
|
+
.s-next{border:1px solid var(--dim)}
|
|
26
|
+
.s-due{background:var(--ink)}
|
|
27
|
+
.s-waiting{border:1px dashed var(--dim)}
|
|
28
|
+
.plwait{margin-top:var(--s3)}
|
|
29
|
+
.plwaitline{all:unset;cursor:pointer;color:var(--dim);font-size:12px}
|
|
30
|
+
.plwaitline:hover{color:var(--ink)}
|
|
31
|
+
.s-failed{width:auto;height:auto;border-radius:0;font-style:normal;color:var(--red)}
|
|
32
|
+
.s-failed::before{content:'✕'}
|
|
33
|
+
.m-failed .plmark{color:var(--red)}
|
|
34
|
+
.plcard{z-index:40;width:max-content;max-width:360px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;box-shadow:0 2px 10px var(--shadow);padding:var(--s2) var(--s3);font-size:13px;color:var(--ink)}
|
|
35
|
+
.plcardin{display:flex;flex-direction:column;gap:var(--s1)}
|
|
36
|
+
.plct{margin-bottom:var(--s1)}
|
|
37
|
+
.plkv{display:grid;grid-template-columns:64px minmax(0,1fr);column-gap:var(--s2)}
|
|
38
|
+
.plkv > span:first-child{color:var(--dim)}
|
|
39
|
+
.plkv > span:last-child{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
40
|
+
.plerr{color:var(--red)}
|
|
41
|
+
.l-ok{color:var(--ok)}.l-warn{color:var(--warn)}.l-red{color:var(--red)}.l-dim{color:var(--dim)}
|
|
42
|
+
.plgo{align-self:flex-start;margin-top:var(--s1);padding:0 var(--s2);font:inherit;line-height:22px}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// the repo's declared processes (serve.processes) as the hub supervises them,
|
|
2
|
+
// polled while its repo is the one shown. A repo that declares none never
|
|
3
|
+
// polls again after the first answer.
|
|
4
|
+
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
5
|
+
import type { ProcessView } from '../repo-processes.ts'
|
|
6
|
+
import type { RepoSt } from './repo-st.ts'
|
|
7
|
+
|
|
8
|
+
export const PROCESSES_POLL_MS = 2_000
|
|
9
|
+
|
|
10
|
+
export class ProcessesSvc {
|
|
11
|
+
list: ProcessView[] = []
|
|
12
|
+
error = ''
|
|
13
|
+
private timer: ReturnType<typeof setTimeout> | null = null
|
|
14
|
+
/** bumped by stop, so a chain that left before it dies at its next await */
|
|
15
|
+
private gen = 0
|
|
16
|
+
private polling = false
|
|
17
|
+
|
|
18
|
+
constructor(private st: RepoSt) {
|
|
19
|
+
makeAutoObservable<this, 'st' | 'timer' | 'gen' | 'polling'>(this, { st: false, timer: false, gen: false, polling: false, list: observableRef })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start(): void {
|
|
23
|
+
if (this.polling) return
|
|
24
|
+
this.polling = true
|
|
25
|
+
const gen = ++this.gen
|
|
26
|
+
const tick = async (): Promise<void> => {
|
|
27
|
+
await this.load()
|
|
28
|
+
if (gen === this.gen && this.list.length > 0) this.timer = setTimeout(() => void tick(), PROCESSES_POLL_MS)
|
|
29
|
+
}
|
|
30
|
+
void tick()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
stop(): void {
|
|
34
|
+
this.polling = false
|
|
35
|
+
this.gen++
|
|
36
|
+
if (this.timer != null) clearTimeout(this.timer)
|
|
37
|
+
this.timer = null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async load(): Promise<void> {
|
|
41
|
+
try {
|
|
42
|
+
const r = await fetch(this.st.url('/api/processes.json'))
|
|
43
|
+
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
44
|
+
const j = (await r.json()) as { processes: ProcessView[] }
|
|
45
|
+
runInAction(() => {
|
|
46
|
+
this.list = j.processes
|
|
47
|
+
this.error = ''
|
|
48
|
+
})
|
|
49
|
+
} catch (e) {
|
|
50
|
+
runInAction(() => { this.error = `processes: ${(e as Error).message}` })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async act(id: string, act: 'start' | 'stop' | 'restart'): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
const r = await fetch(this.st.url('/api/processes/act'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, act }) })
|
|
57
|
+
if (!r.ok) throw new Error(await r.text())
|
|
58
|
+
await this.load()
|
|
59
|
+
} catch (e) {
|
|
60
|
+
runInAction(() => { this.error = `${act} ${id}: ${(e as Error).message}` })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* the project list, the page's left panel. Its classes are pj*: another
|
|
2
|
+
sheet of the page already styles .plist (prose-hits.css). It fills its
|
|
3
|
+
panel (hub-app.tsx, react-resizable-panels), whose width is the only one:
|
|
4
|
+
this sheet sizes the rows and never the list. Colours are hub.css tokens only, spacing is its scale:
|
|
5
|
+
tests/hub-css-invariants.test.ts reads this sheet too. Every icon is 18px
|
|
6
|
+
here AND as attributes on the img (ROW_ICON), so a sheet that fails to
|
|
7
|
+
apply still draws a row, never a 512px icon. */
|
|
8
|
+
.pjlist{flex:1;width:100%;height:100%;display:flex;flex-direction:column;background:var(--bg-side);min-width:0}
|
|
9
|
+
/* the home's global pages: a section of lines in the repo rows' style, then
|
|
10
|
+
a thin divider before the repos */
|
|
11
|
+
.pjhead{flex:none;display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
|
|
12
|
+
/* the rows scroll, the head does not, and no scrollbar draws a band beside
|
|
13
|
+
them: WebKit (the macOS app) ignores scrollbar-width, hence the pseudo */
|
|
14
|
+
.pjfoot{flex:none;display:flex;align-items:center;gap:var(--s1);height:32px;padding:0 var(--s2);border-top:1px solid var(--line)}
|
|
15
|
+
/* the archived rows, just above the foot while its archived button is on:
|
|
16
|
+
the rows panel gives up the room, the foot stays put */
|
|
17
|
+
.pjarch{flex:none;border-top:1px solid var(--line);max-height:40%;overflow-y:auto;scrollbar-width:none;padding:var(--s1) 0}
|
|
18
|
+
.pjarch::-webkit-scrollbar{display:none}
|
|
19
|
+
.pjarow{text-decoration:none;display:flex;align-items:center;gap:var(--s2);width:100%;height:22px;padding:0 var(--s2) 0 var(--s3);border:0;border-radius:0;background:none;color:var(--dim);font-size:12px;text-align:left;cursor:pointer;opacity:.6}
|
|
20
|
+
.pjarow:hover,.pjarow.cur{opacity:1;background:var(--panel2)}
|
|
21
|
+
.pjarow span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
22
|
+
.pjarow img{flex:none;border-radius:3px;object-fit:contain}
|
|
23
|
+
.pjaini{flex:none;width:14px;height:14px;border-radius:3px;background:var(--panel2)}
|
|
24
|
+
/* the foot's two buttons: sort names the order the list is in, archived
|
|
25
|
+
carries its count and stays lit while its rows show */
|
|
26
|
+
.pgsort,.pjarbtn{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s1);background:transparent;border:1px solid var(--line);border-radius:3px;
|
|
27
|
+
color:var(--dim);font-size:11px;cursor:pointer}
|
|
28
|
+
.pgsort{flex:0 1 auto;min-width:0}
|
|
29
|
+
.pgsortl{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
30
|
+
.pgsort .hico,.pjarbtn .hico{flex:none;width:14px;height:14px}
|
|
31
|
+
.pgsort:hover,.pjarbtn:hover{color:var(--ink);border-color:var(--accent)}
|
|
32
|
+
.pjarbtn.on{color:var(--ink);background:var(--sel);border-color:var(--accent)}
|
|
33
|
+
.pjarbtn span{font-variant-numeric:tabular-nums}
|
|
34
|
+
/* the sort menu: over the foot, placed by floating-ui (floating.tsx) */
|
|
35
|
+
.pgmenu{z-index:30;min-width:180px;padding:var(--s1) 0;background:var(--panel);border:1px solid var(--line);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px}
|
|
36
|
+
.pgmhead{padding:var(--s1) var(--s2);color:var(--dim);font-size:11px}
|
|
37
|
+
.pgmi{display:flex;align-items:center;gap:var(--s1);width:100%;height:24px;padding:0 var(--s2);background:none;border:0;border-radius:0;text-align:left;color:var(--ink);font-size:12px;cursor:pointer}
|
|
38
|
+
.pgmi:hover:not(:disabled){background:var(--panel2)}
|
|
39
|
+
.pgmi.on{font-weight:600}
|
|
40
|
+
.pgmi.manual{color:var(--dim);cursor:default;border-top:1px solid var(--line)}
|
|
41
|
+
.pgmi small{margin-left:auto;color:var(--dim);font-size:10px;font-weight:400}
|
|
42
|
+
.pgmck{flex:none;width:12px;color:var(--accent)}
|
|
43
|
+
.pjrows{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:var(--s1) 0;scrollbar-width:none}
|
|
44
|
+
.pjrows::-webkit-scrollbar{display:none}
|
|
45
|
+
.pjwait{display:flex;align-items:center;gap:var(--s2);padding:var(--s1) var(--s3);font-size:12px;color:var(--dim)}
|
|
46
|
+
.pjrow,.pjpl{position:relative;display:flex;align-items:center;gap:var(--s2);width:100%;height:28px;padding:0 var(--s3);
|
|
47
|
+
background:transparent;border:0;border-radius:0;text-align:left;color:var(--dim);font-size:13px;cursor:pointer}
|
|
48
|
+
.pjrow:hover,.pjpl:hover{background:var(--panel2);color:var(--ink)}
|
|
49
|
+
/* the shown repo: its row lit, marked on the left in its identity colour
|
|
50
|
+
(a global page's line: in the accent) */
|
|
51
|
+
.pjrow.cur,.pjpl.cur{background:var(--sel);color:var(--pjc,var(--ink));box-shadow:inset 3px 0 0 var(--pjc,var(--accent));font-weight:600}
|
|
52
|
+
.pjpl .hico{flex:none;width:18px;height:18px;color:inherit}
|
|
53
|
+
/* the selected repo while a global page is up: still marked, dimmer */
|
|
54
|
+
.pjrow.cur.away{background:none;font-weight:500}
|
|
55
|
+
.pjrow img,.pjrow .pgi{flex:none;width:18px;height:18px;border-radius:4px;object-fit:contain;pointer-events:none}
|
|
56
|
+
.pjname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
57
|
+
/* the initial on a hue hashed from the name (projects-svc hueOf) */
|
|
58
|
+
.pgi{display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;user-select:none;
|
|
59
|
+
color:var(--c);background:color-mix(in srgb,var(--c) 22%,var(--panel2))}
|
|
60
|
+
.pgi.h1{--c:var(--k1)}
|
|
61
|
+
.pgi.h2{--c:var(--k2)}
|
|
62
|
+
.pgi.h3{--c:var(--k3)}
|
|
63
|
+
.pgi.h4{--c:var(--k4)}
|
|
64
|
+
.pgi.h5{--c:var(--k5)}
|
|
65
|
+
.pgi.h6{--c:var(--k6)}
|
|
66
|
+
/* the row says three things and no more (projects-svc faceOf) */
|
|
67
|
+
/* closed in VS Code: the icon and the name dim, the badge does not */
|
|
68
|
+
.pjrow.dim img,.pjrow.dim .pgi,.pjrow.dim .pjname{opacity:.5}
|
|
69
|
+
/* ⌘1 to ⌘9 select the first nine rows (keys.ts): the hint sits at the row's end */
|
|
70
|
+
.pjkey{flex:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
|
|
71
|
+
.pjrow:hover .pjkey,.pjrow.cur .pjkey,.pjpl:hover .pjkey,.pjpl.cur .pjkey{opacity:1}
|
|
72
|
+
/* the one badge: open asks, at the end of the row */
|
|
73
|
+
.pga{flex:none;min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;
|
|
74
|
+
font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg);background:var(--warn)}
|
|
75
|
+
/* Claude sessions running: a thin edge on the left that breathes */
|
|
76
|
+
.pgs{position:absolute;left:0;top:5px;bottom:5px;width:2px;border-radius:1px;background:var(--accent);pointer-events:none;animation:pgpulse 2.4s ease-in-out infinite}
|
|
77
|
+
/* the home only: the repo's hub state (starting, crashed) */
|
|
78
|
+
.pgd{flex:none;width:6px;height:6px;border-radius:3px;pointer-events:none}
|
|
79
|
+
.pgd.up{background:var(--ok)}.pgd.starting{background:var(--warn)}.pgd.crashed{background:var(--red)}
|
|
80
|
+
/* the home only: a global process this repo uses is off */
|
|
81
|
+
.pgw{flex:none;width:6px;height:6px;border-radius:1px;background:var(--warn)}
|
|
82
|
+
@keyframes pgpulse{0%,100%{opacity:1}50%{opacity:.25}}
|
|
83
|
+
@media (prefers-reduced-motion:reduce){.pgs{animation:none}}
|
|
84
|
+
/* the hover card: beside its row, placed by floating-ui (floating.tsx), one
|
|
85
|
+
at a time, never interactive. Header, then what needs a human, then the
|
|
86
|
+
label and value rows of the check page (.wfact .wk .wv from hub.css) */
|
|
87
|
+
.pgcard{z-index:20;width:300px;padding:var(--s3);pointer-events:none;
|
|
88
|
+
background:var(--panel);border:1px solid var(--line);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px;line-height:1.45;color:var(--ink)}
|
|
89
|
+
.pgch{display:flex;align-items:center;gap:var(--s2);min-width:0}
|
|
90
|
+
.pgcface{flex:none;display:flex}
|
|
91
|
+
.pgcface img,.pgcface .pgi{width:20px;height:20px;border-radius:4px;font-size:11px}
|
|
92
|
+
.pgcname{flex:1;min-width:0;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
93
|
+
.pgcthis{font-weight:400;font-size:11px;color:var(--dim)}
|
|
94
|
+
.pgcver{flex:none;color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums}
|
|
95
|
+
.pgcline{margin-top:var(--s1);color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
96
|
+
.pgcdesc{margin-top:var(--s1);color:var(--dim);font-size:11px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
|
|
97
|
+
/* NEEDS YOU: a warn toned bullet, set apart from the facts */
|
|
98
|
+
.pgcneeds{margin-top:var(--s2);padding:var(--s1) var(--s2);border-left:2px solid var(--warn);background:var(--panel2);border-radius:0 3px 3px 0}
|
|
99
|
+
.pgcneed{display:flex;gap:var(--s1);min-width:0;color:var(--warn)}
|
|
100
|
+
.pgcneed.red{color:var(--red);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
101
|
+
.pgcask{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
102
|
+
.pgcmore{flex:none}
|
|
103
|
+
.pgcfacts{margin-top:var(--s2)}
|
|
104
|
+
.pgcfacts .wk{flex-basis:64px;white-space:nowrap}
|
|
105
|
+
.pgcfacts .wv{text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;word-break:normal}
|
|
106
|
+
.pgcfacts .wv.warn{color:var(--warn)}
|
|
107
|
+
.pgerr{padding:0 var(--s3);font-size:12px;font-weight:700;color:var(--red);cursor:help;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
108
|
+
/* a drag: the row being moved fades, and the gap it would land in is a line
|
|
109
|
+
drawn on a row edge */
|
|
110
|
+
.pjrow.dragging{opacity:.4}
|
|
111
|
+
.pjrow.dropb::before,.pjrow.dropa::after{content:'';position:absolute;left:var(--s2);right:var(--s2);height:2px;border-radius:1px;background:var(--accent);pointer-events:none}
|
|
112
|
+
.pjrow.dropb::before{top:-1px}
|
|
113
|
+
.pjrow.dropa::after{bottom:-1px}
|
|
114
|
+
/* the separators of the page's panels: a 1px divider in the line colour, the
|
|
115
|
+
panels touching it, and a wider hit area that draws nothing (6px each side).
|
|
116
|
+
The accent shows only on hover and while dragging */
|
|
117
|
+
.rsep{position:relative;flex:none;width:1px;background:var(--line)}
|
|
118
|
+
.rsep::before{content:'';position:absolute;top:0;bottom:0;left:-6px;right:-6px;cursor:col-resize;background:transparent;z-index:6}
|
|
119
|
+
.rsep:hover,.rsep[data-separator="hover"],.rsep[data-separator="active"]{background:var(--accent)}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// the project list: one row per repo that shares this page's house wiki,
|
|
2
|
+
// down the left of the page, a small icon and the repo's name on one line,
|
|
3
|
+
// the shown repo's row marked in its identity colour. Its width is the
|
|
4
|
+
// panel the page gives it (hub-app.tsx, react-resizable-panels). A row says
|
|
5
|
+
// three things and no more (faceOf): the open asks as its one badge, a dim
|
|
6
|
+
// icon while VS Code does not have the folder open, a thin edge breathing
|
|
7
|
+
// while Claude sessions run in it; on the home, a dot while its hub starts
|
|
8
|
+
// or crashed and a mark while a global process it uses is off. Everything
|
|
9
|
+
// else is the hover card, placed by floating-ui beside the row. A click
|
|
10
|
+
// selects the repo in place (so does ⌘1 to ⌘9 for the first nine rows, the
|
|
11
|
+
// hint at each row's end), a double click opens the folder in VS Code, a
|
|
12
|
+
// drag reorders the list. The foot holds two buttons: sort opens a menu of
|
|
13
|
+
// labeled sorts (a pick sorts once, the button names the order the list is
|
|
14
|
+
// in), archived shows or hides the archived repos just above the foot
|
|
15
|
+
// (names only, no ⌘ number, a row opens the archived page, nothing loads),
|
|
16
|
+
// the foot never moving. On the home a section of four lines on top, in the
|
|
17
|
+
// repo rows' style, opens the overview, the picker, the activity and the
|
|
18
|
+
// plan, each with its shortcut at the line's end, the page shown lit. It
|
|
19
|
+
// renders from the first paint with the list this browser kept, so a load
|
|
20
|
+
// moves nothing.
|
|
21
|
+
import { autoUpdate, FloatingPortal, useDismiss, useFloating, useHover, useInteractions, useRole } from '@floating-ui/react'
|
|
22
|
+
import { observer } from 'mobx-react-lite'
|
|
23
|
+
import { useEffect, type CSSProperties } from 'react'
|
|
24
|
+
import { middleware, OPEN_MS } from './floating.tsx'
|
|
25
|
+
import { Icon, ICON } from './icons.tsx'
|
|
26
|
+
import { plainClick } from './home-st.ts'
|
|
27
|
+
import { PICKER_KEY, PLACE_KEY } from './keys.ts'
|
|
28
|
+
import { cardModel, faceOf, hueOf, SORTS, type Place, type Project, type ProjectsSvc } from './projects-svc.ts'
|
|
29
|
+
|
|
30
|
+
/** what the list needs of its page: the hub and the home both carry one */
|
|
31
|
+
export type ListHost = { projects: ProjectsSvc }
|
|
32
|
+
|
|
33
|
+
/** the lines on top of the list, in their order: the overview, the picker,
|
|
34
|
+
* then the places, their shortcuts ⌘J ⌘K ⌘L ⌘' top to bottom */
|
|
35
|
+
const PLACES: { id: Place | 'picker'; label: string; title: string }[] = [
|
|
36
|
+
{ id: 'overview', label: 'overview', title: 'the overview: shipkit, every global process and every repo on this machine' },
|
|
37
|
+
{ id: 'picker', label: 'jump to…', title: 'jump to a page, a repo or one of its resources' },
|
|
38
|
+
{ id: 'activity', label: 'activity', title: 'what every repo committed lately, what it holds uncommitted, its branch and whether it is pushed' },
|
|
39
|
+
{ id: 'planned', label: 'planned', title: 'every check the machine runs on its own: now, next, late, and why' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/** the icon's size on a row, in px, as attributes and in the sheet */
|
|
43
|
+
export const ROW_ICON = 18
|
|
44
|
+
|
|
45
|
+
export const Row = observer(({ app, p, i }: { app: ListHost; p: Project; i: number }) => {
|
|
46
|
+
const s = app.projects
|
|
47
|
+
const gap = s.shownGap
|
|
48
|
+
const face = faceOf(p)
|
|
49
|
+
const open = s.hover === p.name
|
|
50
|
+
const f = useFloating({
|
|
51
|
+
open,
|
|
52
|
+
onOpenChange: (v) => (v ? s.hoverIn(p.name) : s.hoverOut(p.name)),
|
|
53
|
+
placement: 'right-start',
|
|
54
|
+
strategy: 'fixed',
|
|
55
|
+
middleware: middleware(8),
|
|
56
|
+
whileElementsMounted: autoUpdate,
|
|
57
|
+
})
|
|
58
|
+
const hover = useHover(f.context, { delay: { open: OPEN_MS, close: 0 }, enabled: s.drag == null })
|
|
59
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
|
|
60
|
+
// the drop line rides on a row edge, so showing it moves nothing
|
|
61
|
+
const edge = gap === i ? ' dropb' : gap === i + 1 && i === s.list.length - 1 ? ' dropa' : ''
|
|
62
|
+
const cur = s.isCurrent(p)
|
|
63
|
+
const cls = `pjrow${cur ? ' cur' : ''}${cur && s.away ? ' away' : ''}${face.dim ? ' dim' : ''}${s.drag === p.name ? ' dragging' : ''}${edge}`
|
|
64
|
+
const dot = s.dotOf(p.name)
|
|
65
|
+
const warn = s.warnOf(p.name)
|
|
66
|
+
// the shown repo's row is marked in its own colour
|
|
67
|
+
const mark = cur ? ({ '--pjc': s.identityOf(p.name).colorDark } as CSSProperties) : undefined
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<button ref={f.refs.setReference} className={cls} aria-label={p.name} style={mark} draggable
|
|
71
|
+
{...getReferenceProps({
|
|
72
|
+
onClick: () => s.click(p.name),
|
|
73
|
+
onDoubleClick: () => s.doubleClick(p.name),
|
|
74
|
+
onDragStart: (e) => {
|
|
75
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
76
|
+
e.dataTransfer.setData('text/plain', p.name)
|
|
77
|
+
s.dragStart(p.name)
|
|
78
|
+
},
|
|
79
|
+
onDragOver: (e) => {
|
|
80
|
+
if (s.drag == null) return
|
|
81
|
+
e.preventDefault()
|
|
82
|
+
const r = e.currentTarget.getBoundingClientRect()
|
|
83
|
+
s.dragOver(i, e.clientY - r.top, r.height)
|
|
84
|
+
},
|
|
85
|
+
onDrop: (e) => {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
s.drop()
|
|
88
|
+
},
|
|
89
|
+
onDragEnd: () => s.dragEnd(),
|
|
90
|
+
})}>
|
|
91
|
+
<Face p={p} px={ROW_ICON} />
|
|
92
|
+
<span className="pjname">{p.name}</span>
|
|
93
|
+
{face.badge != null ? <b className="pga">{face.badge}</b> : null}
|
|
94
|
+
{face.busy ? <i className="pgs" /> : null}
|
|
95
|
+
{dot != null ? <i className={`pgd ${dot}`} /> : null}
|
|
96
|
+
{warn != null ? <i className="pgw" title={warn} /> : null}
|
|
97
|
+
{i < 9 ? <kbd className="pjkey">⌘{i + 1}</kbd> : null}
|
|
98
|
+
</button>
|
|
99
|
+
{open ? (
|
|
100
|
+
<FloatingPortal>
|
|
101
|
+
<div ref={f.refs.setFloating} className="pgcard" role="tooltip" style={f.floatingStyles} {...getFloatingProps()}>
|
|
102
|
+
<Card p={p} now={Date.now()} />
|
|
103
|
+
</div>
|
|
104
|
+
</FloatingPortal>
|
|
105
|
+
) : null}
|
|
106
|
+
</>
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// the img carries its size as attributes too: an icon file is 512px, and
|
|
111
|
+
// without them a sheet that fails to apply draws it at that size
|
|
112
|
+
export const Face = ({ p, px }: { p: Project; px: number }) => (p.icon != null
|
|
113
|
+
? <img src={p.icon} alt="" width={px} height={px} draggable={false} />
|
|
114
|
+
: <span className={`pgi h${hueOf(p.name)}`}>{(p.name.match(/[a-zA-Z]/)?.[0] ?? p.name.charAt(0)).toUpperCase()}</span>)
|
|
115
|
+
|
|
116
|
+
/** the card for one repo: header, what needs a human, then the fact rows,
|
|
117
|
+
* in the label and value shape the check page's WHAT widget uses */
|
|
118
|
+
export const Card = ({ p, now }: { p: Project; now: number }) => {
|
|
119
|
+
const m = cardModel(p, now)
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<div className="pgch">
|
|
123
|
+
<span className="pgcface"><Face p={p} px={20} /></span>
|
|
124
|
+
<b className="pgcname">{m.head.name}{m.head.current ? <span className="pgcthis"> this hub</span> : null}</b>
|
|
125
|
+
{m.head.version == null ? null : <span className="pgcver">v{m.head.version.replace(/^v/, '')}</span>}
|
|
126
|
+
</div>
|
|
127
|
+
{m.head.branch == null ? null : <div className="pgcline">{m.head.branch}</div>}
|
|
128
|
+
{m.head.description == null ? null : <div className="pgcdesc">{m.head.description}</div>}
|
|
129
|
+
{m.needs == null ? null : (
|
|
130
|
+
<div className="pgcneeds">
|
|
131
|
+
{m.needs.asks == null ? null : (
|
|
132
|
+
<div className="pgcneed">
|
|
133
|
+
<span className="pgcask">{m.needs.asks.count} ask{m.needs.asks.count === 1 ? '' : 's'}: {m.needs.asks.titles.join(' · ')}</span>
|
|
134
|
+
{m.needs.asks.more > 0 ? <span className="pgcmore">+{m.needs.asks.more}</span> : null}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
{m.needs.gate == null ? null : <div className="pgcneed red">{m.needs.gate}</div>}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
{m.facts.length === 0 ? null : (
|
|
141
|
+
<div className="pgcfacts">
|
|
142
|
+
{m.facts.map((f) => (
|
|
143
|
+
<div key={f.k} className="wfact">
|
|
144
|
+
<span className="wk">{f.k}</span>
|
|
145
|
+
<span className={`wv${f.tone == null ? '' : ` ${f.tone}`}`}>{f.v}</span>
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const ProjectList = observer(({ app }: { app: ListHost }) => {
|
|
155
|
+
useEffect(() => app.projects.start(), [app])
|
|
156
|
+
const s = app.projects
|
|
157
|
+
return (
|
|
158
|
+
<nav className="pjlist" aria-label="projects">
|
|
159
|
+
{s.onPlace == null && s.onPicker == null && s.error === '' ? null : (
|
|
160
|
+
<div className="pjhead">
|
|
161
|
+
{PLACES.map((pl) => {
|
|
162
|
+
const id = pl.id
|
|
163
|
+
const run = id === 'picker' ? s.onPicker : s.onPlace == null ? null : () => s.onPlace?.(id)
|
|
164
|
+
if (run == null) return null
|
|
165
|
+
const on = id === 'picker' ? s.pickerOpen() : s.place === id
|
|
166
|
+
const key = id === 'picker' ? PICKER_KEY : PLACE_KEY[id]
|
|
167
|
+
return (
|
|
168
|
+
<button key={id} className={`pjpl${on ? ' cur' : ''}`} aria-label={pl.label} title={`${pl.title} (${key})`} onClick={run}>
|
|
169
|
+
<Icon d={ICON[id === 'picker' ? 'search' : id === 'overview' ? 'home' : id]} />
|
|
170
|
+
<span className="pjname">{pl.label}</span>
|
|
171
|
+
<kbd className="pjkey">{key}</kbd>
|
|
172
|
+
</button>
|
|
173
|
+
)
|
|
174
|
+
})}
|
|
175
|
+
{s.error !== '' ? <span className="pgerr" title={s.error}>! {s.error}</span> : null}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
<div className="pjrows">
|
|
179
|
+
{s.list.length === 0 && s.error === '' ? <span className="pjwait"><span className="spin" />reading the repos…</span> : null}
|
|
180
|
+
{s.list.map((p, i) => <Row key={p.name} app={app} p={p} i={i} />)}
|
|
181
|
+
</div>
|
|
182
|
+
{s.archivedOpen && s.archived.length > 0 && s.onArchived != null ? (
|
|
183
|
+
<div className="pjarch">
|
|
184
|
+
{s.archived.map((p) => (
|
|
185
|
+
<a key={p.name} className="pjarow" href="/archived" title={`${p.name} is archived: nothing runs there`} onClick={(e) => {
|
|
186
|
+
if (!plainClick(e)) return
|
|
187
|
+
e.preventDefault()
|
|
188
|
+
s.onArchived?.()
|
|
189
|
+
}}>
|
|
190
|
+
{p.icon != null ? <img src={p.icon} alt="" width={14} height={14} draggable={false} /> : <i className="pjaini" />}
|
|
191
|
+
<span>{p.name}</span>
|
|
192
|
+
</a>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
<div className="pjfoot">
|
|
197
|
+
<SortButton s={s} />
|
|
198
|
+
{s.archived.length === 0 || s.onArchived == null ? null : (
|
|
199
|
+
<button className={`pjarbtn${s.archivedOpen ? ' on' : ''}`} aria-pressed={s.archivedOpen}
|
|
200
|
+
title={`${s.archivedOpen ? 'hide' : 'show'} the ${s.archived.length} archived repos: nothing runs there, a row opens the archived page`}
|
|
201
|
+
onClick={() => s.toggleArchivedOpen()}>
|
|
202
|
+
<Icon d={ICON.archived} /><span>{s.archived.length}</span>
|
|
203
|
+
</button>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
</nav>
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
/** the words for the order the list is in */
|
|
211
|
+
const orderWords = (s: ProjectsSvc): string => {
|
|
212
|
+
const m = s.sortMode
|
|
213
|
+
return m === 'manual' ? 'manual order' : SORTS.find((x) => x.id === m)!.label
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** the sort button: it names the order the list is in and opens the menu,
|
|
217
|
+
* and sorts nothing itself */
|
|
218
|
+
const SortButton = observer(({ s }: { s: ProjectsSvc }) => {
|
|
219
|
+
const f = useFloating({
|
|
220
|
+
open: s.sortMenuOpen,
|
|
221
|
+
onOpenChange: (v) => s.setSortMenu(v),
|
|
222
|
+
placement: 'top-start',
|
|
223
|
+
strategy: 'fixed',
|
|
224
|
+
middleware: middleware(6),
|
|
225
|
+
whileElementsMounted: autoUpdate,
|
|
226
|
+
})
|
|
227
|
+
const dismiss = useDismiss(f.context)
|
|
228
|
+
const role = useRole(f.context, { role: 'menu' })
|
|
229
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, role])
|
|
230
|
+
return (
|
|
231
|
+
<>
|
|
232
|
+
<button ref={f.refs.setReference} className="pgsort" title={`the list is in ${orderWords(s)}: pick a sort`}
|
|
233
|
+
{...getReferenceProps({ onClick: () => s.toggleSortMenu() })}>
|
|
234
|
+
<Icon d={ICON.sort} /><span className="pgsortl">{orderWords(s)}</span>
|
|
235
|
+
</button>
|
|
236
|
+
{s.sortMenuOpen ? (
|
|
237
|
+
<FloatingPortal>
|
|
238
|
+
<div ref={f.refs.setFloating} className="pgmenu" style={f.floatingStyles} {...getFloatingProps()}>
|
|
239
|
+
<SortMenu s={s} />
|
|
240
|
+
</div>
|
|
241
|
+
</FloatingPortal>
|
|
242
|
+
) : null}
|
|
243
|
+
</>
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
/** the menu's lines: one per sort, the order the list is in checked. The
|
|
248
|
+
* manual order is a state a drag makes, never a pick */
|
|
249
|
+
export const SortMenu = observer(({ s }: { s: ProjectsSvc }) => {
|
|
250
|
+
const cur = s.sortMode
|
|
251
|
+
return (
|
|
252
|
+
<>
|
|
253
|
+
<div className="pgmhead">sort the list once by</div>
|
|
254
|
+
{SORTS.map((m) => (
|
|
255
|
+
<button key={m.id} role="menuitemradio" aria-checked={cur === m.id} className={`pgmi${cur === m.id ? ' on' : ''}`}
|
|
256
|
+
title={m.title} onClick={() => s.sortBy(m.id)}>
|
|
257
|
+
<span className="pgmck">{cur === m.id ? '✓' : ''}</span>{m.label}
|
|
258
|
+
</button>
|
|
259
|
+
))}
|
|
260
|
+
<button role="menuitemradio" aria-checked={cur === 'manual'} className={`pgmi manual${cur === 'manual' ? ' on' : ''}`} disabled
|
|
261
|
+
title="a drag of a row makes the manual order, and every sort ends in it">
|
|
262
|
+
<span className="pgmck">{cur === 'manual' ? '✓' : ''}</span>manual order<small>drag a row</small>
|
|
263
|
+
</button>
|
|
264
|
+
</>
|
|
265
|
+
)
|
|
266
|
+
})
|