rvlib-shipkit 1.2.0 → 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 (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/assets/run-assets.ts +3 -8
  5. package/src/check.ts +20 -6
  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 +6 -2
  18. package/src/checks/SK024-identity-colors.check.ts +5 -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_Ci.ts +5 -0
  25. package/src/cli/cmd_CrossRepoActivity.ts +116 -0
  26. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  27. package/src/cli/cmd_Keys.ts +17 -3
  28. package/src/cli/cmd_Overview.ts +98 -0
  29. package/src/cli/cmd_Planned.ts +117 -0
  30. package/src/cli.ts +9 -1
  31. package/src/collections/checks.ts +3 -2
  32. package/src/collections/providers.ts +1 -1
  33. package/src/collections/test-time.ts +10 -1
  34. package/src/core/agent-spawn.ts +46 -0
  35. package/src/core/archived.ts +82 -0
  36. package/src/core/canonical-scripts.ts +12 -3
  37. package/src/core/check-runner.ts +40 -12
  38. package/src/core/discover-checks.ts +2 -0
  39. package/src/core/file-stamp.ts +20 -0
  40. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  41. package/src/core/identity-color.ts +1 -1
  42. package/src/core/repo-facts.ts +14 -0
  43. package/src/core/retention.ts +14 -5
  44. package/src/core/retired-checks.ts +7 -0
  45. package/src/core/run-check.ts +6 -2
  46. package/src/core/run-ci.ts +43 -12
  47. package/src/core/run-fix.ts +7 -2
  48. package/src/core/run-fixers.ts +10 -6
  49. package/src/core/wire.ts +11 -0
  50. package/src/fleet/scan-fleet.ts +2 -1
  51. package/src/home/activity-shape.ts +135 -0
  52. package/src/home/activity.ts +249 -0
  53. package/src/home/global-units.ts +3 -2
  54. package/src/home/home-routes.ts +32 -5
  55. package/src/home/pm.ts +37 -4
  56. package/src/home/process-rows.ts +205 -0
  57. package/src/home/resources.ts +35 -8
  58. package/src/home/run-daemon.ts +6 -6
  59. package/src/home/run-pm.ts +1 -0
  60. package/src/home/serve-web.ts +6 -3
  61. package/src/home/supervise.ts +16 -1
  62. package/src/home/widget-text.ts +119 -0
  63. package/src/hooks/run-hooks.ts +20 -3
  64. package/src/ledger/db.ts +1 -1
  65. package/src/ledger/gate-checks.ts +17 -3
  66. package/src/ledger/house-runs.ts +43 -0
  67. package/src/ledger/live-timeline.ts +4 -19
  68. package/src/ledger/run-log.ts +39 -10
  69. package/src/ledger/store.ts +19 -2
  70. package/src/ledger/world-cache.ts +51 -4
  71. package/src/plan/cadence.ts +21 -5
  72. package/src/plan/catch-up.ts +37 -0
  73. package/src/plan/plan-timeline.ts +99 -0
  74. package/src/plan/planner.ts +138 -13
  75. package/src/plan/repo-source.ts +120 -8
  76. package/src/release/run-release.ts +9 -2
  77. package/src/release/unit-resolve.ts +8 -12
  78. package/src/repo-config.ts +11 -3
  79. package/src/serve/house-runs.ts +53 -0
  80. package/src/serve/hub-app/activity-st.ts +168 -0
  81. package/src/serve/hub-app/activity-view.tsx +218 -0
  82. package/src/serve/hub-app/activity.css +78 -0
  83. package/src/serve/hub-app/admin-svc.ts +41 -0
  84. package/src/serve/hub-app/admin-view.tsx +42 -0
  85. package/src/serve/hub-app/chart-view.ts +12 -5
  86. package/src/serve/hub-app/check-words.ts +2 -0
  87. package/src/serve/hub-app/controls.css +32 -0
  88. package/src/serve/hub-app/cycle-button.tsx +36 -0
  89. package/src/serve/hub-app/dock-anim.ts +25 -0
  90. package/src/serve/hub-app/dock-st.ts +329 -0
  91. package/src/serve/hub-app/dock-view.tsx +68 -0
  92. package/src/serve/hub-app/dock.css +35 -0
  93. package/src/serve/hub-app/hash-route.ts +7 -1
  94. package/src/serve/hub-app/header-bar.tsx +79 -104
  95. package/src/serve/hub-app/home-st.ts +49 -12
  96. package/src/serve/hub-app/home-strip.css +65 -22
  97. package/src/serve/hub-app/home-strip.tsx +228 -97
  98. package/src/serve/hub-app/home.html +4 -0
  99. package/src/serve/hub-app/hub-app.tsx +96 -160
  100. package/src/serve/hub-app/hub.css +77 -37
  101. package/src/serve/hub-app/icons.tsx +23 -3
  102. package/src/serve/hub-app/index.html +4 -0
  103. package/src/serve/hub-app/keys.ts +42 -7
  104. package/src/serve/hub-app/live-svc.ts +24 -64
  105. package/src/serve/hub-app/machine-st.ts +64 -1
  106. package/src/serve/hub-app/md.ts +38 -5
  107. package/src/serve/hub-app/next-run.ts +1 -1
  108. package/src/serve/hub-app/palette-st.ts +11 -7
  109. package/src/serve/hub-app/palette-view.tsx +47 -0
  110. package/src/serve/hub-app/pill-row.tsx +65 -0
  111. package/src/serve/hub-app/planned-st.ts +21 -85
  112. package/src/serve/hub-app/planned-view.tsx +12 -7
  113. package/src/serve/hub-app/planned.css +5 -2
  114. package/src/serve/hub-app/project-list.css +43 -21
  115. package/src/serve/hub-app/project-list.tsx +122 -23
  116. package/src/serve/hub-app/projects-svc.ts +120 -12
  117. package/src/serve/hub-app/readme-svc.ts +38 -0
  118. package/src/serve/hub-app/readme-view.tsx +19 -0
  119. package/src/serve/hub-app/readme.css +11 -0
  120. package/src/serve/hub-app/repo-sections.tsx +61 -0
  121. package/src/serve/hub-app/repo-st.ts +25 -6
  122. package/src/serve/hub-app/runs-tabs.ts +1 -1
  123. package/src/serve/hub-app/sidebar.tsx +84 -0
  124. package/src/serve/hub-app/tests-view.tsx +31 -4
  125. package/src/serve/hub-app/timeline-chart.tsx +31 -27
  126. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  127. package/src/serve/hub-app/timeline-svc.ts +2 -2
  128. package/src/serve/hub-app/timeline-view.tsx +20 -3
  129. package/src/serve/hub-runtime.ts +17 -9
  130. package/src/serve/other-repos.ts +8 -45
  131. package/src/serve/project-gutter-route.ts +100 -20
  132. package/src/serve/project-gutter.ts +33 -11
  133. package/src/serve/readme-route.ts +57 -0
  134. package/src/serve/serve-hub.ts +13 -8
  135. package/src/serve/tracked-watch.ts +33 -1
  136. package/src/shipkit-files.ts +20 -4
  137. package/src/checks/SK023-stores-under-cap.check.ts +0 -49
  138. package/src/checks/contracts.check.ts +0 -20
@@ -1,37 +1,43 @@
1
1
  // THE page (Bun.serve HTML import — no vite, no build step), one single page
2
- // app for every page the home serves ('/', '/planned', '/r/<repo>/') and for a
2
+ // app for every page the home serves ('/', '/planned', '/activity', '/r/<repo>/') and for a
3
3
  // hub serving on its own at '/'. mobx+react per build/app-state-tree: HomeSt
4
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 ONE tree of every repo
6
- // resource, by path or by type, coloured by state in the label itself, over
7
- // one severity filter persisted at ?f=needs · ?f=red. A row opens its item
8
- // view, an asset row opens the atelier.
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
7
+ // state in the label itself, over one severity filter persisted at
8
+ // ?f=needs · ?f=red. A row opens its item view, an asset row opens the atelier.
9
9
  import { runInAction } from 'mobx'
10
10
  import { observer } from 'mobx-react-lite'
11
- import { StrictMode, useEffect, useRef } from 'react'
11
+ import { StrictMode, useEffect, useLayoutEffect, useRef } from 'react'
12
12
  import { createRoot } from 'react-dom/client'
13
13
  import { ColCard, ColGrid, StatusCard } from './col-card.tsx'
14
14
  import { WIKIS, WikisView } from './wikis-view.tsx'
15
15
  import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
16
16
  import type { Filter, RepoSt } from './repo-st.ts'
17
- import { HomeSt } from './home-st.ts'
17
+ import { HomeSt, PLACE_PATH } from './home-st.ts'
18
18
  import { TimelineView } from './timeline-view.tsx'
19
- import { HeaderBar, LIVE_KEY, LiveStrip } from './header-bar.tsx'
19
+ import { HeaderBar, notUp } from './header-bar.tsx'
20
20
  import { FOLLOW_KEY } from './timeline-chart.tsx'
21
21
  import { CheckCard } from './check-view.tsx'
22
22
  import { TestCard } from './test-view.tsx'
23
23
  import { TestsPage } from './tests-view.tsx'
24
24
  import { AssetStage, CatGrid, InfoChips, MediaTopbar, ToolsBar, VariantsBar } from './media-view.tsx'
25
- import { TYPE_MARK } from './tree-build.ts'
26
- import { TreeRowView } from './tree-row.tsx'
27
- import { Help } from './help.tsx'
25
+ import { Sidebar } from './sidebar.tsx'
26
+ import { AdminPage } from './admin-view.tsx'
27
+ import { ReadmeView } from './readme-view.tsx'
28
28
  import { ProjectList } from './project-list.tsx'
29
- import { Group, Panel, Separator, type Layout } from 'react-resizable-panels'
29
+ import { Group, Panel, Separator, usePanelRef, type Layout } from 'react-resizable-panels'
30
30
  import { LIST, SIDEBAR } from './layout-st.ts'
31
- import { HomeStrip, NotUp, Overview } from './home-strip.tsx'
31
+ import { ArchivedView, HomeStrip, NotUp, Overview } from './home-strip.tsx'
32
32
  import { PlannedView } from './planned-view.tsx'
33
+ import { ActivityView } from './activity-view.tsx'
34
+ import { Palette } from './palette-view.tsx'
33
35
  import { SCREEN_WAIT_MS } from './screen-cache.ts'
34
- import { pageKey } from './keys.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'
35
41
 
36
42
  // home.html marks the home's page; index.html is a hub serving on its own
37
43
  const home = new HomeSt({ home: document.body.dataset.home != null })
@@ -39,112 +45,6 @@ const home = new HomeSt({ home: document.body.dataset.home != null })
39
45
  ;(window as unknown as { app: HomeSt }).app = home
40
46
  home.boot()
41
47
 
42
- // ── the tree: every resource of the repo, one row deep in its path ──────
43
- // the row itself is tree-row.tsx, so a test renders it headless
44
-
45
- const Tree = observer(({ app }: { app: RepoSt }) => {
46
- const t = app.tree
47
- return (
48
- // roving focus: the arrows belong to the tree only while it HAS the
49
- // focus, so the atelier keeps its own keys
50
- <div className="tree" tabIndex={0} onKeyDown={(e) => { if (t.key(e.key)) e.preventDefault() }}>
51
- {t.rows.map((r) => <TreeRowView key={r.key} app={app} row={r} />)}
52
- {!app.cols.loaded && !app.cols.cached ? <div className="tempty twait"><span className="spin" />reading the repo…</div> : null}
53
- {app.cols.loaded && 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}
54
- </div>
55
- )
56
- })
57
-
58
- /** what every glyph in the type slot means, behind one `(?)` in the header */
59
- const TypeLegend = () => (
60
- <Help text={
61
- <span className="legend">
62
- {Object.entries(TYPE_MARK).map(([kind, m]) => (
63
- <span key={kind} className="lrow"><i className={`ty ${m.hue}`}>{m.glyph}</i>{m.word}</span>
64
- ))}
65
- </span>
66
- } />
67
- )
68
-
69
- const TreeTabs = observer(({ app }: { app: RepoSt }) => (
70
- <span className="seg">
71
- {(['path', 'type'] as const).map((t) => (
72
- <button key={t} className={app.tree.tab === t ? 'on' : ''} onClick={() => app.tree.setTab(t)}>by {t}</button>
73
- ))}
74
- </span>
75
- ))
76
-
77
- // segmented severity filter — all · needs (yellow+red) · red, the address
78
- // rides at /?f=… so a filtered view is shareable
79
- // the shortcut letter rides INSIDE its own button, so the binding is
80
- // discoverable without a legend
81
- const FILTER_KEY = { all: 'a', needs: 'n', red: 'r' } as const
82
- const SegFilter = observer(({ app }: { app: RepoSt }) => (
83
- <span className="seg">
84
- {(['all', 'needs', 'red'] as const).map((f) => (
85
- <button key={f} className={app.filter === f ? 'on' : ''} onClick={() => app.setFilter(f)}>
86
- {f}<span className="sk">{FILTER_KEY[f]}</span>
87
- </button>
88
- ))}
89
- </span>
90
- ))
91
-
92
- const Sidebar = observer(({ app }: { app: RepoSt }) => (
93
- <aside>
94
- <div className="shead">
95
- resources
96
- <SegFilter app={app} />
97
- </div>
98
- <div className="ssearch">
99
- <input ref={(el) => { app.searchEl = el }} placeholder="filter… (/ to focus, ⌘K to jump)" value={app.query}
100
- onChange={(e) => app.setQuery(e.target.value)}
101
- onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery('') }} />
102
- </div>
103
- <div className="stabs">
104
- <TreeTabs app={app} />
105
- <span className="sp" />
106
- <TypeLegend />
107
- <span className="cnt">{app.tree.visible.length}</span>
108
- </div>
109
- {/* its line is held while the repo is read, so the tree never drops under it */}
110
- {app.tree.whenLine === '' && app.cols.loaded ? null : <div className="swhen">{app.tree.whenLine === '' ? '\u00a0' : app.tree.whenLine}</div>}
111
- {app.media.dead != null ? <div className="tempty"><span className="warnline">media API down: {app.media.dead}</span></div> : null}
112
- <Tree app={app} />
113
- </aside>
114
- ))
115
-
116
- // ── the one picker, cmd+k: home and planned, the shown repo's targets, then
117
- // every other repo ──────────────────────────────────────────────────────
118
- const Palette = observer(({ app }: { app: HomeSt }) => {
119
- const p = app.palette
120
- if (!p.open) return null
121
- return (
122
- <div className="palov" onClick={() => p.close()}>
123
- <div className="pal" onClick={(e) => e.stopPropagation()}>
124
- <input autoFocus placeholder="jump to…" value={p.query}
125
- onChange={(e) => p.setQuery(e.target.value)}
126
- onKeyDown={(e) => {
127
- if (e.key === 'ArrowDown') { e.preventDefault(); p.move(1) }
128
- else if (e.key === 'ArrowUp') { e.preventDefault(); p.move(-1) }
129
- else if (e.key === 'Enter') p.run()
130
- else if (e.key === 'Escape') p.close()
131
- }} />
132
- <div className="prows">
133
- {p.results.map((jp, n) => (
134
- <div key={jp.id} className={`prow${n === p.idx ? ' on' : ''}`}
135
- onMouseEnter={() => p.setIdx(n)}
136
- onClick={() => p.run(jp)}>
137
- <span className="plabel">{jp.label}</span>
138
- <span className="pchip">{jp.section}</span>
139
- </div>
140
- ))}
141
- {p.results.length === 0 ? <div className="prow none">no match</div> : null}
142
- </div>
143
- </div>
144
- </div>
145
- )
146
- })
147
-
148
48
  // remark box on the ASSET view too: remarks land in .shipkit/assets/notes.md
149
49
  // and come back as `remarks` (exchange both ways, media included)
150
50
  const AssetRemarks = observer(({ app }: { app: RepoSt }) => {
@@ -186,6 +86,7 @@ const Main = observer(({ app }: { app: RepoSt }) => {
186
86
  )
187
87
  if (app.sel?.t === 'col') return <main>{errs}<ColGrid app={app} id={app.sel.id} /></main>
188
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>
189
90
  if (app.sel?.t === 'status') return <main>{errs}<div className="stagewrap"><StatusCard app={app} /></div></main>
190
91
  if (app.sel?.t === 'cat') return <main>{errs}<CatGrid app={app} /></main>
191
92
  if (app.sel?.t === 'asset') return (
@@ -207,10 +108,10 @@ const Main = observer(({ app }: { app: RepoSt }) => {
207
108
  return (
208
109
  <main>
209
110
  {errs}
210
- <div className="stagewrap"><div className="ph">
211
- {allClear ? (app.filter === 'red' ? 'no reds — nothing is blocked ✨' : 'all clear — nothing needs you ✨')
212
- : 'Pick a row on the left.'}
213
- </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>
214
115
  </main>
215
116
  )
216
117
  })
@@ -254,16 +155,6 @@ const Body = observer(({ app }: { app: RepoSt }) => {
254
155
  )
255
156
  })
256
157
 
257
- /** the repo's hub has not answered on the home: its reason and its fix show
258
- * instead of the page. A screen this browser kept shows meanwhile, until
259
- * the machine says the hub is not up */
260
- const notUp = (app: RepoSt): boolean => {
261
- const m = app.home.machine
262
- if (m == null || app.started) return false
263
- if (!app.cols.cached) return true
264
- return m.fresh && m.worker(app.key)?.status !== 'up'
265
- }
266
-
267
158
  /** one repo's page. Every repo visited keeps its region mounted, hidden while
268
159
  * another one shows, so its scroll, its frames and its component state are
269
160
  * there when it comes back; a hidden repo polls nothing, so it repaints
@@ -274,8 +165,8 @@ const RepoView = observer(({ app }: { app: RepoSt }) => {
274
165
  <div className="repo" hidden={!on}>
275
166
  {notUp(app) ? <NotUp app={app.home} /> : (
276
167
  <>
277
- <HeaderBar app={app} />
278
- <LiveStrip app={app} />
168
+ {/* on the home the strip carries the repo's bar */}
169
+ {app.home.machine == null ? <HeaderBar app={app} /> : null}
279
170
  <Body app={app} />
280
171
  {on ? <ComfyOverlay app={app} /> : null}
281
172
  </>
@@ -291,9 +182,10 @@ function onKey(e: KeyboardEvent): void {
291
182
  if (pk != null) {
292
183
  e.preventDefault()
293
184
  if (pk.t === 'picker') home.palette.toggle()
294
- else if (pk.t === 'home' || pk.t === 'planned') {
185
+ else if (pk.t === 'dock') home.dock.cycle()
186
+ else if (pk.t === 'home' || pk.t === 'planned' || pk.t === 'activity') {
295
187
  home.palette.close()
296
- if (home.machine != null) home.navigate(pk.t === 'home' ? '/' : '/planned')
188
+ if (home.machine != null) home.navigate(PLACE_PATH[pk.t === 'home' ? 'overview' : pk.t])
297
189
  } else {
298
190
  const p = home.projects.list[pk.n - 1]
299
191
  home.palette.close()
@@ -312,20 +204,25 @@ function onKey(e: KeyboardEvent): void {
312
204
  e.preventDefault()
313
205
  return
314
206
  }
315
- if (e.key === '/') {
316
- e.preventDefault()
317
- app.searchEl?.focus()
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
+ }
318
214
  return
319
215
  }
320
- if (e.metaKey || e.ctrlKey || e.altKey) return
321
- if (e.key === LIVE_KEY) {
216
+ if (isSearchKey(e)) {
322
217
  e.preventDefault()
323
- app.live.toggle()
218
+ app.searchEl?.focus()
324
219
  return
325
220
  }
326
- if (e.key === FOLLOW_KEY && !app.live.chart.follow) {
221
+ if (e.metaKey || e.ctrlKey || e.altKey) return
222
+ if (e.key === FOLLOW_KEY && (!app.live.chart.follow || !home.dock.chart.follow)) {
327
223
  e.preventDefault()
328
224
  app.live.chart.attach()
225
+ home.dock.chart.attach()
329
226
  return
330
227
  }
331
228
  const f = FILTER_BY_KEY[e.key.toLowerCase()]
@@ -335,33 +232,72 @@ function onKey(e: KeyboardEvent): void {
335
232
  }
336
233
  }
337
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
+
338
242
  const App = observer(() => {
339
243
  useEffect(() => {
340
244
  document.addEventListener('keydown', onKey)
341
245
  return () => document.removeEventListener('keydown', onKey)
342
246
  }, [])
343
247
  const l = home.layout
248
+ const d = home.dock
344
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])
345
261
  return (
346
- // two panels: the project list, its width dragged by the separator
347
- // and kept per browser, then the page. The list and the strip stay
348
- // mounted across pages; only the region under the strip changes
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
349
266
  <>
350
- <Group className="app" orientation="horizontal" elementRef={el}
267
+ <Group className="appv" orientation="vertical" elementRef={vel}
351
268
  onLayoutChanged={(layout, meta) => {
352
- if (!meta.isUserInteraction) return
353
- const px = leftPx(el.current, layout, 'projects')
354
- if (px != null) l.setListWidth(px)
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)
355
276
  }}>
356
- <Panel id="projects" defaultSize={l.listWidth} minSize={LIST.min} maxSize={LIST.max} groupResizeBehavior="preserve-pixel-size" className="pjpanel" style={PANEL}>
357
- <ProjectList app={home} />
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>
358
297
  </Panel>
359
- <Separator className="rsep" />
360
- <Panel id="page" minSize={320} className="appcol" style={PANEL}>
361
- {home.machine == null ? null : <HomeStrip app={home} />}
362
- {home.page === 'overview' ? <Overview app={home} /> : null}
363
- {home.page === 'planned' ? <PlannedView app={home} /> : null}
364
- {home.visited.map((r) => <RepoView key={r.key} app={r} />)}
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} />
365
301
  </Panel>
366
302
  </Group>
367
303
  <Palette app={home} />
@@ -4,7 +4,11 @@
4
4
  action, and red/orange/green reserved for STATE. No rule outside this
5
5
  block writes a colour, and tests/hub-css-invariants.test.ts fails one
6
6
  that does. */
7
- :root{--bg:#0d0e10;--panel:#141619;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
7
+ :root{--bg:#0d0e10;--panel:#141619;
8
+ /* the surfaces of the regions, each a small step from the page so the rail,
9
+ the header and the dock read apart at a glance (no light theme: the page
10
+ is dark only, and colorDark is what it draws) */
11
+ --bg-side:#131519;--bg-top:#171a1f;--bg-dock:#10151b;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
8
12
  --accent:#6b8fb3;--sel:#1a2430;--chk1:#16181b;--chk2:#121417;
9
13
  /* state, and nothing else: red broken · orange taste pending · green measured passing */
10
14
  --red:#d2605c;--warn:#cf9a4e;--ok:#6faa86;--blue:#6f9bd1;
@@ -16,16 +20,23 @@
16
20
  --k1:#8ca8c7;--k2:#a892c9;--k3:#6fb0b8;--k4:#c792b4;--k5:#9198a1;--k6:#8f95cf;
17
21
  --wash:rgba(107,143,179,.15);--wash2:rgba(107,143,179,.11);--hair:rgba(107,143,179,.5);
18
22
  --errbg:#2a1a1a;--errline:#4a2c2c;--errink:#e3bebe;
23
+ /* a running thing's wash, the green twin of errbg and errline */
24
+ --okbg:#15251c;--okline:#2f5a40;
19
25
  /* the ONE spacing scale: every padding, margin and gap in this sheet is a
20
26
  value from it or 0, and tests/hub-css-invariants.test.ts fails the rest */
21
27
  --s0:2px;--s1:4px;--s2:8px;--s3:12px;--s4:16px}
22
28
  *{box-sizing:border-box}html,body,#root{height:100%}
23
29
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
24
- /* the page is two panels (hub-app.tsx): the project list, then .appcol,
25
- which stacks the strip, the header, the live strip and the body */
26
- .app{height:100vh}
30
+ /* a link with no class of its own: the browser default is dark blue on this dark page */
31
+ a,a:visited{color:var(--accent)}
32
+ /* the page is the window's height, the dock under it (hub-app.tsx): over
33
+ the dock two panels, the project list, then .appcol, which stacks the
34
+ strip, the header and the body */
35
+ .appv{height:100vh}
36
+ .app{height:100%}
27
37
  /* a panel's class lands on the div inside the panel (react-resizable-panels) */
28
38
  .pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
39
+ .apptop,.pdock{height:100%;min-height:0}
29
40
  .pjpanel{display:flex}
30
41
  .appcol{display:flex;flex-direction:column}
31
42
  /* one region per repo visited, the shown one alone displayed */
@@ -37,7 +48,11 @@ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans
37
48
  .pmain{display:flex;flex-direction:column}
38
49
  .pmain > main{flex:1}
39
50
 
40
- /* ── the header: repo name, the core pages, the two runs and the run state ── */
51
+ /* ── a repo's bar: on the home the strip (home-strip.css .hstrip), on a hub
52
+ of its own the header (.hdr). Its name, what runs now and the two runs,
53
+ then its processes, its trouble and its editor icons. One line at any
54
+ width: the container queries shrink the labels to their icons, then the
55
+ run state to its spinner ── */
41
56
  /* the repo's processes (serve.processes): a dot, its id, a link once up */
42
57
  .hprocs{display:inline-flex;gap:var(--s1);align-items:center}
43
58
  .hproc{display:inline-flex;align-items:center;gap:var(--s0)}
@@ -45,45 +60,51 @@ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans
45
60
  .hproc a{color:var(--accent);text-decoration:none;font-size:12px}
46
61
  .pdot{display:inline-block;width:7px;height:7px;border-radius:4px;margin-right:var(--s1);background:var(--line)}
47
62
  .pdot.up{background:var(--ok)}.pdot.starting,.pdot.stopping{background:var(--warn)}.pdot.crashed{background:var(--red)}
48
- .hdr{display:flex;gap:var(--s2);align-items:center;height:36px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--panel)}
63
+ .hdr{display:flex;gap:var(--s2);align-items:center;height:36px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--bg-top)}
49
64
  .hdr .sp{flex:1}
50
65
  .hname{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);font-weight:600;margin-right:var(--s2);white-space:nowrap}
51
- .hnav{display:flex;gap:var(--s1);overflow:hidden;align-items:center}
52
- .hnav button{border-color:transparent;color:var(--dim);border-radius:0;border-bottom:1px solid transparent;white-space:nowrap}
53
- .hnav button:hover{color:var(--ink);border-color:transparent;border-bottom-color:var(--line)}
54
- .hnav button.on{color:var(--accent);border-bottom-color:var(--accent)}
66
+ .hdr,.hstrip{container-type:inline-size;white-space:nowrap}
67
+ .hrun{display:inline-flex;align-items:center;gap:var(--s1);flex:none;margin-left:var(--s2)}
68
+ .hbtn{display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s2);font-size:11.5px;white-space:nowrap;flex:none}
69
+ .hbtn .hico{width:13px;height:13px}
70
+ /* the editor icons: bare, dim, lit on hover, as the activity page's Fork icon */
71
+ .hicon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:24px;height:22px;padding:0;border:0;background:none;color:var(--dim)}
72
+ .hicon:hover{color:var(--accent)}
73
+ .hicon .hico{width:15px;height:15px}
55
74
  /* the run state holds its width whatever it says, so a run never moves the buttons beside it */
56
75
  .hstatus{max-width:420px;flex:0 1 auto;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
57
76
  .hstatus.red{color:var(--red)}
58
77
  .hstatus.warn{color:var(--warn)}
59
- .hstate{display:inline-flex;gap:var(--s1);align-items:center;width:260px;min-width:260px;flex:none;justify-content:flex-start;font-size:11.5px;color:var(--dim);overflow:hidden}
78
+ /* its words sit against the run buttons, cut with an ellipsis */
79
+ .hstate{display:inline-flex;gap:var(--s1);align-items:center;width:200px;min-width:200px;flex:none;justify-content:flex-end;font-size:11.5px;color:var(--dim);overflow:hidden}
60
80
  .hstate.on{color:var(--ink)}
61
- .hstate .htext{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
62
- .hstate .spin.idle,.hnav .spin.idle{visibility:hidden;animation:none}
81
+ .hstate .htext{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
82
+ .hstate .spin.idle{visibility:hidden;animation:none}
83
+ /* after .hstate, so a narrow bar wins over its width */
84
+ @container (max-width:600px){.hbtn .lbl{display:none}.hbtn{padding:0 var(--s1)}}
85
+ @container (max-width:520px){.hstate{width:120px;min-width:120px}}
86
+ @container (max-width:420px){.hstate .htext{display:none}.hstate{width:16px;min-width:16px}}
63
87
 
64
- /* ── the live strip: the one run chart. Folded, one thin row of ONE height
65
- whatever runs; unfolded, the chart at the height its lanes hold, set
66
- inline from a held value that grows at once and shrinks only after a
67
- while, so it never jumps every second. Its (?) and the chart tooltip float
68
- in a portal (floating.tsx), so no box of the strip clips them ── */
69
- .live{display:flex;flex-direction:column;height:30px;flex:none;border-bottom:1px solid var(--line);background:var(--bg);font-size:11px;color:var(--dim)}
88
+ /* a row of a (?) legend: one glyph or word, then its meaning */
70
89
  .lrow{display:flex;gap:var(--s2);align-items:center;height:29px;flex:none;padding:0 var(--s3)}
71
- .lrow .sp{flex:1}
72
- .lfold{display:inline-flex;gap:var(--s1);align-items:center;border-color:transparent;color:var(--dim);padding:var(--s0) var(--s1)}
73
- .lfold .sk{font-size:9px;opacity:.55}
74
- .lwho{width:180px;flex:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink)}
75
- .live.idle .lwho{color:var(--dim)}
76
- .lwho i{font-style:normal;color:var(--dim);margin-left:var(--s1)}
77
- .lwho b{font-weight:500;color:var(--accent);margin-left:var(--s1)}
78
- .lchart{flex:1;min-width:0;height:21px;border:1px solid var(--line);border-radius:3px;overflow:hidden}
79
- .lopen{flex:1;min-height:0;overflow-y:auto;padding:0 var(--s3);scrollbar-width:thin;scrollbar-color:var(--line) transparent}
80
- .lnote,.lmore{flex:none;color:var(--warn);white-space:nowrap}
81
- .lbusy{flex:none;white-space:nowrap}
82
90
 
83
- /* ── sidebar ── */
84
- aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto;min-height:0}
85
- .shead{position:sticky;top:0;background:var(--panel);padding:var(--s3);border-bottom:1px solid var(--line);
86
- font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);z-index:2;display:flex;justify-content:space-between;align-items:center}
91
+ /* ── the left pane: the sections, the filter, then the tree, the one part
92
+ that scrolls ── */
93
+ aside{border-right:1px solid var(--line);background:var(--panel);min-height:0;display:flex;flex-direction:column;overflow:hidden}
94
+ aside > *{flex:none}
95
+ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
96
+ /* a repo's sections: the project list's lines (project-list.css .pjpl), a
97
+ little shorter, the current one lit and marked in the accent */
98
+ .rsecs{display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
99
+ .rsec{display:flex;align-items:center;gap:var(--s2);width:100%;height:26px;padding:0 var(--s3);background:transparent;border:0;border-radius:0;text-align:left;color:var(--dim);font-size:13px;cursor:pointer}
100
+ .rsec:hover{background:var(--panel2);color:var(--ink);border-color:transparent}
101
+ .rsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
102
+ .rsec .hico{flex:none;width:16px;height:16px}
103
+ .rsecl{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
104
+ .rsecn{flex:none;min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg)}
105
+ .rsecn.nred{background:var(--red)}
106
+ .rsecn.nwarn{background:var(--warn)}
107
+ .rsec:hover .pjkey,.rsec.cur .pjkey{opacity:1}
87
108
  /* segmented severity filter — all · needs · red. A thin border and an
88
109
  underline on the live one: no fill, no pill */
89
110
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden;flex:none}
@@ -94,10 +115,16 @@ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto
94
115
  .seg button.on{color:var(--accent);border-bottom-color:var(--accent)}
95
116
  .seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
96
117
  .swhen{padding:0 var(--s3) var(--s2);font-size:11px;color:var(--dim)}
97
- .ssearch{padding:var(--s2) var(--s3)}
118
+ /* a narrow pane puts the severity filter under the input, never over it */
119
+ .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3)}
120
+ .sfield{position:relative;flex:1 1 96px;min-width:0;display:flex}
98
121
  .ssearch input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);
99
- padding:var(--s1) var(--s2);font-size:12.5px;outline:none}
122
+ padding:var(--s0) var(--s4) var(--s0) var(--s2);font-size:12px;outline:none}
123
+ .ssearch input::placeholder{color:var(--dim);opacity:.6;font-size:11.5px}
100
124
  .ssearch input:focus{border-color:var(--accent)}
125
+ /* its key, the way the project list prints its keys (.pjkey) */
126
+ .skey{position:absolute;right:var(--s2);top:50%;transform:translateY(-50%);pointer-events:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
127
+ .sfield input:focus + .skey{opacity:0}
101
128
  /* ── the resource tree: ONE row shape, a fixed grid. The indent rides in
102
129
  the FIRST column and the label eats the slack, so every right side slot
103
130
  lines up down the whole column whatever the depth. Colour is on the
@@ -390,7 +417,6 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
390
417
  .tlb.bad,.tlb.red{fill:var(--red)}
391
418
  .tlb.rep{fill:var(--line)}
392
419
  .tlb.run{fill:var(--accent);animation:pulse 1.6s ease-in-out infinite}
393
- .tlb.repo{fill:var(--k6)}
394
420
  .tlband.run{stroke:var(--accent)}
395
421
  .tlconc{fill:var(--hair)}
396
422
  .tlconc.over{fill:var(--warn)}
@@ -439,6 +465,8 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
439
465
  per test check (the tests page), the failing files with their failure line */
440
466
  .rtime{grid-template-columns:minmax(0,1fr) 88px 72px 128px 72px 40px 88px}
441
467
  .rcheckt{grid-template-columns:152px 88px 48px 56px minmax(0,1fr) 72px}
468
+ /* every test file: file, check, duration, status, age */
469
+ .rall{grid-template-columns:minmax(0,1fr) 152px 88px 52px 72px}
442
470
  .rhead .tleft{text-align:left}
443
471
  .tshare{display:grid;grid-template-columns:minmax(0,1fr) 36px;gap:var(--s1);align-items:center}
444
472
  .ttrack{display:block;height:6px;border-radius:1px;overflow:hidden;background:var(--panel2)}
@@ -678,6 +706,9 @@ a.mint:hover{text-decoration:underline}
678
706
  .prow.on{background:var(--panel2)}
679
707
  .prow.none{color:var(--dim);cursor:default}
680
708
  .prow .plabel{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
709
+ /* the entry's icon: a place's button icon, a repo's own (project-list.tsx Face) */
710
+ .prow .pface{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dim)}
711
+ .prow .pface img,.prow .pface .pgi{width:16px;height:16px;border-radius:4px;object-fit:contain}
681
712
  .prow .pchip{font-size:10px;color:var(--dim);background:var(--bg);border:1px solid var(--line);border-radius:5px;
682
713
  padding:var(--s0) var(--s1);text-transform:uppercase;letter-spacing:.08em;flex:none}
683
714
 
@@ -689,3 +720,12 @@ a.mint:hover{text-decoration:underline}
689
720
  pre{background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:var(--s4);overflow-x:auto;
690
721
  font:12.5px/1.55 ui-monospace,Menlo,monospace;white-space:pre-wrap}
691
722
  .ok{color:var(--ok)}.warn{color:var(--warn)}.fail{color:var(--red)}.dim{color:var(--dim)}
723
+
724
+ /* ── the admin page: one row per control, its sentence under its name ── */
725
+ .adrow{display:flex;align-items:center;gap:var(--s4);padding:var(--s3) 0;border-top:1px solid var(--line)}
726
+ .adtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--s0);font-size:13px}
727
+ .adtext .dim{font-size:12px}
728
+ .adctl{flex:none;display:flex;align-items:center}
729
+ .adflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
730
+ .adflag input{width:auto;margin:0}
731
+ .adconfirm{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px}
@@ -1,12 +1,32 @@
1
- // the few line icons of the home (the overview's section headers, the two
2
- // places on top of the project list): one 20 unit path each, stroked in the text colour, so
3
- // the page's tokens colour them and nothing else does.
1
+ // the few line icons of the home (the overview's section headers, the places
2
+ // on top of the project list, a repo page's sections and its top bar): one 20
3
+ // unit path each, stroked in the text colour, so the page's tokens colour them
4
+ // and nothing else does.
4
5
  export const ICON = {
5
6
  shipkit: 'M3 7.5 10 4l7 3.5v5L10 16l-7-3.5zM3 7.5 10 11l7-3.5M10 11v5',
6
7
  process: 'M2 10h3.5l2-5 5 10 2-5H18',
7
8
  monitor: 'M3 4h14v9H3zM7 16.5h6M10 13v3.5',
9
+ archived: 'M2.5 4h15v3.5h-15zM4 7.5V16h12V7.5M8 10.5h4',
8
10
  home: 'M3 9.5 10 3.5l7 6M5 8v8.5h10V8M8.5 16.5v-4.5h3v4.5',
9
11
  planned: 'M5.5 3h9M5.5 17h9M6.5 3c0 4 7 4 7 7s-7 3-7 7M13.5 3c0 4-7 4-7 7s7 3 7 7',
12
+ browser: 'M10 2.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 1 0 0-15M2.5 10h15M10 2.5c-3 3-3 12 0 15M10 2.5c3 3 3 12 0 15',
13
+ window: 'M2.5 4h15v12h-15zM2.5 7.5h15M5 5.75h.01M7 5.75h.01',
14
+ activity: 'M6 3v14M6 6.5a2 2 0 1 0 0 .01M6 13.5a2 2 0 1 0 0 .01M10 6.5h7M10 13.5h7M10 10h5',
15
+ fork: 'M6 5v10M6 3a2 2 0 1 0 0 .01M6 17a2 2 0 1 0 0 .01M14 5a2 2 0 1 0 0 .01M14 7c0 4.5-8 3.5-8 7',
16
+ search: 'M8.5 3a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0-11M12.5 12.5 17 17',
17
+ // a repo page's sections, then its two runs and its editor
18
+ checks: 'M4 10.5 7.5 14 16 5.5',
19
+ runs: 'M3 5h8M3 10h12M3 15h6',
20
+ asks: 'M3 4h14v9H8l-4 3.5V13H3z',
21
+ tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
22
+ media: 'M3 4h14v12H3zM3 13l4-4 4 4 2-2 4 4M13 7.5h.01',
23
+ wikis: 'M10 5.5C8.5 4 6 3.5 3 4v11c3-.5 5.5 0 7 1.5c1.5-1.5 4-2 7-1.5V4c-3-.5-5.5 0-7 1.5zM10 5.5v11',
24
+ admin: 'M4 5h12M4 10h12M4 15h12M7 3.5v3M13 8.5v3M9 13.5v3',
25
+ health: 'M2 10h3.5l2-5 5 10 2-5H18',
26
+ gate: 'M10 2.5 16 5v4.5c0 4-2.7 6.5-6 8c-3.3-1.5-6-4-6-8V5zM7.5 10l2 2 3.5-3.5',
27
+ // the VS Code mark, its ribbon drawn as one outline
28
+ vscode: 'M14 2.5l3.5 1.7v11.6L14 17.5zM14 2.5 4.5 12.3 2.5 11V9l2-1.3L14 17.5M14 6.8 10.2 10l3.8 3.2',
29
+ sort: 'M6 3.5v13M3 13.5l3 3 3-3M14 16.5v-13M11 6.5l3-3 3 3',
10
30
  } as const
11
31
 
12
32
  export const Icon = ({ d }: { d: string }) => (
@@ -9,6 +9,10 @@
9
9
  <link rel="stylesheet" href="./prose-hits.css" />
10
10
  <link rel="stylesheet" href="./home-strip.css" />
11
11
  <link rel="stylesheet" href="./planned.css" />
12
+ <link rel="stylesheet" href="./activity.css" />
13
+ <link rel="stylesheet" href="./readme.css" />
14
+ <link rel="stylesheet" href="./dock.css" />
15
+ <link rel="stylesheet" href="./controls.css" />
12
16
  </head>
13
17
  <body>
14
18
  <div id="root"></div>