rvlib-shipkit 1.1.0 → 1.2.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 +67 -0
- package/package.json +3 -1
- package/src/check.ts +24 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
- package/src/checks/SK023-stores-under-cap.check.ts +49 -0
- package/src/checks/SK024-identity-colors.check.ts +49 -0
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +18 -5
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli.ts +22 -2
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/attach.ts +14 -9
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +40 -18
- package/src/core/discover-checks.ts +36 -18
- 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/retention.ts +213 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +3 -0
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +86 -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 +208 -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 +141 -0
- package/src/home/resources.ts +338 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +149 -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 +172 -0
- package/src/home/supervise.ts +346 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widgets.ts +102 -0
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +5 -0
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/run-log.ts +58 -23
- package/src/ledger/store.ts +228 -103
- package/src/ledger/world-cache.ts +15 -13
- package/src/plan/cadence.ts +237 -0
- package/src/plan/planner.ts +303 -0
- package/src/plan/repo-source.ts +100 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/repo-config.ts +51 -2
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +1 -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/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +1 -1
- package/src/serve/hub-app/header-bar.tsx +44 -12
- 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 +241 -0
- package/src/serve/hub-app/home-strip.css +70 -0
- package/src/serve/hub-app/home-strip.tsx +245 -0
- package/src/serve/hub-app/home.html +17 -0
- package/src/serve/hub-app/hub-app.tsx +201 -93
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +43 -23
- package/src/serve/hub-app/icons.tsx +14 -0
- package/src/serve/hub-app/index.html +3 -1
- package/src/serve/hub-app/keys.ts +25 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +12 -9
- package/src/serve/hub-app/machine-st.ts +270 -0
- 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 +88 -0
- package/src/serve/hub-app/planned-st.ts +171 -0
- package/src/serve/hub-app/planned-view.tsx +109 -0
- package/src/serve/hub-app/planned.css +39 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +97 -0
- package/src/serve/hub-app/project-list.tsx +167 -0
- package/src/serve/hub-app/projects-svc.ts +139 -17
- 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/{hub-st.ts → repo-st.ts} +155 -84
- 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/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +10 -10
- package/src/serve/hub-app/timeline-chart.tsx +24 -12
- package/src/serve/hub-app/timeline-svc.ts +5 -4
- package/src/serve/hub-app/timeline-view.tsx +13 -13
- 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 +542 -0
- package/src/serve/project-gutter-route.ts +21 -0
- package/src/serve/project-gutter.ts +3 -2
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +142 -44
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +114 -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/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
// THE HEALTH PASS TIMER. While the hub serves, the whole health pass runs once
|
|
2
|
-
// a day, and once at startup when the last whole pass on record is older than
|
|
3
|
-
// that. One timer for the repo, no per check schedule: a check's own `ttl` is
|
|
4
|
-
// its cadence, and this pass is only the heartbeat that replays an answer
|
|
5
|
-
// still fresh and re-measures one that expired.
|
|
6
|
-
//
|
|
7
|
-
// The run is the hub's own in process runner, the same path as the button, so
|
|
8
|
-
// its rows land in the same run record. "When was the last pass" is read off
|
|
9
|
-
// that record, which is what makes a `shipkit check` in a terminal count too.
|
|
10
|
-
|
|
11
|
-
/** how often the health pass runs while the hub serves */
|
|
12
|
-
export const HEALTH_EVERY_MS = 24 * 60 * 60 * 1000
|
|
13
|
-
|
|
14
|
-
/** how often the timer looks. The pass itself is daily, this only bounds how
|
|
15
|
-
* late after its due time it starts */
|
|
16
|
-
export const SCHEDULE_LOOK_MS = 5 * 60 * 1000
|
|
17
|
-
|
|
18
|
-
export type ScheduleState = {
|
|
19
|
-
everyMs: number
|
|
20
|
-
/** when the last whole health pass on record started, null = none */
|
|
21
|
-
lastPassAt: number | null
|
|
22
|
-
/** when the next one starts: now when it is overdue */
|
|
23
|
-
nextAt: number
|
|
24
|
-
/** the timer's own pass is running right now */
|
|
25
|
-
running: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type ScheduleDeps = {
|
|
29
|
-
now: () => number
|
|
30
|
-
/** the last whole health pass on record, whoever ran it */
|
|
31
|
-
lastPassAt: () => number | null
|
|
32
|
-
/** another run in this process is going: the timer never stacks on it */
|
|
33
|
-
busy: () => boolean
|
|
34
|
-
/** the pass itself */
|
|
35
|
-
run: () => Promise<void>
|
|
36
|
-
everyMs?: number
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type TickResult = 'ran' | 'fresh' | 'busy'
|
|
40
|
-
|
|
41
|
-
export class HealthSchedule {
|
|
42
|
-
readonly everyMs: number
|
|
43
|
-
running = false
|
|
44
|
-
/** when this timer last STARTED a pass. A pass that dies before it writes
|
|
45
|
-
* a record would otherwise be due again at the very next look */
|
|
46
|
-
private attemptedAt: number | null = null
|
|
47
|
-
private timer: ReturnType<typeof setInterval> | null = null
|
|
48
|
-
|
|
49
|
-
constructor(private readonly d: ScheduleDeps) {
|
|
50
|
-
this.everyMs = d.everyMs ?? HEALTH_EVERY_MS
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** the newest of the record and this timer's own last attempt */
|
|
54
|
-
private lastAt(): number | null {
|
|
55
|
-
const rec = this.d.lastPassAt()
|
|
56
|
-
if (rec == null) return this.attemptedAt
|
|
57
|
-
return this.attemptedAt == null ? rec : Math.max(rec, this.attemptedAt)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
due(): boolean {
|
|
61
|
-
const last = this.lastAt()
|
|
62
|
-
return last == null || this.d.now() - last >= this.everyMs
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
state(): ScheduleState {
|
|
66
|
-
const now = this.d.now()
|
|
67
|
-
const last = this.lastAt()
|
|
68
|
-
return {
|
|
69
|
-
everyMs: this.everyMs,
|
|
70
|
-
lastPassAt: this.d.lastPassAt(),
|
|
71
|
-
nextAt: last == null ? now : Math.max(now, last + this.everyMs),
|
|
72
|
-
running: this.running,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** one look: run the pass when it is due and nothing else is running */
|
|
77
|
-
async tick(): Promise<TickResult> {
|
|
78
|
-
if (this.running || this.d.busy()) return 'busy'
|
|
79
|
-
if (!this.due()) return 'fresh'
|
|
80
|
-
this.running = true
|
|
81
|
-
this.attemptedAt = this.d.now()
|
|
82
|
-
try {
|
|
83
|
-
await this.d.run()
|
|
84
|
-
} finally {
|
|
85
|
-
this.running = false
|
|
86
|
-
}
|
|
87
|
-
return 'ran'
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** look now, then every SCHEDULE_LOOK_MS. A second start is a no-op */
|
|
91
|
-
start(onError: (e: Error) => void, lookMs = SCHEDULE_LOOK_MS): void {
|
|
92
|
-
if (this.timer != null) return
|
|
93
|
-
const look = (): void => void this.tick().catch((e: Error) => onError(e))
|
|
94
|
-
look()
|
|
95
|
-
this.timer = setInterval(look, lookMs)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
stop(): void {
|
|
99
|
-
if (this.timer != null) clearInterval(this.timer)
|
|
100
|
-
this.timer = null
|
|
101
|
-
}
|
|
102
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/* the project gutter. It is FIXED to the left edge, so it takes no cell of
|
|
2
|
-
the .app grid, and the page makes room for it by padding, whatever the
|
|
3
|
-
grid holds. Colours are hub.css tokens only, spacing is its scale:
|
|
4
|
-
tests/hub-css-invariants.test.ts reads this sheet too. */
|
|
5
|
-
.app:has(> .pgut){--gut:44px;padding-left:var(--gut)}
|
|
6
|
-
.pgut{position:fixed;left:0;top:0;bottom:0;width:var(--gut,44px);z-index:5;display:flex;flex-direction:column;align-items:center;
|
|
7
|
-
gap:var(--s1);padding:var(--s2) 0;background:var(--bg);border-right:1px solid var(--line);overflow-y:auto;scrollbar-width:none}
|
|
8
|
-
.pgt{position:relative;flex:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;padding:0;
|
|
9
|
-
background:transparent;border:0;border-radius:6px;cursor:pointer}
|
|
10
|
-
.pgt:hover{background:var(--panel2)}
|
|
11
|
-
.pgt.cur{background:var(--sel)}
|
|
12
|
-
.pgt img{width:24px;height:24px;border-radius:5px;object-fit:cover;pointer-events:none}
|
|
13
|
-
/* the initial on a hue hashed from the name (projects-svc hueOf) */
|
|
14
|
-
.pgi{width:24px;height:24px;border-radius:5px;display:flex;align-items:center;justify-content:center;
|
|
15
|
-
font-size:12px;font-weight:600;user-select:none;color:var(--c);background:color-mix(in srgb,var(--c) 22%,var(--panel2))}
|
|
16
|
-
.pgi.h1{--c:var(--k1)}
|
|
17
|
-
.pgi.h2{--c:var(--k2)}
|
|
18
|
-
.pgi.h3{--c:var(--k3)}
|
|
19
|
-
.pgi.h4{--c:var(--k4)}
|
|
20
|
-
.pgi.h5{--c:var(--k5)}
|
|
21
|
-
.pgi.h6{--c:var(--k6)}
|
|
22
|
-
/* the face says three things and no more (projects-svc faceOf) */
|
|
23
|
-
/* closed in VS Code: the icon dims, the badge does not */
|
|
24
|
-
.pgt.dim img,.pgt.dim .pgi{opacity:.45}
|
|
25
|
-
/* the one badge: open asks, top right */
|
|
26
|
-
.pga{position:absolute;top:0;right:0;min-width:14px;height:14px;padding:0 var(--s0);border-radius:7px;
|
|
27
|
-
font-size:9px;font-weight:700;line-height:14px;text-align:center;color:var(--bg);background:var(--warn)}
|
|
28
|
-
/* Claude sessions running: a thin edge on the left that breathes */
|
|
29
|
-
.pgs{position:absolute;left:0;top:6px;bottom:6px;width:2px;border-radius:1px;background:var(--accent);pointer-events:none;animation:pgpulse 2.4s ease-in-out infinite}
|
|
30
|
-
@keyframes pgpulse{0%,100%{opacity:1}50%{opacity:.25}}
|
|
31
|
-
@media (prefers-reduced-motion:reduce){.pgs{animation:none}}
|
|
32
|
-
/* the hover card: right of the rail, one at a time, never interactive.
|
|
33
|
-
Header, then what needs a human, then the label and value rows of the
|
|
34
|
-
check page (.wfact .wk .wv from hub.css) */
|
|
35
|
-
.pgcard{position:fixed;left:calc(var(--gut,44px) + var(--s2));z-index:20;width:300px;padding:var(--s3);pointer-events:none;
|
|
36
|
-
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)}
|
|
37
|
-
.pgch{display:flex;align-items:center;gap:var(--s2);min-width:0}
|
|
38
|
-
.pgcface{flex:none;display:flex}
|
|
39
|
-
.pgcface img,.pgcface .pgi{width:20px;height:20px;border-radius:4px;font-size:11px}
|
|
40
|
-
.pgcname{flex:1;min-width:0;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
41
|
-
.pgcthis{font-weight:400;font-size:11px;color:var(--dim)}
|
|
42
|
-
.pgcver{flex:none;color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums}
|
|
43
|
-
.pgcline{margin-top:var(--s1);color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
44
|
-
.pgcdesc{margin-top:var(--s1);color:var(--dim);font-size:11px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
|
|
45
|
-
/* NEEDS YOU: a warn toned bullet, set apart from the facts */
|
|
46
|
-
.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}
|
|
47
|
-
.pgcneed{display:flex;gap:var(--s1);min-width:0;color:var(--warn)}
|
|
48
|
-
.pgcneed.red{color:var(--red);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
49
|
-
.pgcask{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
50
|
-
.pgcmore{flex:none}
|
|
51
|
-
.pgcfacts{margin-top:var(--s2)}
|
|
52
|
-
.pgcfacts .wk{flex-basis:64px;white-space:nowrap}
|
|
53
|
-
.pgcfacts .wv{text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;word-break:normal}
|
|
54
|
-
.pgcfacts .wv.warn{color:var(--warn)}
|
|
55
|
-
.pgerr{font-size:12px;font-weight:700;color:var(--red);cursor:help}
|
|
56
|
-
/* the one time sort by last activity, above the icons */
|
|
57
|
-
.pgsort{flex:none;width:32px;height:20px;padding:0;background:transparent;border:1px solid var(--line);border-radius:3px;
|
|
58
|
-
color:var(--dim);font-size:12px;line-height:18px;cursor:pointer}
|
|
59
|
-
.pgsort:hover{color:var(--ink);border-color:var(--accent)}
|
|
60
|
-
/* a drag: the tile being moved fades, and the gap it would land in is a line
|
|
61
|
-
drawn on a tile edge, centred in the gap between two tiles */
|
|
62
|
-
.pgt.dragging{opacity:.4}
|
|
63
|
-
.pgt.dropb::before,.pgt.dropa::after{content:'';position:absolute;left:2px;right:2px;height:2px;border-radius:1px;background:var(--accent);pointer-events:none}
|
|
64
|
-
.pgt.dropb::before{top:-3px}
|
|
65
|
-
.pgt.dropa::after{bottom:-3px}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// the project gutter: one icon per repo that shares this hub's house wiki,
|
|
2
|
-
// down the far left of the page. The face stays quiet (faceOf): at most one
|
|
3
|
-
// badge, the open asks; the icon dims while VS Code does not have the folder
|
|
4
|
-
// open; a thin edge pulses while Claude sessions run in it; the selected
|
|
5
|
-
// background marks this hub's own repo. Everything else is the hover card,
|
|
6
|
-
// shown at once to the right of the icon. A click opens the folder in VS
|
|
7
|
-
// Code, a drag reorders the rail, and the button on top sorts it once by
|
|
8
|
-
// last activity. Fixed position, so it takes no cell of the page grid
|
|
9
|
-
// (project-gutter.css).
|
|
10
|
-
import { observer } from 'mobx-react-lite'
|
|
11
|
-
import { useEffect, useLayoutEffect, useRef } from 'react'
|
|
12
|
-
import type { HubSt } from './hub-st.ts'
|
|
13
|
-
import { cardModel, faceOf, hueOf, type Project } from './projects-svc.ts'
|
|
14
|
-
|
|
15
|
-
export const Tile = observer(({ app, p, i }: { app: HubSt; p: Project; i: number }) => {
|
|
16
|
-
const s = app.projects
|
|
17
|
-
const gap = s.shownGap
|
|
18
|
-
const face = faceOf(p)
|
|
19
|
-
// the drop line rides on a tile edge, so showing it moves nothing
|
|
20
|
-
const edge = gap === i ? ' dropb' : gap === i + 1 && i === s.list.length - 1 ? ' dropa' : ''
|
|
21
|
-
const cls = `pgt${p.current ? ' cur' : ''}${face.dim ? ' dim' : ''}${s.drag === p.name ? ' dragging' : ''}${edge}`
|
|
22
|
-
return (
|
|
23
|
-
<button className={cls} aria-label={p.name}
|
|
24
|
-
draggable onClick={() => void s.open(p.name)}
|
|
25
|
-
onMouseEnter={(e) => {
|
|
26
|
-
const r = e.currentTarget.getBoundingClientRect()
|
|
27
|
-
s.hoverIn(p.name, r.top + r.height / 2)
|
|
28
|
-
}}
|
|
29
|
-
onMouseLeave={() => s.hoverOut(p.name)}
|
|
30
|
-
onDragStart={(e) => {
|
|
31
|
-
e.dataTransfer.effectAllowed = 'move'
|
|
32
|
-
e.dataTransfer.setData('text/plain', p.name)
|
|
33
|
-
s.dragStart(p.name)
|
|
34
|
-
}}
|
|
35
|
-
onDragOver={(e) => {
|
|
36
|
-
if (s.drag == null) return
|
|
37
|
-
e.preventDefault()
|
|
38
|
-
const r = e.currentTarget.getBoundingClientRect()
|
|
39
|
-
s.dragOver(i, e.clientY - r.top, r.height)
|
|
40
|
-
}}
|
|
41
|
-
onDrop={(e) => {
|
|
42
|
-
e.preventDefault()
|
|
43
|
-
s.drop()
|
|
44
|
-
}}
|
|
45
|
-
onDragEnd={() => s.dragEnd()}>
|
|
46
|
-
<Face p={p} />
|
|
47
|
-
{face.badge != null ? <b className="pga">{face.badge}</b> : null}
|
|
48
|
-
{face.busy ? <i className="pgs" /> : null}
|
|
49
|
-
</button>
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const Face = ({ p }: { p: Project }) => (p.icon != null
|
|
54
|
-
? <img src={p.icon} alt="" draggable={false} />
|
|
55
|
-
: <span className={`pgi h${hueOf(p.name)}`}>{(p.name.match(/[a-zA-Z]/)?.[0] ?? p.name.charAt(0)).toUpperCase()}</span>)
|
|
56
|
-
|
|
57
|
-
/** the card for one repo: header, what needs a human, then the fact rows,
|
|
58
|
-
* in the label and value shape the check page's WHAT widget uses */
|
|
59
|
-
export const Card = ({ p, now }: { p: Project; now: number }) => {
|
|
60
|
-
const m = cardModel(p, now)
|
|
61
|
-
return (
|
|
62
|
-
<>
|
|
63
|
-
<div className="pgch">
|
|
64
|
-
<span className="pgcface"><Face p={p} /></span>
|
|
65
|
-
<b className="pgcname">{m.head.name}{m.head.current ? <span className="pgcthis"> this hub</span> : null}</b>
|
|
66
|
-
{m.head.version == null ? null : <span className="pgcver">v{m.head.version.replace(/^v/, '')}</span>}
|
|
67
|
-
</div>
|
|
68
|
-
{m.head.branch == null ? null : <div className="pgcline">{m.head.branch}</div>}
|
|
69
|
-
{m.head.description == null ? null : <div className="pgcdesc">{m.head.description}</div>}
|
|
70
|
-
{m.needs == null ? null : (
|
|
71
|
-
<div className="pgcneeds">
|
|
72
|
-
{m.needs.asks == null ? null : (
|
|
73
|
-
<div className="pgcneed">
|
|
74
|
-
<span className="pgcask">{m.needs.asks.count} ask{m.needs.asks.count === 1 ? '' : 's'}: {m.needs.asks.titles.join(' · ')}</span>
|
|
75
|
-
{m.needs.asks.more > 0 ? <span className="pgcmore">+{m.needs.asks.more}</span> : null}
|
|
76
|
-
</div>
|
|
77
|
-
)}
|
|
78
|
-
{m.needs.gate == null ? null : <div className="pgcneed red">{m.needs.gate}</div>}
|
|
79
|
-
</div>
|
|
80
|
-
)}
|
|
81
|
-
{m.facts.length === 0 ? null : (
|
|
82
|
-
<div className="pgcfacts">
|
|
83
|
-
{m.facts.map((f) => (
|
|
84
|
-
<div key={f.k} className="wfact">
|
|
85
|
-
<span className="wk">{f.k}</span>
|
|
86
|
-
<span className={`wv${f.tone == null ? '' : ` ${f.tone}`}`}>{f.v}</span>
|
|
87
|
-
</div>
|
|
88
|
-
))}
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
</>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** the one hover card: right of the rail, centred on the icon's row, then
|
|
96
|
-
* clamped into the viewport once its own height is known */
|
|
97
|
-
export const HoverCard = observer(({ app }: { app: HubSt }) => {
|
|
98
|
-
const s = app.projects
|
|
99
|
-
const p = s.hovered
|
|
100
|
-
const y = s.hover?.y ?? 0
|
|
101
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
102
|
-
useLayoutEffect(() => {
|
|
103
|
-
const el = ref.current
|
|
104
|
-
if (el == null) return
|
|
105
|
-
const h = el.offsetHeight
|
|
106
|
-
el.style.top = `${Math.max(8, Math.min(y - h / 2, window.innerHeight - h - 8))}px`
|
|
107
|
-
})
|
|
108
|
-
if (p == null) return null
|
|
109
|
-
return (
|
|
110
|
-
<div ref={ref} className="pgcard" role="tooltip">
|
|
111
|
-
<Card p={p} now={Date.now()} />
|
|
112
|
-
</div>
|
|
113
|
-
)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
export const ProjectGutter = observer(({ app }: { app: HubSt }) => {
|
|
117
|
-
useEffect(() => app.projects.start(), [app])
|
|
118
|
-
const s = app.projects
|
|
119
|
-
if (s.list.length === 0 && s.error === '') return null
|
|
120
|
-
return (
|
|
121
|
-
<nav className="pgut" aria-label="projects">
|
|
122
|
-
<button className="pgsort" title="sort by last activity, once: the result is the new manual order" onClick={() => s.sortByActivity()}>⇅</button>
|
|
123
|
-
{s.list.map((p, i) => <Tile key={p.name} app={app} p={p} i={i} />)}
|
|
124
|
-
{s.error !== '' ? <span className="pgerr" title={s.error}>!</span> : null}
|
|
125
|
-
<HoverCard app={app} />
|
|
126
|
-
</nav>
|
|
127
|
-
)
|
|
128
|
-
})
|