rvlib-shipkit 2.1.1 → 2.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 (142) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +2 -1
  3. package/src/agent-time/mask.ts +43 -0
  4. package/src/agent-time/render.ts +187 -0
  5. package/src/agent-time/report.ts +335 -0
  6. package/src/agent-time/shape.ts +271 -0
  7. package/src/agent-time/signals.ts +84 -0
  8. package/src/agent-time/store.ts +379 -0
  9. package/src/agent-time/timeline.ts +218 -0
  10. package/src/agent-time/turns.ts +135 -0
  11. package/src/assets/kits/identity-kit.ts +7 -3
  12. package/src/assets/run-assets.ts +18 -4
  13. package/src/checks/SK004-gitignore-policy.check.ts +11 -6
  14. package/src/checks/SK013-one-way-in-policy.check.ts +13 -8
  15. package/src/cli/cmd_Calls.ts +53 -0
  16. package/src/cli/cmd_Commands.ts +163 -43
  17. package/src/cli/cmd_Review.ts +948 -0
  18. package/src/cli/cmd_Shot.ts +63 -0
  19. package/src/cli/cmd_Start.ts +8 -4
  20. package/src/cli/cmd_Time.ts +112 -0
  21. package/src/cli/cmd_Trailer.ts +361 -0
  22. package/src/cli/kit/builtins.ts +40 -0
  23. package/src/cli/kit/calls.ts +177 -0
  24. package/src/cli/kit/families.ts +67 -0
  25. package/src/cli/kit/index.ts +8 -0
  26. package/src/cli/kit/pages-check.ts +36 -0
  27. package/src/cli/kit/project-cli.ts +73 -0
  28. package/src/cli/kit/verb.ts +167 -0
  29. package/src/cli.ts +15 -2
  30. package/src/collections/collection-types.ts +6 -1
  31. package/src/collections/providers.ts +3 -0
  32. package/src/collections/review.ts +70 -0
  33. package/src/core/layout.ts +12 -2
  34. package/src/core/migrate-layout.ts +31 -8
  35. package/src/core/one-way-in-policy.ts +12 -4
  36. package/src/reflect/prompt-corpus.ts +15 -114
  37. package/src/reflect/transcript.ts +254 -0
  38. package/src/review/anim.ts +228 -0
  39. package/src/review/auto.ts +141 -0
  40. package/src/review/format.ts +150 -0
  41. package/src/review/http.ts +23 -0
  42. package/src/review/marks.ts +43 -0
  43. package/src/review/media.ts +45 -0
  44. package/src/review/model.ts +854 -0
  45. package/src/review/project.ts +110 -0
  46. package/src/review/role.ts +48 -0
  47. package/src/review/routes.ts +228 -0
  48. package/src/review/store.ts +1002 -0
  49. package/src/review/threads.ts +237 -0
  50. package/src/review/trailer-desk.ts +385 -0
  51. package/src/review/trailer-model.ts +183 -0
  52. package/src/review/trailer-routes.ts +136 -0
  53. package/src/review/unit.ts +165 -0
  54. package/src/review/watchers.ts +54 -0
  55. package/src/scaffold/cli-skeleton.ts +149 -0
  56. package/src/scaffold/files/agent-index.md +2 -2
  57. package/src/scaffold/files/tsconfig.json +3 -0
  58. package/src/scaffold/files/verify-skill.md +1 -1
  59. package/src/scaffold/project-scaffold.ts +90 -12
  60. package/src/serve/embed.ts +35 -0
  61. package/src/serve/hub-app/activity-view.tsx +1 -1
  62. package/src/serve/hub-app/activity.css +0 -4
  63. package/src/serve/hub-app/dock-view.tsx +5 -4
  64. package/src/serve/hub-app/hash-route.ts +28 -1
  65. package/src/serve/hub-app/header-bar.tsx +3 -1
  66. package/src/serve/hub-app/home-st.ts +10 -2
  67. package/src/serve/hub-app/home-strip.tsx +3 -2
  68. package/src/serve/hub-app/home.html +1 -0
  69. package/src/serve/hub-app/hub-app.tsx +80 -11
  70. package/src/serve/hub-app/hub.css +45 -13
  71. package/src/serve/hub-app/icons.tsx +9 -0
  72. package/src/serve/hub-app/index.html +1 -0
  73. package/src/serve/hub-app/insights-view.tsx +4 -3
  74. package/src/serve/hub-app/keys.ts +14 -10
  75. package/src/serve/hub-app/layout-st.ts +63 -2
  76. package/src/serve/hub-app/planned-view.tsx +3 -2
  77. package/src/serve/hub-app/planned.css +0 -1
  78. package/src/serve/hub-app/project-list.css +22 -4
  79. package/src/serve/hub-app/project-list.tsx +41 -8
  80. package/src/serve/hub-app/projects-svc.ts +4 -0
  81. package/src/serve/hub-app/repo-sections.ts +15 -3
  82. package/src/serve/hub-app/repo-st.ts +45 -3
  83. package/src/serve/hub-app/review/clip-st.ts +208 -0
  84. package/src/serve/hub-app/review/kind-view.tsx +171 -0
  85. package/src/serve/hub-app/review/kit.tsx +113 -0
  86. package/src/serve/hub-app/review/review-answer.tsx +239 -0
  87. package/src/serve/hub-app/review/review-board.tsx +46 -0
  88. package/src/serve/hub-app/review/review-list.tsx +156 -0
  89. package/src/serve/hub-app/review/review-page.tsx +101 -0
  90. package/src/serve/hub-app/review/review-st.ts +779 -0
  91. package/src/serve/hub-app/review/review-views.tsx +503 -0
  92. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  93. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  94. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  95. package/src/serve/hub-app/review.css +329 -0
  96. package/src/serve/hub-app/theme-st.ts +56 -0
  97. package/src/serve/hub-app/tree-build.ts +4 -0
  98. package/src/serve/hub-app/tree-row.tsx +1 -1
  99. package/src/serve/hub-app/tree-svc.ts +4 -0
  100. package/src/serve/hub-runtime.ts +5 -3
  101. package/src/serve/same-origin.ts +5 -1
  102. package/src/serve/serve-hub.ts +8 -0
  103. package/src/shipkit-files.ts +12 -4
  104. package/src/shot/client.ts +67 -0
  105. package/src/shot/clip-encoder.ts +146 -0
  106. package/src/shot/dom-clip.ts +124 -0
  107. package/src/shot/serve-shot.ts +54 -0
  108. package/src/shot/shooter.ts +343 -0
  109. package/src/shot/shot-ask.ts +189 -0
  110. package/src/shot/shot.shipkit.ts +14 -0
  111. package/src/smoke/run-smoke.ts +2 -2
  112. package/src/start-repo.ts +29 -14
  113. package/src/suite/judge-suites.ts +56 -10
  114. package/src/suite/select.ts +2 -5
  115. package/src/suite/suite-cli.ts +3 -2
  116. package/src/trailer/adapter.ts +78 -0
  117. package/src/trailer/build.ts +603 -0
  118. package/src/trailer/ffmpeg.ts +72 -0
  119. package/src/trailer/footage.ts +173 -0
  120. package/src/trailer/grammar.ts +1290 -0
  121. package/src/trailer/graph.ts +508 -0
  122. package/src/trailer/holes.ts +72 -0
  123. package/src/trailer/load-unit.ts +31 -0
  124. package/src/trailer/look.ts +124 -0
  125. package/src/trailer/passage.ts +129 -0
  126. package/src/trailer/plan.ts +192 -0
  127. package/src/trailer/prune.ts +193 -0
  128. package/src/trailer/score/compose.ts +240 -0
  129. package/src/trailer/score/dsp.ts +272 -0
  130. package/src/trailer/score/palette.ts +188 -0
  131. package/src/trailer/score/pattern.ts +91 -0
  132. package/src/trailer/score/score.ts +214 -0
  133. package/src/trailer/score/sound.ts +112 -0
  134. package/src/trailer/score/synth.ts +361 -0
  135. package/src/trailer/states.ts +76 -0
  136. package/src/trailer/stitch.ts +233 -0
  137. package/src/trailer/titles.ts +379 -0
  138. package/src/trailer/url.ts +108 -0
  139. package/src/trailer/vertical-draw.ts +286 -0
  140. package/src/trailer/vertical.ts +148 -0
  141. package/src/trailer/vocab.ts +59 -0
  142. package/src/trailer/words.ts +254 -0
@@ -7,7 +7,7 @@
7
7
  // (sidebar.tsx), coloured by state in the label itself, under one filter
8
8
  // input and one severity filter persisted at
9
9
  // ?f=needs · ?f=red. A row opens its item view, an asset row opens the atelier.
10
- import { runInAction } from 'mobx'
10
+ import { reaction, runInAction } from 'mobx'
11
11
  import { observer } from 'mobx-react-lite'
12
12
  import { StrictMode, useEffect, useLayoutEffect, useRef } from 'react'
13
13
  import { createRoot } from 'react-dom/client'
@@ -17,6 +17,7 @@ import { WIKIS, WikisView } from './wikis-view.tsx'
17
17
  import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
18
18
  import type { Filter, RepoSt } from './repo-st.ts'
19
19
  import { HomeSt, PLACE_PATH } from './home-st.ts'
20
+ import { hashFor } from './hash-route.ts'
20
21
  import { TimelineView } from './timeline-view.tsx'
21
22
  import { HeaderBar, notUp } from './header-bar.tsx'
22
23
  import { FOLLOW_KEY } from './timeline-chart.tsx'
@@ -31,6 +32,8 @@ import { WhenPage } from './when-view.tsx'
31
32
  import { GitPage } from './git-view.tsx'
32
33
  import { AdminPage } from './admin-view.tsx'
33
34
  import { ReadmeView } from './readme-view.tsx'
35
+ import { ReviewPage } from './review/review-page.tsx'
36
+ import { TrailerPage } from './review/trailer-page.tsx'
34
37
  import { ProjectList } from './project-list.tsx'
35
38
  import { Group, Panel, Separator, usePanelRef, type Layout } from 'react-resizable-panels'
36
39
  import { LIST, SIDEBAR } from './layout-st.ts'
@@ -46,8 +49,11 @@ import { Dock } from './dock-view.tsx'
46
49
  import { DOCK } from './dock-st.ts'
47
50
  import { animateDock } from './dock-anim.ts'
48
51
 
49
- // home.html marks the home's page; index.html is a hub serving on its own
50
- const home = new HomeSt({ home: document.body.dataset.home != null })
52
+ // home.html marks the home's page; index.html is a hub serving on its own; `<base>/embed/?page=review` is one repo's
53
+ // review or trailer page alone, for another tool's frame (embed.ts), its base read from where it sits
54
+ const embedAt = /^(.*)\/embed\/?$/.exec(location.pathname)
55
+ const embed: 'review' | 'trailer' | null = embedAt == null ? null : new URLSearchParams(location.search).get('page') === 'trailer' ? 'trailer' : 'review'
56
+ const home = new HomeSt(embedAt == null ? { home: document.body.dataset.home != null } : { home: false, base: embedAt[1] ?? '' })
51
57
  // the root as a global — app code, console debugging (build/app-state-tree)
52
58
  ;(window as unknown as { app: HomeSt }).app = home
53
59
  home.boot()
@@ -97,6 +103,8 @@ const Main = observer(({ app }: { app: RepoSt }) => {
97
103
  {app.sel.id === 'checks' ? <CheckTable app={app} id={app.sel.id} /> : app.sel.id === 'asks' ? <AskTable app={app} id={app.sel.id} /> : app.sel.id === 'creds' ? <SecretTable app={app} id={app.sel.id} /> : <ItemTable app={app} id={app.sel.id} />}
98
104
  </main>
99
105
  )
106
+ if (app.sel?.t === 'review') return <main>{errs}<ReviewPage app={app} /></main>
107
+ if (app.sel?.t === 'trailer') return <main>{errs}<TrailerPage app={app} /></main>
100
108
  if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
101
109
  if (app.sel?.t === 'admin') return <main>{errs}<div className="stagewrap"><AdminPage app={app} /></div></main>
102
110
  if (app.sel?.t === 'plan') return <main>{errs}<div className="stagewrap"><PlanPage app={app} /></div></main>
@@ -165,7 +173,7 @@ const Body = observer(({ app }: { app: RepoSt }) => {
165
173
  <Sidebar app={app} />
166
174
  </Panel>
167
175
  <Separator className="rsep" />
168
- <Panel id="main" minSize={320} className="pmain" style={PANEL}>
176
+ <Panel id="main" minSize={320} className="pmain" style={PANEL} onPointerDownCapture={() => app.tree.leave()}>
169
177
  <Main app={app} />
170
178
  </Panel>
171
179
  </Group>
@@ -194,6 +202,13 @@ const RepoView = observer(({ app }: { app: RepoSt }) => {
194
202
 
195
203
  /** the keys of the page: they act on the repo shown, and on nothing else */
196
204
  function onKey(e: KeyboardEvent): void {
205
+ // the embed page has its page's own keys and nothing else
206
+ if (embed != null) {
207
+ const app = home.shown
208
+ const t = e.target as HTMLElement
209
+ if (app != null && t.tagName !== 'TEXTAREA' && t.tagName !== 'INPUT' && !e.metaKey && !e.ctrlKey && !e.altKey && app.pageKey(e)) e.preventDefault()
210
+ return
211
+ }
197
212
  // the command keys beat the focus check: they work from anywhere, any page
198
213
  const pk = pageKey(e)
199
214
  // in a text field ⌘↑ and ⌘↓ keep moving the caret to its ends
@@ -202,7 +217,8 @@ function onKey(e: KeyboardEvent): void {
202
217
  e.preventDefault()
203
218
  if (pk.t === 'picker') home.palette.toggle()
204
219
  else if (pk.t === 'dock') home.dock.cycle()
205
- else if (pk.t === 'home' || pk.t === 'planned' || pk.t === 'activity') {
220
+ else if (pk.t === 'rail') home.layout.toggleRail()
221
+ else if (pk.t === 'home' || pk.t === 'planned' || pk.t === 'activity' || pk.t === 'insights') {
206
222
  home.palette.close()
207
223
  if (home.machine != null) home.navigate(PLACE_PATH[pk.t === 'home' ? 'overview' : pk.t])
208
224
  } else if (pk.t === 'step') {
@@ -226,6 +242,12 @@ function onKey(e: KeyboardEvent): void {
226
242
  e.preventDefault()
227
243
  return
228
244
  }
245
+ // so do the review and trailer pages (RepoSt.pageKey)
246
+ const plain = !e.metaKey && !e.ctrlKey && !e.altKey
247
+ if (plain && app.pageKey(e)) {
248
+ e.preventDefault()
249
+ return
250
+ }
229
251
  const sk = sectionKey(e)
230
252
  if (sk != null) {
231
253
  const s = repoSections(app).find((x) => x.id === sk)
@@ -280,6 +302,21 @@ const App = observer(() => {
280
302
  const el = useRef<HTMLDivElement | null>(null)
281
303
  const vel = useRef<HTMLDivElement | null>(null)
282
304
  const dockRef = usePanelRef()
305
+ const listRef = usePanelRef()
306
+ // ⌘B folds the list to its rail and back; a drag that already did it finds it there
307
+ const rail = l.listRail
308
+ const railW = l.railPx
309
+ useLayoutEffect(() => {
310
+ const p = listRef.current
311
+ if (p == null) return
312
+ if (p.isCollapsed() !== rail) {
313
+ if (rail) p.collapse()
314
+ else {
315
+ p.expand()
316
+ p.resize(l.listWidth)
317
+ }
318
+ } else if (rail && Math.abs(p.getSize().inPixels - railW) >= 1) p.resize(railW)
319
+ }, [rail, railW, listRef, l])
283
320
  // the dock's size moves the boundary, and only the boundary: its first
284
321
  // height is the panel's default, read before the first render. After the
285
322
  // render, so the panel already holds the bounds of its new size, and
@@ -309,10 +346,15 @@ const App = observer(() => {
309
346
  <Group className="app" orientation="horizontal" elementRef={el}
310
347
  onLayoutChanged={(layout, meta) => {
311
348
  if (!meta.isUserInteraction) return
349
+ // a drag below the list's min folds it to its rail, a drag out unfolds it
350
+ const folded = listRef.current?.isCollapsed() === true
351
+ l.setRail(folded)
352
+ if (folded) return
312
353
  const px = leftPx(el.current, layout, 'projects')
313
354
  if (px != null) l.setListWidth(px)
314
355
  }}>
315
- <Panel id="projects" defaultSize={l.listWidth} minSize={LIST.min} maxSize={LIST.max} groupResizeBehavior="preserve-pixel-size" className="pjpanel" style={PANEL}>
356
+ <Panel id="projects" panelRef={listRef} defaultSize={rail ? railW : l.listWidth} minSize={LIST.min} maxSize={LIST.max} collapsible collapsedSize={railW}
357
+ groupResizeBehavior="preserve-pixel-size" className="pjpanel" style={PANEL}>
316
358
  <ProjectList app={home} />
317
359
  </Panel>
318
360
  <Separator className="rsep" />
@@ -337,12 +379,39 @@ const App = observer(() => {
337
379
  )
338
380
  })
339
381
 
382
+ /** the embed page: the one page, no tree, no list, no dock. Its address and the frame's host follow each other: every
383
+ * page move is posted to the parent, and the parent's `{ shipkit: 'go', hash }` moves the page; both only on this origin */
384
+ const EmbedApp = observer(({ page }: { page: 'review' | 'trailer' }) => {
385
+ const app = home.shown
386
+ useEffect(() => {
387
+ document.addEventListener('keydown', onKey)
388
+ const onMessage = (e: MessageEvent): void => {
389
+ const m = e.data as { shipkit?: unknown; hash?: unknown } | null
390
+ if (e.source !== window.parent || e.origin !== location.origin || m?.shipkit !== 'go' || typeof m.hash !== 'string') return
391
+ location.hash = m.hash.replace(/^#?/, '#')
392
+ }
393
+ window.addEventListener('message', onMessage)
394
+ return () => {
395
+ document.removeEventListener('keydown', onKey)
396
+ window.removeEventListener('message', onMessage)
397
+ }
398
+ }, [])
399
+ useEffect(() => {
400
+ if (app == null) return
401
+ if (app.sel?.t !== page) app.go(page === 'review' ? { t: 'review', id: null } : { t: 'trailer', trailer: null, cut: null })
402
+ if (window.parent === window) return
403
+ return reaction(
404
+ () => (app.sel == null ? '' : (hashFor(app.sel) ?? '')),
405
+ (hash) => window.parent.postMessage({ shipkit: 'route', page, hash: `#${hash}` }, location.origin),
406
+ { fireImmediately: true },
407
+ )
408
+ }, [app, page])
409
+ if (app == null) return null
410
+ return <main className="embed" style={{ position: 'fixed', inset: 0 }}>{page === 'review' ? <ReviewPage app={app} /> : <TrailerPage app={app} />}</main>
411
+ })
412
+
340
413
  // the shown repo's kept screen is read before the first paint, so a refresh
341
414
  // paints it at once; a slow or absent store costs at most SCREEN_WAIT_MS
342
415
  await Promise.race([home.shown?.cols.restore(), new Promise((ok) => setTimeout(ok, SCREEN_WAIT_MS))])
343
416
 
344
- createRoot(document.getElementById('root')!).render(
345
- <StrictMode>
346
- <App />
347
- </StrictMode>,
348
- )
417
+ createRoot(document.getElementById('root')!).render(<StrictMode>{embed == null ? <App /> : <EmbedApp page={embed} />}</StrictMode>)
@@ -6,8 +6,8 @@
6
6
  that does. */
7
7
  :root{--bg:#0d0e10;--panel:#141619;
8
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) */
9
+ the header and the dock read apart at a glance; the light values are the
10
+ next block) */
11
11
  --bg-side:#131519;--bg-top:#171a1f;--bg-dock:#10151b;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
12
12
  --accent:#6b8fb3;--sel:#1a2430;--chk1:#16181b;--chk2:#121417;
13
13
  /* state, and nothing else: red broken · orange taste pending · green measured passing */
@@ -18,6 +18,8 @@
18
18
  --ground:#1b2521;--groundline:#2a3630;--tag:#eef2f6;
19
19
  /* the KIND hues: a row's type, never its state (tree-build TYPE_MARK) */
20
20
  --k1:#8ca8c7;--k2:#a892c9;--k3:#6fb0b8;--k4:#c792b4;--k5:#9198a1;--k6:#8f95cf;
21
+ /* the review section's own hue: the one row of the tree meant to be seen first */
22
+ --review:#e879d4;
21
23
  --wash:rgba(107,143,179,.15);--wash2:rgba(107,143,179,.11);--hair:rgba(107,143,179,.5);
22
24
  --errbg:#2a1a1a;--errline:#4a2c2c;--errink:#e3bebe;
23
25
  /* a running thing's wash, the green twin of errbg and errline */
@@ -25,6 +27,26 @@
25
27
  /* the ONE spacing scale: every padding, margin and gap in this sheet is a
26
28
  value from it or 0, and tests/hub-css-invariants.test.ts fails the rest */
27
29
  --s0:2px;--s1:4px;--s2:8px;--s3:12px;--s4:16px}
30
+ /* THE LIGHT TOKENS: the same names, read on a light page (theme-st.ts sets
31
+ <html data-theme>). Every token of the block above has its light value
32
+ here, and tests/hub-css-invariants.test.ts fails a missing one */
33
+ :root[data-theme=light]{--bg:#f6f7f9;--panel:#ffffff;
34
+ --bg-side:#eef0f3;--bg-top:#f2f4f7;--bg-dock:#eceff3;--panel2:#e9ecf0;--line:#d6dbe1;--ink:#1c2026;--dim:#5d646d;
35
+ --accent:#3b6896;--sel:#dbe6f2;--chk1:#e9ecf0;--chk2:#f7f8fa;
36
+ --red:#c03f3a;--warn:#b0700f;--ok:#2d8754;--blue:#2b69b0;
37
+ --scrim:rgba(20,24,30,.35);--shadow:rgba(20,24,30,.16);--solid:#000;
38
+ --hi1:rgba(0,0,0,.2);--hi2:rgba(0,0,0,.11);--hi3:rgba(0,0,0,.045);
39
+ --ground:#e3ede6;--groundline:#c6d6cb;--tag:#eef2f6;
40
+ --k1:#3f6690;--k2:#7055a3;--k3:#2b7c86;--k4:#a34e80;--k5:#5d646d;--k6:#4f56a8;
41
+ --review:#c0279d;
42
+ --wash:rgba(59,104,150,.12);--wash2:rgba(59,104,150,.08);--hair:rgba(59,104,150,.45);
43
+ --errbg:#fbe9e8;--errline:#efc4c2;--errink:#8a2520;
44
+ --okbg:#e6f3eb;--okline:#9ccdaf}
45
+ /* a repo's own colour, the one fit for the theme (projects-svc idVars) */
46
+ .idc{color:var(--idd)}
47
+ :root[data-theme=light] .idc{color:var(--idl)}
48
+ .idb{background:var(--idd)}
49
+ :root[data-theme=light] .idb{background:var(--idl)}
28
50
  *{box-sizing:border-box}html,body,#root{height:100%}
29
51
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
30
52
  /* a link with no class of its own: the browser default is dark blue on this dark page */
@@ -43,7 +65,9 @@ a,a:visited{color:var(--accent)}
43
65
  .repo{flex:1;min-height:0;display:flex;flex-direction:column}
44
66
  .repo[hidden]{display:none}
45
67
  .appbody{flex:1;min-height:0}
46
- .pside{display:flex;flex-direction:column}
68
+ .pside{display:flex;flex-direction:column;container-type:inline-size}
69
+ /* a narrow sidebar keeps the names: the key hints and the filter's letters go first */
70
+ @container (max-width:200px){.tsec .pjkey,.ssearch .seg .sk{display:none}.ssearch{padding:var(--s2)}}
47
71
  .pside > aside{flex:1}
48
72
  .pmain{display:flex;flex-direction:column}
49
73
  .pmain > main{flex:1}
@@ -101,14 +125,21 @@ aside > .tscroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3
101
125
  .rsecn.nred{background:var(--red)}
102
126
  .rsecn.nwarn{background:var(--warn)}
103
127
  .tsec:hover .pjkey,.tsec.cur .pjkey{opacity:1}
104
- /* segmented severity filter — all · needs · red. A thin border and an
105
- underline on the live one: no fill, no pill */
106
- .seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden;flex:none}
107
- .seg button{background:transparent;color:var(--dim);border:0;border-bottom:1px solid transparent;border-radius:0;
108
- padding:var(--s0) var(--s2);font-size:11px;font-weight:500;text-transform:lowercase;letter-spacing:0;cursor:pointer}
128
+ /* review: its own hue on the icon, the label and the mark, empty or not, so the eye finds it */
129
+ .tsec.s-review,.tsec.s-review:hover,.tsec.s-review.cur{color:var(--review)}
130
+ .tsec.s-review.empty{opacity:.75}
131
+ .tsec.s-review.cur{box-shadow:inset 3px 0 0 var(--review)}
132
+ .tsec.s-review .rsecn.nwarn{background:var(--review)}
133
+ /* THE SELECT SHAPE: every pick on the page, a native select or a segmented
134
+ bar of exclusive buttons (.seg), is one 22px pill with a 1px line border;
135
+ the live segment is the accent on the selection wash. Nothing else
136
+ draws a select: a new one takes .seg or a plain select */
137
+ .seg{display:inline-flex;align-items:stretch;height:22px;border:1px solid var(--line);border-radius:999px;overflow:hidden;flex:none}
138
+ .seg button{display:inline-flex;align-items:center;background:transparent;color:var(--dim);border:0;border-radius:0;
139
+ padding:0 var(--s2);font-size:12px;font-weight:500;line-height:1;text-transform:lowercase;letter-spacing:0;cursor:pointer}
109
140
  .seg button+button{border-left:1px solid var(--line)}
110
- .seg button:hover{color:var(--ink)}
111
- .seg button.on{color:var(--accent);border-bottom-color:var(--accent)}
141
+ .seg button:hover{color:var(--ink);background:var(--panel2)}
142
+ .seg button.on{color:var(--accent);background:var(--sel)}
112
143
  .seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
113
144
  /* a narrow pane puts the severity filter under the input, never over it */
114
145
  .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line)}
@@ -201,7 +232,6 @@ textarea{width:100%;min-height:64px;background:var(--bg);color:var(--ink);border
201
232
  .toolbar button{padding:var(--s1) var(--s3);font-size:12px}
202
233
  .toolbar .sp{flex:1}
203
234
  .toolbar .xn{display:flex;gap:var(--s0);align-items:center;font-size:12px;color:var(--dim)}
204
- .toolbar .xn select{padding:var(--s1);font-size:12px}
205
235
  /* BUTTONS: small, flat, one pixel border on the panel colour, a 12px
206
236
  lowercase label. The accent is a border and an ink colour on the primary
207
237
  action, never a fill, and nothing here is a big rounded rectangle */
@@ -212,7 +242,9 @@ button.primary{color:var(--accent);border-color:var(--accent)}
212
242
  button.ghost{color:var(--dim)}
213
243
  button.ghost:hover{color:var(--ink)}
214
244
  button:disabled{opacity:.4;cursor:default;border-color:var(--line);color:var(--dim)}
215
- select,input[type=number]{background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s2)}
245
+ select{height:22px;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:999px;padding:0 var(--s2);font-size:12px;cursor:pointer}
246
+ select:hover:not(:disabled){border-color:var(--accent)}
247
+ input[type=number]{background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s2)}
216
248
  .status{font-size:12px;color:var(--dim);text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:46vw;flex:none}
217
249
  @keyframes rot{to{transform:rotate(360deg)}}
218
250
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
@@ -343,7 +375,7 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
343
375
  /* panel content inherits the hub's chunky default button — inside a card
344
376
  everything is DENSE, repo panel scripts get this for free */
345
377
  .fbody button{padding:var(--s0) var(--s2);font-size:11.5px;font-weight:600;border-radius:3px}
346
- .fbody input:not([type=color]),.fbody select{padding:var(--s0) var(--s1);font-size:11.5px}
378
+ .fbody input:not([type=color]){padding:var(--s0) var(--s1);font-size:11.5px}
347
379
  .fbody label{font-size:11.5px}
348
380
  .fbody .jsonta{width:100%;min-height:160px}
349
381
  .vbar{flex:none;border-left:1px solid var(--line);background:var(--panel);display:flex;min-height:0;position:relative}
@@ -28,8 +28,11 @@ export const ICON = {
28
28
  secrets: 'M7 7a3 3 0 1 0 0 6a3 3 0 1 0 0-6M10 10h7.5M15 10v2.5M17.5 10v2',
29
29
  runs: 'M3 5h8M3 10h12M3 15h6',
30
30
  asks: 'M3 4h14v9H8l-4 3.5V13H3z',
31
+ // pkm-multi's rail marks: rate_review, a bubble with a pen, and movie, a clapper
32
+ review: 'M3 3.5h14v10H7.5L3 17zM7 10.5l4.5-4.5 1.5 1.5L8.5 12H7z',
31
33
  tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
32
34
  media: 'M3 4h14v12H3zM3 13l4-4 4 4 2-2 4 4M13 7.5h.01',
35
+ trailer: 'M2.5 4.5h15v11h-15zM2.5 8.5h15M6 4.5 7.5 8.5M10 4.5l1.5 4M14 4.5l1.5 4',
33
36
  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',
34
37
  admin: 'M4 5h12M4 10h12M4 15h12M7 3.5v3M13 8.5v3M9 13.5v3',
35
38
  health: 'M2 10h3.5l2-5 5 10 2-5H18',
@@ -37,6 +40,12 @@ export const ICON = {
37
40
  // the VS Code mark, its ribbon drawn as one outline
38
41
  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',
39
42
  sort: 'M6 3.5v13M3 13.5l3 3 3-3M14 16.5v-13M11 6.5l3-3 3 3',
43
+ // the theme button: a sun, a moon, and a half filled disc for the system's
44
+ sun: 'M10 6.5a3.5 3.5 0 1 0 .01 0M10 2v2M10 16v2M2 10h2M16 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M4.3 15.7l1.4-1.4M14.3 5.7l1.4-1.4',
45
+ moon: 'M15.5 12.5A6.5 6.5 0 0 1 7.5 4.5a6.5 6.5 0 1 0 8 8z',
46
+ auto: 'M10 3a7 7 0 1 0 .01 0M10 3v14M10 5.5h4M10 8.5h6M10 11.5h6M10 14.5h4',
47
+ // the project list's fold: a panel with its rail marked
48
+ rail: 'M3 4h14v12H3zM7.5 4v12',
40
49
  // a process's own switches on the overview
41
50
  play: 'M6.5 4.5v11l9-5.5z',
42
51
  stop: 'M5.5 5.5h9v9h-9z',
@@ -14,6 +14,7 @@
14
14
  <link rel="stylesheet" href="./readme.css" />
15
15
  <link rel="stylesheet" href="./dock.css" />
16
16
  <link rel="stylesheet" href="./controls.css" />
17
+ <link rel="stylesheet" href="./review.css" />
17
18
  </head>
18
19
  <body>
19
20
  <div id="root"></div>
@@ -80,16 +80,17 @@ export const InsightsView = observer(({ app }: { app: HomeSt }) => {
80
80
  return (
81
81
  <div className="hall activity insights">
82
82
  <div className="actop">
83
- <div className="acwin" role="radiogroup" aria-label="which repos">
83
+ <div className="seg" role="radiogroup" aria-label="which repos">
84
84
  {(Object.keys(SCOPE_WORDS) as Scope[]).map((s) => (
85
85
  <button key={s} role="radio" aria-checked={st.scope === s} className={st.scope === s ? 'on' : ''} title={SCOPE_WORDS[s].title} onClick={() => st.setScope(s)}>{SCOPE_WORDS[s].label}</button>
86
86
  ))}
87
87
  </div>
88
- <div className="acwin" role="radiogroup" aria-label="which boxes">
88
+ <div className="seg" role="radiogroup" aria-label="which boxes">
89
89
  <button role="radio" aria-checked={st.tag == null} className={st.tag == null ? 'on' : ''} onClick={() => st.setTag(null)}>all</button>
90
90
  {st.tags.map((t) => <button key={t} role="radio" aria-checked={st.tag === t} className={st.tag === t ? 'on' : ''} onClick={() => st.setTag(t)}>{t}</button>)}
91
91
  </div>
92
- <input className="inquery" type="search" placeholder="filter: a package, a script…" aria-label="filter every box" value={st.query} onChange={(e) => st.setQuery(e.currentTarget.value)} />
92
+ {/* focused as the page opens (⌘; then type a package name) */}
93
+ <input className="inquery" type="search" autoFocus placeholder="filter: a package, a script…" aria-label="filter every box" value={st.query} onChange={(e) => st.setQuery(e.currentTarget.value)} />
93
94
  <button type="button" className="acpill" disabled={st.loading} onClick={() => st.refresh()} title="read every package.json again now · an answer is kept 5 minutes">
94
95
  <span className={`acring${st.loading ? ' on' : ''}`}>↻</span>
95
96
  </button>
@@ -1,12 +1,13 @@
1
1
  // the page's command keys, read the same on every layout: ⌘1 to ⌘9 select the
2
2
  // nth repo of the project list, ⌘↑ and ⌘↓ the repo above or below, then the home row under the right hand: ⌘J
3
- // the picker, ⌘K the overview, ⌘L the activity, ⌘' the plan. ⌘B cycles the
4
- // dock at the bottom of the page (dock-st.ts): the line, slim, open.
5
- // A digit and the quote are read from the physical key (`code`), so an
3
+ // the picker, ⌘K the overview, ⌘L the activity, ⌘; the insights, ⌘' the plan.
4
+ // ⌘B folds the project list to its icon rail and back (layout-st.ts), ⌘⇧B
5
+ // cycles the dock at the bottom of the page (dock-st.ts): the line, slim, open.
6
+ // A digit, the semicolon and the quote are read from the physical key (`code`), so an
6
7
  // AZERTY keyboard, where the key under 1 types `&`, selects the same repo. Ctrl stands in for
7
8
  // ⌘ off a Mac. The macOS app binds none of these to a menu item, so they
8
9
  // reach the page (tests/home-strip.test.tsx reads ShipkitGui.swift).
9
- export type PageKey = { t: 'repo'; n: number } | { t: 'step'; by: -1 | 1 } | { t: 'home' } | { t: 'planned' } | { t: 'activity' } | { t: 'picker' } | { t: 'dock' }
10
+ export type PageKey = { t: 'repo'; n: number } | { t: 'step'; by: -1 | 1 } | { t: 'home' } | { t: 'planned' } | { t: 'activity' } | { t: 'insights' } | { t: 'picker' } | { t: 'dock' } | { t: 'rail' }
10
11
 
11
12
  type KeyLike = { key: string; code: string; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean }
12
13
 
@@ -14,26 +15,29 @@ export function pageKey(e: KeyLike): PageKey | null {
14
15
  if (!(e.metaKey || e.ctrlKey) || e.altKey) return null
15
16
  const digit = /^Digit([1-9])$/.exec(e.code)
16
17
  if (digit != null) return { t: 'repo', n: Number(digit[1]) }
17
- if (e.shiftKey) return null
18
+ if (e.shiftKey) return e.code === 'KeyB' || e.key.toLowerCase() === 'b' ? { t: 'dock' } : null
18
19
  if (e.key === 'ArrowUp') return { t: 'step', by: -1 }
19
20
  if (e.key === 'ArrowDown') return { t: 'step', by: 1 }
20
21
  if (e.code === 'Quote' || e.key === "'") return { t: 'planned' }
22
+ if (e.code === 'Semicolon' || e.key === ';') return { t: 'insights' }
21
23
  const k = e.key.toLowerCase()
22
24
  if (k === 'j') return { t: 'picker' }
23
25
  if (k === 'k') return { t: 'home' }
24
26
  if (k === 'l') return { t: 'activity' }
25
- if (k === 'b') return { t: 'dock' }
27
+ if (k === 'b') return { t: 'rail' }
26
28
  return null
27
29
  }
28
30
 
29
31
  /** the shortcut each place of the home carries on its button */
30
- export const PLACE_KEY: Record<'overview' | 'planned' | 'activity', string> & Partial<Record<string, string>> = { overview: '⌘K', planned: "⌘'", activity: '⌘L' }
32
+ export const PLACE_KEY: Record<'overview' | 'planned' | 'activity' | 'insights', string> & Partial<Record<string, string>> = { overview: '⌘K', planned: "⌘'", activity: '⌘L', insights: '⌘;' }
31
33
  /** the picker's shortcut, on the first button, before home */
32
34
  export const PICKER_KEY = '⌘J'
33
35
  /** the repo above and below, hinted at the project list's foot */
34
36
  export const STEP_KEYS = ['⌘↑', '⌘↓'] as const
35
37
  /** the dock's shortcut, on the dock's own header */
36
- export const DOCK_KEY = '⌘B'
38
+ export const DOCK_KEY = '⌘⇧B'
39
+ /** the project list between its full width and its icon rail */
40
+ export const RAIL_KEY = '⌘B'
37
41
 
38
42
  /** the tree's filter, on a repo page: a plain `/` focuses it, and its
39
43
  * badge at the input's right prints this */
@@ -54,8 +58,8 @@ export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'al
54
58
  * plus a letter only types a character inside a text field, where the page
55
59
  * leaves the key alone. Read from the physical key (`code`): on a Mac ⌥C
56
60
  * types ç, and AZERTY moves the letters */
57
- export type SectionId = 'readme' | 'contracts' | 'shipkit' | 'repo' | 'secrets' | 'when' | 'runs' | 'plan' | 'processes' | 'git' | 'asks' | 'reflections' | 'tests' | 'media' | 'wikis' | 'admin'
58
- export const SECTION_LETTER: Record<SectionId, string> = { readme: 'E', contracts: 'K', shipkit: 'S', repo: 'C', secrets: 'X', when: 'O', runs: 'R', plan: 'N', processes: 'P', git: 'G', asks: 'A', reflections: 'F', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
61
+ export type SectionId = 'readme' | 'contracts' | 'shipkit' | 'repo' | 'secrets' | 'when' | 'runs' | 'plan' | 'processes' | 'git' | 'asks' | 'review' | 'reflections' | 'tests' | 'media' | 'trailer' | 'wikis' | 'admin'
62
+ export const SECTION_LETTER: Record<SectionId, string> = { readme: 'E', review: 'V', trailer: 'L', contracts: 'K', shipkit: 'S', repo: 'C', secrets: 'X', when: 'O', runs: 'R', plan: 'N', processes: 'P', git: 'G', asks: 'A', reflections: 'F', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
59
63
  /** a section's words on its row, where the id alone would not say it */
60
64
  export const SECTION_LABEL: Partial<Record<SectionId, string>> = { shipkit: 'shipkit checks', repo: 'repo checks' }
61
65
  export const sectionLabel = (id: SectionId): string => SECTION_LABEL[id] ?? id
@@ -3,7 +3,9 @@
3
3
  // its separator (react-resizable-panels) and kept per browser in px, so the
4
4
  // next load renders at that width from its first paint. A stored value that
5
5
  // is not a number, or is out of bounds, reads as the default: no runtime
6
- // value can hand the page a list wider than LIST.max.
6
+ // value can hand the page a list wider than LIST.max. The list also folds to
7
+ // its icon rail: a drag below its min or ⌘B, kept per browser too, its icons
8
+ // small, medium or large (RAIL_ICONS), the rail as wide as they need.
7
9
  import { makeAutoObservable } from 'mobx'
8
10
 
9
11
  export type Bounds = { key: string; min: number; max: number; def: number }
@@ -11,7 +13,26 @@ export type Bounds = { key: string; min: number; max: number; def: number }
11
13
  /** the project list: icon and name on one line */
12
14
  export const LIST: Bounds = { key: 'hub:list-width', min: 140, max: 360, def: 200 }
13
15
  /** a repo's sidebar: the resource tree */
14
- export const SIDEBAR: Bounds = { key: 'hub:sidebar-width', min: 220, max: 640, def: 330 }
16
+ export const SIDEBAR: Bounds = { key: 'hub:sidebar-width', min: 150, max: 640, def: 330 }
17
+
18
+ /** the rail's icon sizes, in px: the foot's button cycles them */
19
+ export const RAIL_ICONS = { s: 18, m: 24, l: 32 } as const
20
+ export type RailIcon = keyof typeof RAIL_ICONS
21
+ /** the rail is its icon and this much on each side */
22
+ const RAIL_PAD = 13
23
+ /** the project list folded: its icons only, a row's name in its hover card */
24
+ export const railPx = (size: RailIcon): number => RAIL_ICONS[size] + 2 * RAIL_PAD
25
+ const RAIL_KEY = 'hub:list-rail'
26
+ const RAIL_ICON_KEY = 'hub:rail-icon'
27
+
28
+ function loadRailIcon(): RailIcon {
29
+ try {
30
+ const v = globalThis.localStorage?.getItem(RAIL_ICON_KEY) ?? ''
31
+ return v in RAIL_ICONS ? (v as RailIcon) : 'm'
32
+ } catch {
33
+ return 'm'
34
+ }
35
+ }
15
36
 
16
37
  /** a width in its bounds, or the default for anything else */
17
38
  export const clampWidth = (v: unknown, b: Bounds): number => {
@@ -33,8 +54,20 @@ function saveWidth(b: Bounds, px: number): void {
33
54
  } catch { /* private window or blocked site data: the width lasts the tab */ }
34
55
  }
35
56
 
57
+ function loadRail(): boolean {
58
+ try {
59
+ return globalThis.localStorage?.getItem(RAIL_KEY) === '1'
60
+ } catch {
61
+ return false
62
+ }
63
+ }
64
+
36
65
  export class LayoutSt {
37
66
  listWidth = loadWidth(LIST)
67
+ /** the list shows its icons only */
68
+ listRail = loadRail()
69
+ /** the rail's icon size */
70
+ railIcon: RailIcon = loadRailIcon()
38
71
  sidebarWidth = loadWidth(SIDEBAR)
39
72
 
40
73
  constructor() {
@@ -47,6 +80,34 @@ export class LayoutSt {
47
80
  saveWidth(LIST, this.listWidth)
48
81
  }
49
82
 
83
+ /** the list folded to its icons, or back at its kept width */
84
+ setRail(on: boolean): void {
85
+ if (this.listRail === on) return
86
+ this.listRail = on
87
+ try {
88
+ globalThis.localStorage?.setItem(RAIL_KEY, on ? '1' : '0')
89
+ } catch { /* private window or blocked site data: the rail lasts the tab */ }
90
+ }
91
+
92
+ /** the rail's width, its icon and its margins */
93
+ get railPx(): number {
94
+ return railPx(this.railIcon)
95
+ }
96
+
97
+ /** the rail's size button: small, medium, large, then small again */
98
+ cycleRailIcon(): void {
99
+ const all = Object.keys(RAIL_ICONS) as RailIcon[]
100
+ this.railIcon = all[(all.indexOf(this.railIcon) + 1) % all.length]!
101
+ try {
102
+ globalThis.localStorage?.setItem(RAIL_ICON_KEY, this.railIcon)
103
+ } catch { /* private window or blocked site data: the size lasts the tab */ }
104
+ }
105
+
106
+ /** ⌘B */
107
+ toggleRail(): void {
108
+ this.setRail(!this.listRail)
109
+ }
110
+
50
111
  /** a drag of the sidebar's separator ended at this width */
51
112
  setSidebarWidth(px: number): void {
52
113
  this.sidebarWidth = clampWidth(Math.round(px), SIDEBAR)
@@ -8,6 +8,7 @@
8
8
  // place, the check opens its own page in that repo.
9
9
  import { observer } from 'mobx-react-lite'
10
10
  import type { CSSProperties, MouseEvent, ReactNode } from 'react'
11
+ import { idVars } from './projects-svc.ts'
11
12
  import type { PlanRow } from '../../plan/planner.ts'
12
13
  import { Tip } from './floating.tsx'
13
14
  import type { HomeSt } from './home-st.ts'
@@ -62,14 +63,14 @@ const Card = observer(({ app, r, now }: { app: HomeSt; r: PlanRow; now: number }
62
63
  const Row = observer(({ app, r, now }: { app: HomeSt; r: PlanRow; now: number }) => {
63
64
  const st = app.planned
64
65
  const m = markOf(r)
65
- const colour = { '--pc': app.projects.identityOf(r.repo).colorDark } as CSSProperties
66
+ const colour = idVars(app.projects.identityOf(r.repo)) as CSSProperties
66
67
  return (
67
68
  <Tip className={`plr m-${m.mark}`} content={<Card app={app} r={r} now={now} />} tipClass="plcard" placement="right-start" fallbacks={['bottom-end', 'top-end']} interactive>
68
69
  <span className="plwhen">{whenWords(r, now)}</span>
69
70
  <span className="plwhat">
70
71
  <InPlace className="plrepo" href={repoPage(r.repo)} go={() => st.openRepo(r.repo)}>
71
72
  <Face app={app} repo={r.repo} />
72
- <span style={colour}>{r.repo}</span>
73
+ <span className="idc" style={colour}>{r.repo}</span>
73
74
  </InPlace>
74
75
  <span className="pldim"> · </span>
75
76
  <InPlace className="plcheck" href={checkAddress(r.repo, r.check)} go={() => st.openCheck(r.repo, r.check)}>{r.check}</InPlace>
@@ -15,7 +15,6 @@
15
15
  .m-running .plwhen{color:var(--accent)}
16
16
  .plwhat{min-width:0;overflow:hidden;text-overflow:ellipsis}
17
17
  .plrepo,.plcheck{color:inherit;text-decoration:none}
18
- .plrepo span{color:var(--pc)}
19
18
  .plrepo:hover span,.plcheck:hover{text-decoration:underline}
20
19
  .plface{width:14px;height:14px;margin-right:var(--s1);vertical-align:-2px;border-radius:3px;object-fit:contain}
21
20
  .pldim{color:var(--dim)}
@@ -22,16 +22,16 @@
22
22
  .pjaini{flex:none;width:14px;height:14px;border-radius:3px;background:var(--panel2)}
23
23
  /* the foot: sort (its title names the order), the step keys hint, archived
24
24
  carries its count and stays lit while its rows show */
25
- .pgsort,.pjarbtn{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s1);background:transparent;border:1px solid var(--line);border-radius:3px;
25
+ .pgsort,.pjarbtn,.pjfold{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s1);background:transparent;border:1px solid var(--line);border-radius:3px;
26
26
  color:var(--dim);font-size:11px;cursor:pointer}
27
27
  .pgsort{flex:0 1 auto;min-width:0}
28
28
  /* the step keys, a hint: the repo above or below */
29
29
  .pjstep{flex:none;display:inline-flex;gap:var(--s0);margin-left:auto;color:var(--dim);font-size:11px}
30
30
  .pjstep kbd{padding:0 var(--s1);border:1px solid var(--line);border-radius:3px;font:inherit}
31
31
  .pgsortl{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
32
- .pgsort .hico,.pjarbtn .hico{flex:none;width:14px;height:14px}
33
- .pgsort:hover,.pjarbtn:hover{color:var(--ink);border-color:var(--accent)}
34
- .pjarbtn.on{color:var(--ink);background:var(--sel);border-color:var(--accent)}
32
+ .pgsort .hico,.pjarbtn .hico,.pjfold .hico{flex:none;width:14px;height:14px}
33
+ .pgsort:hover,.pjarbtn:hover,.pjfold:hover{color:var(--ink);border-color:var(--accent)}
34
+ .pjarbtn.on,.pjfold[aria-pressed=true]{color:var(--ink);background:var(--sel);border-color:var(--accent)}
35
35
  .pjarbtn span{font-variant-numeric:tabular-nums}
36
36
  /* the sort menu: over the foot, placed by floating-ui (floating.tsx) */
37
37
  .pgmenu{z-index:30;min-width:180px;padding:var(--s1) 0;background:var(--panel);border:1px solid var(--line);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px}
@@ -50,6 +50,9 @@
50
50
  .pjrow:hover,.pjpl:hover{background:var(--panel2);color:var(--ink)}
51
51
  /* the shown repo: its row lit, marked on the left in its identity colour
52
52
  (a global page's line: in the accent) */
53
+ /* the repo's colour for the theme: colorDark, or colorLight on a light page (projects-svc idVars) */
54
+ .pjrow{--pjc:var(--idd)}
55
+ :root[data-theme=light] .pjrow{--pjc:var(--idl)}
53
56
  .pjrow.cur,.pjpl.cur{background:var(--sel);color:var(--pjc,var(--ink));box-shadow:inset 3px 0 0 var(--pjc,var(--accent));font-weight:600}
54
57
  .pjpl .hico{flex:none;width:18px;height:18px;color:inherit}
55
58
  /* the selected repo while a global page is up: still marked, dimmer */
@@ -119,3 +122,18 @@
119
122
  .rsep{position:relative;flex:none;width:1px;background:var(--line)}
120
123
  .rsep::before{content:'';position:absolute;top:0;bottom:0;left:-6px;right:-6px;cursor:col-resize;background:transparent;z-index:6}
121
124
  .rsep:hover,.rsep[data-separator="hover"],.rsep[data-separator="active"]{background:var(--accent)}
125
+ /* the rail: the list folded to its icons (layout-st RAIL_PX). The names, the
126
+ ⌘ hints and the step hint hide, every icon centres, the badge and the dots
127
+ ride the icon's corner, the foot stacks its buttons */
128
+ .pjlist.rail .pjname,.pjlist.rail .pjkey,.pjlist.rail .pjstep,.pjlist.rail .pgsortl,.pjlist.rail .pjarow span,.pjlist.rail .pgerr{display:none}
129
+ .pjlist.rail .pjrow,.pjlist.rail .pjpl,.pjlist.rail .pjarow{justify-content:center;padding:0}
130
+ /* the rail's icon size (layout-st RAIL_ICONS, the foot's S M L button): the repo icons and the row under them grow */
131
+ .pjlist.rail .pjrow{height:calc(var(--ri,18px) + 10px)}
132
+ .pjlist.rail .pjrow img,.pjlist.rail .pjrow .pgi{width:var(--ri,18px);height:var(--ri,18px)}
133
+ .pjlist.rail .pjrow .pgi{font-size:calc(var(--ri,18px) * .55)}
134
+ .pjsize{font:600 10px/1 ui-monospace,Menlo,monospace}
135
+ .pjlist.rail .pga{position:absolute;top:var(--s0);left:50%;margin-left:var(--s1);min-width:14px;height:14px;padding:0 var(--s0);border-radius:7px;font-size:9px;line-height:14px}
136
+ .pjlist.rail .pgd,.pjlist.rail .pgw{position:absolute;bottom:var(--s1);left:50%;margin-left:var(--s1)}
137
+ .pjlist.rail .pjfoot{flex-direction:column;height:auto;padding:var(--s1) 0}
138
+ .pjlist.rail .pjfoot button{justify-content:center;width:28px;padding:0}
139
+ .pjlist.rail .pjarbtn span{display:none}