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.
Files changed (210) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +1 -1
  3. package/package.json +3 -1
  4. package/src/assets/run-assets.ts +3 -8
  5. package/src/check.ts +40 -2
  6. package/src/checks/SK001-package-json-shape.check.ts +14 -3
  7. package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
  8. package/src/checks/SK004-gitignore-policy.check.ts +11 -3
  9. package/src/checks/SK005-rvlib-versions.check.ts +13 -1
  10. package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
  11. package/src/checks/SK007-github-visibility.check.ts +9 -9
  12. package/src/checks/SK008-reviewer-agent.check.ts +14 -8
  13. package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
  14. package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
  15. package/src/checks/SK015-nested-repo.check.ts +5 -1
  16. package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
  17. package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
  18. package/src/checks/SK024-identity-colors.check.ts +54 -0
  19. package/src/checks/SK025-release-drift.check.ts +58 -0
  20. package/src/checks/SK026-asks-autoclose.check.ts +44 -0
  21. package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
  22. package/src/checks/SK028-readme.check.ts +79 -0
  23. package/src/cli/cmd_Assets.ts +1 -1
  24. package/src/cli/cmd_Check.ts +26 -1
  25. package/src/cli/cmd_Ci.ts +23 -5
  26. package/src/cli/cmd_CrossRepoActivity.ts +116 -0
  27. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  28. package/src/cli/cmd_Home.ts +170 -0
  29. package/src/cli/cmd_Keys.ts +17 -3
  30. package/src/cli/cmd_Overview.ts +98 -0
  31. package/src/cli/cmd_Planned.ts +117 -0
  32. package/src/cli.ts +30 -2
  33. package/src/collections/checks.ts +3 -2
  34. package/src/collections/providers.ts +1 -1
  35. package/src/collections/test-time.ts +10 -1
  36. package/src/comfy/comfy-panel.ts +1 -16
  37. package/src/core/agent-spawn.ts +46 -0
  38. package/src/core/archived.ts +82 -0
  39. package/src/core/attach.ts +14 -9
  40. package/src/core/canonical-scripts.ts +12 -3
  41. package/src/core/check-file-names.ts +8 -0
  42. package/src/core/check-runner.ts +80 -30
  43. package/src/core/discover-checks.ts +38 -18
  44. package/src/core/file-stamp.ts +20 -0
  45. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  46. package/src/core/identity-color.ts +203 -0
  47. package/src/core/identity.ts +91 -0
  48. package/src/core/job-follow.ts +102 -0
  49. package/src/core/job-run.ts +27 -0
  50. package/src/core/jobs.ts +87 -27
  51. package/src/core/legacy-shapes.ts +1 -14
  52. package/src/core/machine-db.ts +83 -0
  53. package/src/core/one-lib.ts +85 -0
  54. package/src/core/port-holders.ts +43 -0
  55. package/src/core/repo-facts.ts +14 -0
  56. package/src/core/retention.ts +222 -0
  57. package/src/core/retired-checks.ts +7 -0
  58. package/src/core/run-check.ts +12 -4
  59. package/src/core/run-ci.ts +46 -12
  60. package/src/core/run-fix.ts +7 -2
  61. package/src/core/run-fixers.ts +10 -6
  62. package/src/core/slots.ts +5 -0
  63. package/src/core/statement-end.ts +17 -0
  64. package/src/core/wire.ts +11 -0
  65. package/src/fleet/scan-fleet.ts +2 -1
  66. package/src/home/activity-shape.ts +135 -0
  67. package/src/home/activity.ts +249 -0
  68. package/src/home/attach.ts +95 -0
  69. package/src/home/autostart.ts +108 -0
  70. package/src/home/global-units.ts +87 -0
  71. package/src/home/gui-app/ShipkitGui.swift +159 -0
  72. package/src/home/gui-app.ts +99 -0
  73. package/src/home/handoff.ts +51 -0
  74. package/src/home/home-launchd.ts +104 -0
  75. package/src/home/home-paths.ts +88 -0
  76. package/src/home/home-routes.ts +235 -0
  77. package/src/home/home-start.ts +105 -0
  78. package/src/home/home-state.ts +43 -0
  79. package/src/home/home-words.ts +12 -0
  80. package/src/home/hubs.ts +286 -0
  81. package/src/home/lib-watch.ts +139 -0
  82. package/src/home/machine-stores.ts +115 -0
  83. package/src/home/open-app.ts +16 -0
  84. package/src/home/pm-config.ts +37 -0
  85. package/src/home/pm.ts +174 -0
  86. package/src/home/process-rows.ts +205 -0
  87. package/src/home/resources.ts +365 -0
  88. package/src/home/run-daemon.ts +137 -0
  89. package/src/home/run-pm.ts +150 -0
  90. package/src/home/run-unit-action.ts +25 -0
  91. package/src/home/run-web.ts +37 -0
  92. package/src/home/serve-daemon.ts +31 -0
  93. package/src/home/serve-web.ts +175 -0
  94. package/src/home/supervise.ts +361 -0
  95. package/src/home/via-hub.ts +142 -0
  96. package/src/home/widget-text.ts +119 -0
  97. package/src/home/widgets.ts +102 -0
  98. package/src/hooks/run-hooks.ts +20 -3
  99. package/src/ledger/check-stats.ts +19 -45
  100. package/src/ledger/db.ts +69 -17
  101. package/src/ledger/gate-checks.ts +22 -3
  102. package/src/ledger/git-scan.ts +31 -7
  103. package/src/ledger/house-runs.ts +43 -0
  104. package/src/ledger/ledger.ts +40 -14
  105. package/src/ledger/live-timeline.ts +4 -19
  106. package/src/ledger/run-log.ts +92 -28
  107. package/src/ledger/store.ts +245 -103
  108. package/src/ledger/world-cache.ts +64 -15
  109. package/src/plan/cadence.ts +253 -0
  110. package/src/plan/catch-up.ts +37 -0
  111. package/src/plan/plan-timeline.ts +99 -0
  112. package/src/plan/planner.ts +428 -0
  113. package/src/plan/repo-source.ts +212 -0
  114. package/src/problems/problem-line.ts +1 -1
  115. package/src/release/run-release.ts +9 -2
  116. package/src/release/unit-resolve.ts +8 -12
  117. package/src/repo-config.ts +61 -4
  118. package/src/serve/house-runs.ts +53 -0
  119. package/src/serve/hub-app/activity-st.ts +168 -0
  120. package/src/serve/hub-app/activity-view.tsx +218 -0
  121. package/src/serve/hub-app/activity.css +78 -0
  122. package/src/serve/hub-app/admin-svc.ts +41 -0
  123. package/src/serve/hub-app/admin-view.tsx +42 -0
  124. package/src/serve/hub-app/chart-view.ts +12 -5
  125. package/src/serve/hub-app/check-view.tsx +9 -9
  126. package/src/serve/hub-app/check-words.ts +3 -1
  127. package/src/serve/hub-app/checks-svc.ts +12 -8
  128. package/src/serve/hub-app/col-card.tsx +4 -4
  129. package/src/serve/hub-app/cols-svc.ts +92 -13
  130. package/src/serve/hub-app/comfy-svc.ts +12 -6
  131. package/src/serve/hub-app/comfy-view.tsx +3 -3
  132. package/src/serve/hub-app/controls.css +32 -0
  133. package/src/serve/hub-app/cycle-button.tsx +36 -0
  134. package/src/serve/hub-app/dock-anim.ts +25 -0
  135. package/src/serve/hub-app/dock-st.ts +329 -0
  136. package/src/serve/hub-app/dock-view.tsx +68 -0
  137. package/src/serve/hub-app/dock.css +35 -0
  138. package/src/serve/hub-app/floating.tsx +49 -0
  139. package/src/serve/hub-app/geometry-view.tsx +4 -4
  140. package/src/serve/hub-app/hash-route.ts +8 -2
  141. package/src/serve/hub-app/header-bar.tsx +102 -95
  142. package/src/serve/hub-app/health-svc.ts +3 -3
  143. package/src/serve/hub-app/help.tsx +10 -7
  144. package/src/serve/hub-app/home-st.ts +278 -0
  145. package/src/serve/hub-app/home-strip.css +113 -0
  146. package/src/serve/hub-app/home-strip.tsx +376 -0
  147. package/src/serve/hub-app/home.html +21 -0
  148. package/src/serve/hub-app/hub-app.tsx +221 -177
  149. package/src/serve/hub-app/hub-types.ts +0 -3
  150. package/src/serve/hub-app/hub.css +112 -52
  151. package/src/serve/hub-app/icons.tsx +34 -0
  152. package/src/serve/hub-app/index.html +7 -1
  153. package/src/serve/hub-app/keys.ts +60 -0
  154. package/src/serve/hub-app/layout-st.ts +55 -0
  155. package/src/serve/hub-app/live-svc.ts +35 -72
  156. package/src/serve/hub-app/machine-st.ts +333 -0
  157. package/src/serve/hub-app/md.ts +38 -5
  158. package/src/serve/hub-app/media-svc.ts +63 -44
  159. package/src/serve/hub-app/media-view.tsx +20 -20
  160. package/src/serve/hub-app/next-run.ts +24 -30
  161. package/src/serve/hub-app/palette-st.ts +92 -0
  162. package/src/serve/hub-app/palette-view.tsx +47 -0
  163. package/src/serve/hub-app/pill-row.tsx +65 -0
  164. package/src/serve/hub-app/planned-st.ts +107 -0
  165. package/src/serve/hub-app/planned-view.tsx +114 -0
  166. package/src/serve/hub-app/planned.css +42 -0
  167. package/src/serve/hub-app/processes-svc.ts +63 -0
  168. package/src/serve/hub-app/project-list.css +119 -0
  169. package/src/serve/hub-app/project-list.tsx +266 -0
  170. package/src/serve/hub-app/projects-svc.ts +253 -23
  171. package/src/serve/hub-app/prose-hits-st.ts +3 -3
  172. package/src/serve/hub-app/prose-hits-view.tsx +6 -6
  173. package/src/serve/hub-app/readme-svc.ts +38 -0
  174. package/src/serve/hub-app/readme-view.tsx +19 -0
  175. package/src/serve/hub-app/readme.css +11 -0
  176. package/src/serve/hub-app/repo-sections.tsx +61 -0
  177. package/src/serve/hub-app/repo-st.ts +340 -0
  178. package/src/serve/hub-app/runs-tabs.ts +1 -1
  179. package/src/serve/hub-app/screen-cache.ts +74 -0
  180. package/src/serve/hub-app/sidebar.tsx +84 -0
  181. package/src/serve/hub-app/test-view.tsx +4 -4
  182. package/src/serve/hub-app/tests-svc.ts +3 -3
  183. package/src/serve/hub-app/tests-view.tsx +41 -14
  184. package/src/serve/hub-app/timeline-chart.tsx +55 -39
  185. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  186. package/src/serve/hub-app/timeline-svc.ts +7 -6
  187. package/src/serve/hub-app/timeline-view.tsx +33 -16
  188. package/src/serve/hub-app/tree-row.tsx +2 -2
  189. package/src/serve/hub-app/tree-svc.ts +2 -2
  190. package/src/serve/hub-app/wikis-view.tsx +10 -6
  191. package/src/serve/hub-runtime.ts +550 -0
  192. package/src/serve/other-repos.ts +8 -45
  193. package/src/serve/project-gutter-route.ts +121 -20
  194. package/src/serve/project-gutter.ts +33 -11
  195. package/src/serve/readme-route.ts +57 -0
  196. package/src/serve/repo-processes.ts +89 -0
  197. package/src/serve/run-serve.ts +110 -402
  198. package/src/serve/serve-hub.ts +155 -52
  199. package/src/serve/stale-restart.ts +5 -0
  200. package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
  201. package/src/serve/warm-cell.ts +95 -0
  202. package/src/shipkit-files.ts +130 -0
  203. package/src/shipkit.ts +2 -1
  204. package/src/suite/kept-timings.ts +1 -1
  205. package/src/wiki/graph-positions.ts +1 -1
  206. package/src/checks/contracts.check.ts +0 -20
  207. package/src/serve/health-schedule.ts +0 -102
  208. package/src/serve/hub-app/hub-st.ts +0 -250
  209. package/src/serve/hub-app/project-gutter.css +0 -65
  210. 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). One surface
2
- // per repo: ONE tree of every repo resource, by path or by type, coloured by
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
- // /?f=needs · /?f=red. A row opens its item view, an asset row opens the
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 { HubSt, type Filter } from './hub-st.ts'
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, LIVE_KEY, LiveStrip } from './header-bar.tsx'
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 { TYPE_MARK } from './tree-build.ts'
23
- import { TreeRowView } from './tree-row.tsx'
24
- import { Help } from './help.tsx'
25
- import { ProjectGutter } from './project-gutter.tsx'
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
- const app = new HubSt()
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: HubSt }).app = app
30
- app.boot()
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"><div className="ph">
197
- {allClear ? (app.filter === 'red' ? 'no reds — nothing is blocked ✨' : 'all clear — nothing needs you ✨')
198
- : 'Pick a row on the left.'}
199
- </div></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
- // a flex ROW: a column mounted before `.appcol` sits left of the
251
- // header and everything under it
252
- <div className="app">
253
- <ProjectGutter app={app} />
254
- <div className="appcol">
255
- <HeaderBar app={app} />
256
- <LiveStrip app={app} />
257
- <div className="appbody">
258
- <Sidebar />
259
- <Main />
260
- </div>
261
- </div>
262
- <Palette />
263
- <ComfyOverlay app={app} />
264
- </div>
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 }