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
|
@@ -1,139 +1,53 @@
|
|
|
1
|
-
// THE page (Bun.serve HTML import — no vite, no build step)
|
|
2
|
-
//
|
|
1
|
+
// THE page (Bun.serve HTML import — no vite, no build step), one single page
|
|
2
|
+
// app for every page the home serves ('/', '/planned', '/activity', '/r/<repo>/') and for a
|
|
3
|
+
// hub serving on its own at '/'. mobx+react per build/app-state-tree: HomeSt
|
|
4
|
+
// is the single root (window.app), each repo a RepoSt under it, services own
|
|
5
|
+
// state, components read and call. A repo's page is its sections over ONE
|
|
6
|
+
// tree of every repo resource (sidebar.tsx), by path or by type, coloured by
|
|
3
7
|
// state in the label itself, over one severity filter persisted at
|
|
4
|
-
//
|
|
5
|
-
// atelier. mobx+react per build/app-state-tree: HubSt is the single root
|
|
6
|
-
// (window.app), services own state, components read and call.
|
|
8
|
+
// ?f=needs · ?f=red. A row opens its item view, an asset row opens the atelier.
|
|
7
9
|
import { runInAction } from 'mobx'
|
|
8
10
|
import { observer } from 'mobx-react-lite'
|
|
9
|
-
import { StrictMode, useEffect } from 'react'
|
|
11
|
+
import { StrictMode, useEffect, useLayoutEffect, useRef } from 'react'
|
|
10
12
|
import { createRoot } from 'react-dom/client'
|
|
11
13
|
import { ColCard, ColGrid, StatusCard } from './col-card.tsx'
|
|
12
14
|
import { WIKIS, WikisView } from './wikis-view.tsx'
|
|
13
15
|
import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
|
|
14
|
-
import {
|
|
16
|
+
import type { Filter, RepoSt } from './repo-st.ts'
|
|
17
|
+
import { HomeSt, PLACE_PATH } from './home-st.ts'
|
|
15
18
|
import { TimelineView } from './timeline-view.tsx'
|
|
16
|
-
import { HeaderBar,
|
|
19
|
+
import { HeaderBar, notUp } from './header-bar.tsx'
|
|
17
20
|
import { FOLLOW_KEY } from './timeline-chart.tsx'
|
|
18
21
|
import { CheckCard } from './check-view.tsx'
|
|
19
22
|
import { TestCard } from './test-view.tsx'
|
|
20
23
|
import { TestsPage } from './tests-view.tsx'
|
|
21
24
|
import { AssetStage, CatGrid, InfoChips, MediaTopbar, ToolsBar, VariantsBar } from './media-view.tsx'
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
25
|
+
import { Sidebar } from './sidebar.tsx'
|
|
26
|
+
import { AdminPage } from './admin-view.tsx'
|
|
27
|
+
import { ReadmeView } from './readme-view.tsx'
|
|
28
|
+
import { ProjectList } from './project-list.tsx'
|
|
29
|
+
import { Group, Panel, Separator, usePanelRef, type Layout } from 'react-resizable-panels'
|
|
30
|
+
import { LIST, SIDEBAR } from './layout-st.ts'
|
|
31
|
+
import { ArchivedView, HomeStrip, NotUp, Overview } from './home-strip.tsx'
|
|
32
|
+
import { PlannedView } from './planned-view.tsx'
|
|
33
|
+
import { ActivityView } from './activity-view.tsx'
|
|
34
|
+
import { Palette } from './palette-view.tsx'
|
|
35
|
+
import { SCREEN_WAIT_MS } from './screen-cache.ts'
|
|
36
|
+
import { isSearchKey, pageKey, sectionKey } from './keys.ts'
|
|
37
|
+
import { repoSections } from './repo-sections.tsx'
|
|
38
|
+
import { Dock } from './dock-view.tsx'
|
|
39
|
+
import { DOCK } from './dock-st.ts'
|
|
40
|
+
import { animateDock } from './dock-anim.ts'
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
// home.html marks the home's page; index.html is a hub serving on its own
|
|
43
|
+
const home = new HomeSt({ home: document.body.dataset.home != null })
|
|
28
44
|
// the root as a global — app code, console debugging (build/app-state-tree)
|
|
29
|
-
;(window as unknown as { app:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// ── the tree: every resource of the repo, one row deep in its path ──────
|
|
33
|
-
// the row itself is tree-row.tsx, so a test renders it headless
|
|
34
|
-
|
|
35
|
-
const Tree = observer(() => {
|
|
36
|
-
const t = app.tree
|
|
37
|
-
return (
|
|
38
|
-
// roving focus: the arrows belong to the tree only while it HAS the
|
|
39
|
-
// focus, so the atelier keeps its own keys
|
|
40
|
-
<div className="tree" tabIndex={0} onKeyDown={(e) => { if (t.key(e.key)) e.preventDefault() }}>
|
|
41
|
-
{t.rows.map((r) => <TreeRowView key={r.key} app={app} row={r} />)}
|
|
42
|
-
{t.rows.length === 0 ? <div className="tempty">{app.filter === 'red' ? 'no reds — nothing is blocked ✨' : app.filter === 'needs' ? 'nothing needs you ✨' : 'no resources yet'}</div> : null}
|
|
43
|
-
</div>
|
|
44
|
-
)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
/** what every glyph in the type slot means, behind one `(?)` in the header */
|
|
48
|
-
const TypeLegend = () => (
|
|
49
|
-
<Help text={
|
|
50
|
-
<span className="legend">
|
|
51
|
-
{Object.entries(TYPE_MARK).map(([kind, m]) => (
|
|
52
|
-
<span key={kind} className="lrow"><i className={`ty ${m.hue}`}>{m.glyph}</i>{m.word}</span>
|
|
53
|
-
))}
|
|
54
|
-
</span>
|
|
55
|
-
} />
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
const TreeTabs = observer(() => (
|
|
59
|
-
<span className="seg">
|
|
60
|
-
{(['path', 'type'] as const).map((t) => (
|
|
61
|
-
<button key={t} className={app.tree.tab === t ? 'on' : ''} onClick={() => app.tree.setTab(t)}>by {t}</button>
|
|
62
|
-
))}
|
|
63
|
-
</span>
|
|
64
|
-
))
|
|
65
|
-
|
|
66
|
-
// segmented severity filter — all · needs (yellow+red) · red, the address
|
|
67
|
-
// rides at /?f=… so a filtered view is shareable
|
|
68
|
-
// the shortcut letter rides INSIDE its own button, so the binding is
|
|
69
|
-
// discoverable without a legend
|
|
70
|
-
const FILTER_KEY = { all: 'a', needs: 'n', red: 'r' } as const
|
|
71
|
-
const SegFilter = observer(() => (
|
|
72
|
-
<span className="seg">
|
|
73
|
-
{(['all', 'needs', 'red'] as const).map((f) => (
|
|
74
|
-
<button key={f} className={app.filter === f ? 'on' : ''} onClick={() => app.setFilter(f)}>
|
|
75
|
-
{f}<span className="sk">{FILTER_KEY[f]}</span>
|
|
76
|
-
</button>
|
|
77
|
-
))}
|
|
78
|
-
</span>
|
|
79
|
-
))
|
|
80
|
-
|
|
81
|
-
const Sidebar = observer(() => (
|
|
82
|
-
<aside>
|
|
83
|
-
<div className="shead">
|
|
84
|
-
resources
|
|
85
|
-
<SegFilter />
|
|
86
|
-
</div>
|
|
87
|
-
<div className="ssearch">
|
|
88
|
-
<input id="treeq" placeholder="filter… (/ to focus, ⌘J to jump)" value={app.query}
|
|
89
|
-
onChange={(e) => app.setQuery(e.target.value)}
|
|
90
|
-
onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery('') }} />
|
|
91
|
-
</div>
|
|
92
|
-
<div className="stabs">
|
|
93
|
-
<TreeTabs />
|
|
94
|
-
<span className="sp" />
|
|
95
|
-
<TypeLegend />
|
|
96
|
-
<span className="cnt">{app.tree.visible.length}</span>
|
|
97
|
-
</div>
|
|
98
|
-
{app.tree.whenLine === '' ? null : <div className="swhen">{app.tree.whenLine}</div>}
|
|
99
|
-
{app.media.dead != null ? <div className="tempty"><span className="warnline">media API down: {app.media.dead}</span></div> : null}
|
|
100
|
-
<Tree />
|
|
101
|
-
</aside>
|
|
102
|
-
))
|
|
103
|
-
|
|
104
|
-
// ── cmd+j palette: every jump target behind one input ────────────────────
|
|
105
|
-
const Palette = observer(() => {
|
|
106
|
-
if (!app.palOpen) return null
|
|
107
|
-
return (
|
|
108
|
-
<div className="palov" onClick={() => app.palClose()}>
|
|
109
|
-
<div className="pal" onClick={(e) => e.stopPropagation()}>
|
|
110
|
-
<input autoFocus placeholder="jump to…" value={app.palQuery}
|
|
111
|
-
onChange={(e) => app.setPalQuery(e.target.value)}
|
|
112
|
-
onKeyDown={(e) => {
|
|
113
|
-
if (e.key === 'ArrowDown') { e.preventDefault(); app.palMove(1) }
|
|
114
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); app.palMove(-1) }
|
|
115
|
-
else if (e.key === 'Enter') app.palRun()
|
|
116
|
-
else if (e.key === 'Escape') app.palClose()
|
|
117
|
-
}} />
|
|
118
|
-
<div className="prows">
|
|
119
|
-
{app.palResults.map((jp, n) => (
|
|
120
|
-
<div key={jp.id} className={`prow${n === app.palIdx ? ' on' : ''}`}
|
|
121
|
-
onMouseEnter={() => app.setPalIdx(n)}
|
|
122
|
-
onClick={() => { app.palClose(); jp.run() }}>
|
|
123
|
-
<span className="plabel">{jp.label}</span>
|
|
124
|
-
<span className="pchip">{jp.section}</span>
|
|
125
|
-
</div>
|
|
126
|
-
))}
|
|
127
|
-
{app.palResults.length === 0 ? <div className="prow none">no match</div> : null}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
)
|
|
132
|
-
})
|
|
45
|
+
;(window as unknown as { app: HomeSt }).app = home
|
|
46
|
+
home.boot()
|
|
133
47
|
|
|
134
48
|
// remark box on the ASSET view too: remarks land in .shipkit/assets/notes.md
|
|
135
49
|
// and come back as `remarks` (exchange both ways, media included)
|
|
136
|
-
const AssetRemarks = observer(() => {
|
|
50
|
+
const AssetRemarks = observer(({ app }: { app: RepoSt }) => {
|
|
137
51
|
const m = app.media
|
|
138
52
|
const c = app.cols
|
|
139
53
|
if (m.curId == null) return null
|
|
@@ -151,7 +65,7 @@ const AssetRemarks = observer(() => {
|
|
|
151
65
|
)
|
|
152
66
|
})
|
|
153
67
|
|
|
154
|
-
const Main = observer(() => {
|
|
68
|
+
const Main = observer(({ app }: { app: RepoSt }) => {
|
|
155
69
|
const m = app.media
|
|
156
70
|
const errs = m.errors.length > 0 ? <div className="errs">{m.errors.join('\n')}</div> : null
|
|
157
71
|
if (app.sel?.t === 'item') return (
|
|
@@ -172,6 +86,7 @@ const Main = observer(() => {
|
|
|
172
86
|
)
|
|
173
87
|
if (app.sel?.t === 'col') return <main>{errs}<ColGrid app={app} id={app.sel.id} /></main>
|
|
174
88
|
if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
|
|
89
|
+
if (app.sel?.t === 'admin') return <main>{errs}<div className="stagewrap"><AdminPage app={app} /></div></main>
|
|
175
90
|
if (app.sel?.t === 'status') return <main>{errs}<div className="stagewrap"><StatusCard app={app} /></div></main>
|
|
176
91
|
if (app.sel?.t === 'cat') return <main>{errs}<CatGrid app={app} /></main>
|
|
177
92
|
if (app.sel?.t === 'asset') return (
|
|
@@ -184,7 +99,7 @@ const Main = observer(() => {
|
|
|
184
99
|
<AssetStage app={app} />
|
|
185
100
|
<VariantsBar app={app} />
|
|
186
101
|
</div>
|
|
187
|
-
<AssetRemarks />
|
|
102
|
+
<AssetRemarks app={app} />
|
|
188
103
|
<div className="help"><kbd>←</kbd><kbd>→</kbd> roll · <kbd>Enter</kbd> pick · <kbd>⇧A</kbd> alt · <kbd>X</kbd> discard — picks ship via the kit's ship hook when present · category title = grid view</div>
|
|
189
104
|
</main>
|
|
190
105
|
)
|
|
@@ -193,78 +108,207 @@ const Main = observer(() => {
|
|
|
193
108
|
return (
|
|
194
109
|
<main>
|
|
195
110
|
{errs}
|
|
196
|
-
<div className="stagewrap"
|
|
197
|
-
{allClear ?
|
|
198
|
-
:
|
|
199
|
-
</div
|
|
111
|
+
<div className="stagewrap">
|
|
112
|
+
{allClear ? <div className="ph">{app.filter === 'red' ? 'no reds — nothing is blocked ✨' : 'all clear — nothing needs you ✨'}</div>
|
|
113
|
+
: <ReadmeView app={app} />}
|
|
114
|
+
</div>
|
|
200
115
|
</main>
|
|
201
116
|
)
|
|
202
117
|
})
|
|
203
118
|
|
|
204
119
|
const FILTER_BY_KEY: Record<string, Filter | undefined> = { a: 'all', n: 'needs', r: 'red' }
|
|
205
120
|
|
|
121
|
+
/** a panel never scrolls itself: its content scrolls where it means to (the
|
|
122
|
+
* list's rows, the tree, the page), and a panel's own scrollbar is a dark
|
|
123
|
+
* band in WebKit, the macOS app's engine, which draws classic scrollbars.
|
|
124
|
+
* overflow is the one inline style the library lets a panel set */
|
|
125
|
+
const PANEL = { overflow: 'hidden' } as const
|
|
126
|
+
|
|
127
|
+
/** a drag of a separator ended: the width of the panel on its left, in px,
|
|
128
|
+
* from the group's layout (a percentage of the group's width) */
|
|
129
|
+
const leftPx = (el: HTMLElement | null, layout: Layout, id: string): number | null => {
|
|
130
|
+
const pct = layout[id]
|
|
131
|
+
const w = el?.getBoundingClientRect().width
|
|
132
|
+
return pct == null || w == null || w === 0 ? null : (pct / 100) * w
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** a repo's sidebar and its main area, the sidebar's width dragged by the
|
|
136
|
+
* separator between them and kept per browser */
|
|
137
|
+
const Body = observer(({ app }: { app: RepoSt }) => {
|
|
138
|
+
const l = app.home.layout
|
|
139
|
+
const el = useRef<HTMLDivElement | null>(null)
|
|
140
|
+
return (
|
|
141
|
+
<Group className="appbody" orientation="horizontal" elementRef={el}
|
|
142
|
+
onLayoutChanged={(layout, meta) => {
|
|
143
|
+
if (!meta.isUserInteraction) return
|
|
144
|
+
const px = leftPx(el.current, layout, 'sidebar')
|
|
145
|
+
if (px != null) l.setSidebarWidth(px)
|
|
146
|
+
}}>
|
|
147
|
+
<Panel id="sidebar" defaultSize={l.sidebarWidth} minSize={SIDEBAR.min} maxSize={SIDEBAR.max} groupResizeBehavior="preserve-pixel-size" className="pside" style={PANEL}>
|
|
148
|
+
<Sidebar app={app} />
|
|
149
|
+
</Panel>
|
|
150
|
+
<Separator className="rsep" />
|
|
151
|
+
<Panel id="main" minSize={320} className="pmain" style={PANEL}>
|
|
152
|
+
<Main app={app} />
|
|
153
|
+
</Panel>
|
|
154
|
+
</Group>
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
/** one repo's page. Every repo visited keeps its region mounted, hidden while
|
|
159
|
+
* another one shows, so its scroll, its frames and its component state are
|
|
160
|
+
* there when it comes back; a hidden repo polls nothing, so it repaints
|
|
161
|
+
* nothing */
|
|
162
|
+
const RepoView = observer(({ app }: { app: RepoSt }) => {
|
|
163
|
+
const on = app.isShown
|
|
164
|
+
return (
|
|
165
|
+
<div className="repo" hidden={!on}>
|
|
166
|
+
{notUp(app) ? <NotUp app={app.home} /> : (
|
|
167
|
+
<>
|
|
168
|
+
{/* on the home the strip carries the repo's bar */}
|
|
169
|
+
{app.home.machine == null ? <HeaderBar app={app} /> : null}
|
|
170
|
+
<Body app={app} />
|
|
171
|
+
{on ? <ComfyOverlay app={app} /> : null}
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
/** the keys of the page: they act on the repo shown, and on nothing else */
|
|
179
|
+
function onKey(e: KeyboardEvent): void {
|
|
180
|
+
// the command keys beat the focus check: they work from anywhere, any page
|
|
181
|
+
const pk = pageKey(e)
|
|
182
|
+
if (pk != null) {
|
|
183
|
+
e.preventDefault()
|
|
184
|
+
if (pk.t === 'picker') home.palette.toggle()
|
|
185
|
+
else if (pk.t === 'dock') home.dock.cycle()
|
|
186
|
+
else if (pk.t === 'home' || pk.t === 'planned' || pk.t === 'activity') {
|
|
187
|
+
home.palette.close()
|
|
188
|
+
if (home.machine != null) home.navigate(PLACE_PATH[pk.t === 'home' ? 'overview' : pk.t])
|
|
189
|
+
} else {
|
|
190
|
+
const p = home.projects.list[pk.n - 1]
|
|
191
|
+
home.palette.close()
|
|
192
|
+
if (p != null) home.projects.click(p.name)
|
|
193
|
+
}
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
if (home.palette.open) return // the picker's input owns its keys
|
|
197
|
+
const app = home.shown
|
|
198
|
+
if (app == null) return
|
|
199
|
+
const t = e.target as HTMLElement
|
|
200
|
+
if (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') return
|
|
201
|
+
// the atelier owns its roll keys while an asset is open, and
|
|
202
|
+
// says so by consuming them
|
|
203
|
+
if (app.sel?.t === 'asset' && app.media.key(e.key)) {
|
|
204
|
+
e.preventDefault()
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
const sk = sectionKey(e)
|
|
208
|
+
if (sk != null) {
|
|
209
|
+
const s = repoSections(app).find((x) => x.id === sk)
|
|
210
|
+
if (s != null) {
|
|
211
|
+
e.preventDefault()
|
|
212
|
+
s.go()
|
|
213
|
+
}
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if (isSearchKey(e)) {
|
|
217
|
+
e.preventDefault()
|
|
218
|
+
app.searchEl?.focus()
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
222
|
+
if (e.key === FOLLOW_KEY && (!app.live.chart.follow || !home.dock.chart.follow)) {
|
|
223
|
+
e.preventDefault()
|
|
224
|
+
app.live.chart.attach()
|
|
225
|
+
home.dock.chart.attach()
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
const f = FILTER_BY_KEY[e.key.toLowerCase()]
|
|
229
|
+
if (f != null) {
|
|
230
|
+
e.preventDefault()
|
|
231
|
+
app.setFilter(f)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** a drag of a separator ended: the height of the panel below it, in px */
|
|
236
|
+
const belowPx = (el: HTMLElement | null, layout: Layout, id: string): number | null => {
|
|
237
|
+
const pct = layout[id]
|
|
238
|
+
const h = el?.getBoundingClientRect().height
|
|
239
|
+
return pct == null || h == null || h === 0 ? null : (pct / 100) * h
|
|
240
|
+
}
|
|
241
|
+
|
|
206
242
|
const App = observer(() => {
|
|
207
243
|
useEffect(() => {
|
|
208
|
-
const onKey = (e: KeyboardEvent): void => {
|
|
209
|
-
// ⌘J/^J beats the focus check — the palette must open from anywhere
|
|
210
|
-
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'j') {
|
|
211
|
-
e.preventDefault()
|
|
212
|
-
app.palToggle()
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
const t = e.target as HTMLElement
|
|
216
|
-
if (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') return
|
|
217
|
-
if (app.palOpen) return // the palette input owns its keys
|
|
218
|
-
// the atelier owns its roll keys while an asset is open, and
|
|
219
|
-
// says so by consuming them
|
|
220
|
-
if (app.sel?.t === 'asset' && app.media.key(e.key)) {
|
|
221
|
-
e.preventDefault()
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
if (e.key === '/') {
|
|
225
|
-
e.preventDefault()
|
|
226
|
-
document.getElementById('treeq')?.focus()
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
230
|
-
if (e.key === LIVE_KEY) {
|
|
231
|
-
e.preventDefault()
|
|
232
|
-
app.live.toggle()
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
if (e.key === FOLLOW_KEY && !app.live.chart.follow) {
|
|
236
|
-
e.preventDefault()
|
|
237
|
-
app.live.chart.attach()
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
const f = FILTER_BY_KEY[e.key.toLowerCase()]
|
|
241
|
-
if (f != null) {
|
|
242
|
-
e.preventDefault()
|
|
243
|
-
app.setFilter(f)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
244
|
document.addEventListener('keydown', onKey)
|
|
247
245
|
return () => document.removeEventListener('keydown', onKey)
|
|
248
246
|
}, [])
|
|
247
|
+
const l = home.layout
|
|
248
|
+
const d = home.dock
|
|
249
|
+
const el = useRef<HTMLDivElement | null>(null)
|
|
250
|
+
const vel = useRef<HTMLDivElement | null>(null)
|
|
251
|
+
const dockRef = usePanelRef()
|
|
252
|
+
// the dock's size moves the boundary, and only the boundary: its first
|
|
253
|
+
// height is the panel's default, read before the first render. After the
|
|
254
|
+
// render, so the panel already holds the bounds of its new size, and
|
|
255
|
+
// eased (dock-anim.ts), so the boundary slides rather than jumps
|
|
256
|
+
const px = d.px
|
|
257
|
+
useLayoutEffect(() => {
|
|
258
|
+
const p = dockRef.current
|
|
259
|
+
if (p != null && Math.abs(p.getSize().inPixels - px) >= 1) animateDock(vel.current, () => p.resize(px))
|
|
260
|
+
}, [px, dockRef])
|
|
249
261
|
return (
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
// the page over the dock: the project list, its width dragged by the
|
|
263
|
+
// separator and kept per browser, then the page; under both, the
|
|
264
|
+
// dock, on every page. The list, the strip and the dock stay mounted
|
|
265
|
+
// across pages; only the region under the strip changes
|
|
266
|
+
<>
|
|
267
|
+
<Group className="appv" orientation="vertical" elementRef={vel}
|
|
268
|
+
onLayoutChanged={(layout, meta) => {
|
|
269
|
+
if (!meta.isUserInteraction || d.size !== 'open') return
|
|
270
|
+
const px = belowPx(vel.current, layout, 'dock')
|
|
271
|
+
if (px == null) return
|
|
272
|
+
// a drag past a bound lands on it: the panel's own bounds
|
|
273
|
+
// stay 0 and the max, so the key can take it to any size
|
|
274
|
+
d.setHeight(px)
|
|
275
|
+
if (Math.abs(px - d.height) >= 1) dockRef.current?.resize(d.height)
|
|
276
|
+
}}>
|
|
277
|
+
<Panel id="top" minSize={200} className="apptop" style={PANEL}>
|
|
278
|
+
<Group className="app" orientation="horizontal" elementRef={el}
|
|
279
|
+
onLayoutChanged={(layout, meta) => {
|
|
280
|
+
if (!meta.isUserInteraction) return
|
|
281
|
+
const px = leftPx(el.current, layout, 'projects')
|
|
282
|
+
if (px != null) l.setListWidth(px)
|
|
283
|
+
}}>
|
|
284
|
+
<Panel id="projects" defaultSize={l.listWidth} minSize={LIST.min} maxSize={LIST.max} groupResizeBehavior="preserve-pixel-size" className="pjpanel" style={PANEL}>
|
|
285
|
+
<ProjectList app={home} />
|
|
286
|
+
</Panel>
|
|
287
|
+
<Separator className="rsep" />
|
|
288
|
+
<Panel id="page" minSize={320} className="appcol" style={PANEL}>
|
|
289
|
+
{home.machine == null ? null : <HomeStrip app={home} />}
|
|
290
|
+
{home.page === 'overview' ? <Overview app={home} /> : null}
|
|
291
|
+
{home.page === 'planned' ? <PlannedView app={home} /> : null}
|
|
292
|
+
{home.page === 'activity' ? <ActivityView app={home} /> : null}
|
|
293
|
+
{home.page === 'archived' ? <ArchivedView app={home} /> : null}
|
|
294
|
+
{home.visited.map((r) => <RepoView key={r.key} app={r} />)}
|
|
295
|
+
</Panel>
|
|
296
|
+
</Group>
|
|
297
|
+
</Panel>
|
|
298
|
+
<Separator className={`rsep hsep${d.size === 'open' ? '' : ' fixed'}`} disabled={d.size !== 'open'} />
|
|
299
|
+
<Panel id="dock" panelRef={dockRef} defaultSize={d.px} minSize={0} maxSize={DOCK.max} groupResizeBehavior="preserve-pixel-size" className="pdock" style={PANEL}>
|
|
300
|
+
<Dock app={home} />
|
|
301
|
+
</Panel>
|
|
302
|
+
</Group>
|
|
303
|
+
<Palette app={home} />
|
|
304
|
+
</>
|
|
265
305
|
)
|
|
266
306
|
})
|
|
267
307
|
|
|
308
|
+
// the shown repo's kept screen is read before the first paint, so a refresh
|
|
309
|
+
// paints it at once; a slow or absent store costs at most SCREEN_WAIT_MS
|
|
310
|
+
await Promise.race([home.shown?.cols.restore(), new Promise((ok) => setTimeout(ok, SCREEN_WAIT_MS))])
|
|
311
|
+
|
|
268
312
|
createRoot(document.getElementById('root')!).render(
|
|
269
313
|
<StrictMode>
|
|
270
314
|
<App />
|
|
@@ -126,9 +126,6 @@ export type CheckFacts = {
|
|
|
126
126
|
* (serve/hub-problems.ts): the header shows it, never the tree */
|
|
127
127
|
export type HubProblem = { id: string; text: string; severity: 'red' | 'yellow' }
|
|
128
128
|
|
|
129
|
-
/** the hub's daily health pass timer, as the resources feed sends it
|
|
130
|
-
* (serve/health-schedule.ts). null = this hub runs no timer */
|
|
131
|
-
export type Schedule = { everyMs: number; lastPassAt: number | null; nextAt: number; running: boolean }
|
|
132
129
|
|
|
133
130
|
/** one declared slow suite, with the case test-speed doctrine demands */
|
|
134
131
|
export type SlowSuite = { file: string; catches: string; whyNotFast: string }
|