rvlib-shipkit 2.2.0 → 2.4.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 (140) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/package.json +1 -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 +5 -2
  12. package/src/cli/cmd_Commands.ts +9 -0
  13. package/src/cli/cmd_Review.ts +974 -0
  14. package/src/cli/cmd_Sound.ts +209 -0
  15. package/src/cli/cmd_Time.ts +112 -0
  16. package/src/cli/cmd_Trailer.ts +264 -6
  17. package/src/cli/kit/verb.ts +22 -4
  18. package/src/cli.ts +8 -4
  19. package/src/collections/collection-types.ts +6 -1
  20. package/src/collections/providers.ts +3 -0
  21. package/src/collections/review.ts +70 -0
  22. package/src/core/app-icon.ts +10 -6
  23. package/src/core/layout.ts +2 -0
  24. package/src/reflect/prompt-corpus.ts +15 -119
  25. package/src/reflect/transcript.ts +254 -0
  26. package/src/repo-config.ts +3 -0
  27. package/src/review/anim.ts +228 -0
  28. package/src/review/auto.ts +141 -0
  29. package/src/review/format.ts +158 -0
  30. package/src/review/http.ts +23 -0
  31. package/src/review/marks.ts +43 -0
  32. package/src/review/media.ts +45 -0
  33. package/src/review/model.ts +896 -0
  34. package/src/review/project.ts +126 -0
  35. package/src/review/role.ts +48 -0
  36. package/src/review/routes.ts +239 -0
  37. package/src/review/store.ts +1011 -0
  38. package/src/review/threads.ts +237 -0
  39. package/src/review/trailer-desk.ts +385 -0
  40. package/src/review/trailer-model.ts +183 -0
  41. package/src/review/trailer-routes.ts +136 -0
  42. package/src/review/unit.ts +183 -0
  43. package/src/review/watchers.ts +54 -0
  44. package/src/serve/embed.ts +35 -0
  45. package/src/serve/hub-app/activity-view.tsx +1 -1
  46. package/src/serve/hub-app/activity.css +0 -4
  47. package/src/serve/hub-app/admin-svc.ts +1 -1
  48. package/src/serve/hub-app/admin-view.tsx +1 -1
  49. package/src/serve/hub-app/dock-view.tsx +5 -4
  50. package/src/serve/hub-app/hash-route.ts +36 -1
  51. package/src/serve/hub-app/header-bar.tsx +3 -1
  52. package/src/serve/hub-app/home-st.ts +17 -3
  53. package/src/serve/hub-app/home-strip.tsx +3 -2
  54. package/src/serve/hub-app/home.html +1 -0
  55. package/src/serve/hub-app/hub-app.tsx +80 -11
  56. package/src/serve/hub-app/hub.css +83 -14
  57. package/src/serve/hub-app/icons.tsx +15 -0
  58. package/src/serve/hub-app/index.html +1 -0
  59. package/src/serve/hub-app/insights-view.tsx +4 -3
  60. package/src/serve/hub-app/keys.ts +14 -10
  61. package/src/serve/hub-app/layout-st.ts +63 -2
  62. package/src/serve/hub-app/md.ts +32 -0
  63. package/src/serve/hub-app/planned-view.tsx +3 -2
  64. package/src/serve/hub-app/planned.css +0 -1
  65. package/src/serve/hub-app/project-list.css +26 -6
  66. package/src/serve/hub-app/project-list.tsx +46 -10
  67. package/src/serve/hub-app/projects-svc.ts +7 -2
  68. package/src/serve/hub-app/repo-sections.ts +15 -3
  69. package/src/serve/hub-app/repo-st.ts +61 -4
  70. package/src/serve/hub-app/review/clip-st.ts +258 -0
  71. package/src/serve/hub-app/review/kind-view.tsx +240 -0
  72. package/src/serve/hub-app/review/kit.tsx +113 -0
  73. package/src/serve/hub-app/review/review-answer.tsx +269 -0
  74. package/src/serve/hub-app/review/review-board.tsx +46 -0
  75. package/src/serve/hub-app/review/review-list.tsx +156 -0
  76. package/src/serve/hub-app/review/review-page.tsx +96 -0
  77. package/src/serve/hub-app/review/review-st.ts +902 -0
  78. package/src/serve/hub-app/review/review-views.tsx +648 -0
  79. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  80. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  81. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  82. package/src/serve/hub-app/review.css +389 -0
  83. package/src/serve/hub-app/theme-st.ts +56 -0
  84. package/src/serve/hub-app/tree-build.ts +4 -0
  85. package/src/serve/hub-app/tree-row.tsx +1 -1
  86. package/src/serve/hub-app/tree-svc.ts +4 -0
  87. package/src/serve/hub-app/wiki-st.ts +190 -0
  88. package/src/serve/hub-app/wikis-view.tsx +134 -5
  89. package/src/serve/hub-runtime.ts +9 -7
  90. package/src/serve/project-gutter-route.ts +25 -1
  91. package/src/serve/project-gutter.ts +1 -1
  92. package/src/serve/same-origin.ts +5 -1
  93. package/src/serve/serve-hub.ts +15 -0
  94. package/src/shipkit-files.ts +9 -5
  95. package/src/shot/client.ts +17 -5
  96. package/src/shot/shooter.ts +19 -3
  97. package/src/shot/shot-ask.ts +9 -1
  98. package/src/shot/stepped-clip.ts +53 -0
  99. package/src/trailer/adapter.ts +90 -6
  100. package/src/trailer/build.ts +459 -59
  101. package/src/trailer/ffmpeg.ts +22 -0
  102. package/src/trailer/fonts.ts +55 -0
  103. package/src/trailer/footage.ts +112 -43
  104. package/src/trailer/grammar.ts +864 -127
  105. package/src/trailer/graph.ts +308 -49
  106. package/src/trailer/holes.ts +72 -0
  107. package/src/trailer/load-unit.ts +20 -3
  108. package/src/trailer/look.ts +20 -3
  109. package/src/trailer/motion.ts +207 -0
  110. package/src/trailer/passage.ts +129 -0
  111. package/src/trailer/plan.ts +82 -23
  112. package/src/trailer/prune.ts +204 -0
  113. package/src/trailer/score/compose.ts +256 -0
  114. package/src/trailer/score/dsp.ts +272 -0
  115. package/src/trailer/score/gm-render.ts +43 -0
  116. package/src/trailer/score/gm.ts +90 -0
  117. package/src/trailer/score/midi.ts +143 -0
  118. package/src/trailer/score/palette.ts +188 -0
  119. package/src/trailer/score/pattern.ts +98 -0
  120. package/src/trailer/score/score.ts +228 -0
  121. package/src/trailer/score/sound.ts +123 -0
  122. package/src/trailer/score/synth.ts +383 -0
  123. package/src/trailer/sound/audio.ts +148 -0
  124. package/src/trailer/sound/brand.ts +42 -0
  125. package/src/trailer/sound/comfy.ts +38 -0
  126. package/src/trailer/sound/eleven.ts +72 -0
  127. package/src/trailer/sound/fluid.ts +71 -0
  128. package/src/trailer/sound/gen.ts +139 -0
  129. package/src/trailer/sound/index.ts +60 -0
  130. package/src/trailer/sound/presets.ts +69 -0
  131. package/src/trailer/sound/run.ts +189 -0
  132. package/src/trailer/states.ts +76 -0
  133. package/src/trailer/stitch.ts +92 -9
  134. package/src/trailer/titles.ts +471 -150
  135. package/src/trailer/url.ts +18 -3
  136. package/src/trailer/vocab.ts +34 -4
  137. package/src/trailer/words.ts +275 -0
  138. package/src/wiki/graph-mount.ts +3 -0
  139. package/src/wiki/graph-page.ts +45 -9
  140. package/src/wiki/graph-pages.ts +128 -0
@@ -4,7 +4,8 @@
4
4
  // then the status as small pills that fold into +N rather than wrap. The
5
5
  // state is dock-st.ts; the height is the page's panel (hub-app.tsx).
6
6
  import { observer } from 'mobx-react-lite'
7
- import { useEffect, useState } from 'react'
7
+ import { useEffect, useState, type CSSProperties } from 'react'
8
+ import { idVars } from './projects-svc.ts'
8
9
  import { Tip } from './floating.tsx'
9
10
  import { Icon, ICON } from './icons.tsx'
10
11
  import type { HomeSt } from './home-st.ts'
@@ -57,8 +58,8 @@ const GroupLabel = observer(({ app, g }: { app: HomeSt; g: PlacedGroup }) => {
57
58
  return (
58
59
  <button className="dgroup" onClick={() => app.dock.openRepo(g.key)}
59
60
  title={`${g.key}: open the repo${g.hidden > 0 ? `\n${g.hidden} check run(s) did not fit in its lanes` : ''}`}>
60
- {p?.icon != null ? <img src={p.icon} alt="" width={12} height={12} draggable={false} /> : <i className="dgico" style={{ background: id.colorDark }} />}
61
- <span style={{ color: id.colorDark }}>{g.key}</span>
61
+ {p?.icon != null ? <img src={p.icon} alt="" width={12} height={12} draggable={false} /> : <i className="dgico idb" style={idVars(id) as CSSProperties} />}
62
+ <span className="idc" style={idVars(id) as CSSProperties}>{g.key}</span>
62
63
  {/* slim folds its lanes on purpose: the count is news only open */}
63
64
  {g.hidden > 0 && app.dock.size === 'open' ? <b>+{g.hidden}</b> : null}
64
65
  </button>
@@ -72,7 +73,7 @@ const PillFace = observer(({ app, p }: { app: HomeSt; p: DockPill }) => {
72
73
  <span className={`pchip ${p.tone}`}>
73
74
  {p.repo == null ? null : pj?.icon != null
74
75
  ? <img src={pj.icon} alt="" width={12} height={12} draggable={false} />
75
- : <i className="dgico" style={{ background: app.projects.identityOf(p.repo).colorDark }} />}
76
+ : <i className="dgico idb" style={idVars(app.projects.identityOf(p.repo)) as CSSProperties} />}
76
77
  {p.mark == null ? null : <b className={`pcmark ${p.markTone ?? 'dim'}`}>{p.mark}</b>}
77
78
  <span className="pctext">{p.text}</span>
78
79
  {p.age == null ? null : <span className="pcage">{p.age}</span>}
@@ -35,18 +35,28 @@ export type HashSel =
35
35
  | { t: 'when' }
36
36
  /** the checks one owner ships: shipkit's, or the repo's own */
37
37
  | { t: 'checks'; from: CheckOwner }
38
+ /** the review cards, `id` the open one, null for the first the list shows */
39
+ | { t: 'review'; id: string | null; q?: string }
40
+ /** a trailer's cuts, played with their notes: a trailer folder and one of its cuts, null for the first; the note
41
+ * open on it, and the notes filter when it is not the page's default (all) */
42
+ | { t: 'trailer'; trailer: string | null; cut: string | null; note?: string | null; filter?: 'open' | 'all' | null }
38
43
 
39
44
  /** whose a gate check is */
40
45
  export type CheckOwner = 'shipkit' | 'repo'
41
46
 
42
47
  export type ParsedHash = {
43
- kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'readme' | 'processes' | 'plan' | 'git' | 'when' | 'checks' | 'none'
48
+ kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'readme' | 'processes' | 'plan' | 'git' | 'when' | 'checks' | 'review' | 'trailer' | 'none'
44
49
  /** the collection, for an item. null for everything else */
45
50
  col: string | null
46
51
  id: string
47
52
  /** the media tool set, when the address carried one. An empty array is a
48
53
  * choice (every tool off), null is no statement at all */
49
54
  tools: string[] | null
55
+ /** a trailer address only: the note open, the notes filter */
56
+ note?: string
57
+ filter?: 'open' | 'all'
58
+ /** a review address only: its view and list filters, as `view=1&subject=place` */
59
+ q?: string
50
60
  }
51
61
 
52
62
  const ITEM = 'item:'
@@ -62,6 +72,8 @@ const PLAN = 'page:plan'
62
72
  const GIT = 'page:git'
63
73
  const WHEN = 'page:when'
64
74
  const CHECKS = 'page:checks:'
75
+ const REVIEW = 'page:review'
76
+ const TRAILER = 'page:trailer'
65
77
 
66
78
  /** what this selection puts in the url. null only for no selection at all,
67
79
  * and then the hash is left alone rather than wiped */
@@ -81,6 +93,13 @@ export function hashFor(sel: HashSel | null): string | null {
81
93
  case 'git': return GIT
82
94
  case 'when': return WHEN
83
95
  case 'checks': return `${CHECKS}${sel.from}`
96
+ // page:review:<id>?view=1&subject=place, the query only when it says something
97
+ case 'review': return `${sel.id == null ? REVIEW : `${REVIEW}:${sel.id}`}${sel.q ? `?${sel.q}` : ''}`
98
+ case 'trailer': {
99
+ // page:trailer:<trailer>/<cut>/<note>?filter=all, each part only when it says something
100
+ const note = sel.trailer != null && sel.note ? `/${sel.cut ?? ''}/${sel.note}` : sel.cut == null ? '' : `/${sel.cut}`
101
+ return `${sel.trailer == null ? TRAILER : `${TRAILER}:${sel.trailer}${note}`}${sel.filter ? `?filter=${sel.filter}` : ''}`
102
+ }
84
103
  }
85
104
  }
86
105
 
@@ -95,6 +114,20 @@ export function parseHash(raw: string): ParsedHash {
95
114
  if (raw === PLAN) return { kind: 'plan', col: null, id: '', tools: null }
96
115
  if (raw === GIT) return { kind: 'git', col: null, id: '', tools: null }
97
116
  if (raw === WHEN) return { kind: 'when', col: null, id: '', tools: null }
117
+ if (raw === REVIEW || raw.startsWith(`${REVIEW}:`) || raw.startsWith(`${REVIEW}?`)) {
118
+ const qi = raw.indexOf('?')
119
+ const path = qi === -1 ? raw : raw.slice(0, qi)
120
+ const q = qi === -1 ? '' : raw.slice(qi + 1)
121
+ return { kind: 'review', col: null, id: path.slice(REVIEW.length + 1), tools: null, ...(q ? { q } : {}) }
122
+ }
123
+ // the trailer folder rides in `col`, its cut in `id`
124
+ if (raw === TRAILER || raw.startsWith(`${TRAILER}:`) || raw.startsWith(`${TRAILER}?`)) {
125
+ const qi = raw.indexOf('?')
126
+ const path = qi === -1 ? raw : raw.slice(0, qi)
127
+ const [trailer = '', cut = '', note = ''] = path.slice(TRAILER.length + 1).split('/')
128
+ const f = qi === -1 ? null : new URLSearchParams(raw.slice(qi + 1)).get('filter')
129
+ return { kind: 'trailer', col: trailer === '' ? null : trailer, id: cut, tools: null, ...(note ? { note } : {}), ...(f === 'open' || f === 'all' ? { filter: f } : {}) }
130
+ }
98
131
  if (raw === `${CHECKS}shipkit` || raw === `${CHECKS}repo`) return { kind: 'checks', col: null, id: raw.slice(CHECKS.length), tools: null }
99
132
  if (raw.startsWith(`${PROCESSES}:`)) {
100
133
  const id = raw.slice(PROCESSES.length + 1)
@@ -148,6 +181,8 @@ export function selOf(p: ParsedHash): HashSel | null {
148
181
  case 'git': return { t: 'git' }
149
182
  case 'when': return { t: 'when' }
150
183
  case 'checks': return { t: 'checks', from: p.id as CheckOwner }
184
+ case 'review': return { t: 'review', id: p.id === '' ? null : p.id, ...(p.q ? { q: p.q } : {}) }
185
+ case 'trailer': return { t: 'trailer', trailer: p.col, cut: p.id === '' ? null : p.id, ...(p.note ? { note: p.note } : {}), ...(p.filter ? { filter: p.filter } : {}) }
151
186
  }
152
187
  }
153
188
 
@@ -14,6 +14,8 @@ import { Icon, ICON } from './icons.tsx'
14
14
  import { Pop, Tip } from './floating.tsx'
15
15
  import { FloatCards, type Card } from './float-cards.tsx'
16
16
  import type { RepoSt } from './repo-st.ts'
17
+ import type { CSSProperties } from 'react'
18
+ import { idVars } from './projects-svc.ts'
17
19
 
18
20
  /** the repo's hub has not answered on the home: its reason and its fix show
19
21
  * instead of the page. A screen this browser kept shows meanwhile, until
@@ -113,7 +115,7 @@ export const RepoTools = observer(({ app }: { app: RepoSt }) => (
113
115
  export const HeaderBar = observer(({ app }: { app: RepoSt }) => (
114
116
  <>
115
117
  <header className="hdr">
116
- <span className="hname" style={app.name === '' ? undefined : { color: app.home.projects.identityOf(app.name).colorDark }}>{app.name !== '' ? app.name : 'hub'}</span>
118
+ <span className={app.name === '' ? 'hname' : 'hname idc'} style={app.name === '' ? undefined : (idVars(app.home.projects.identityOf(app.name)) as CSSProperties)}>{app.name !== '' ? app.name : 'hub'}</span>
117
119
  <span className="sp" />
118
120
  <RepoTools app={app} />
119
121
  <RepoRun app={app} />
@@ -14,6 +14,7 @@ import { makeAutoObservable, observableShallow, reaction, type IReactionDisposer
14
14
  import { currentHash, hashFor } from './hash-route.ts'
15
15
  import { MachineSt, type MachineOpts } from './machine-st.ts'
16
16
  import { LayoutSt } from './layout-st.ts'
17
+ import { ThemeSt } from './theme-st.ts'
17
18
  import { DockSt } from './dock-st.ts'
18
19
  import { PaletteSt } from './palette-st.ts'
19
20
  import { PlannedSt } from './planned-st.ts'
@@ -60,6 +61,8 @@ export const plainClick = (e: { button: number; metaKey: boolean; ctrlKey: boole
60
61
  export type HomeOpts = MachineOpts & {
61
62
  /** true on the home's web; false on a hub serving on its own */
62
63
  home: boolean
64
+ /** the one repo's base, read from where the page sits: the embed page under a proxy's prefix (embed.ts) */
65
+ base?: string
63
66
  }
64
67
 
65
68
  export class HomeSt {
@@ -78,6 +81,8 @@ export class HomeSt {
78
81
  layout: LayoutSt
79
82
  /** the run chart docked under the whole page */
80
83
  dock: DockSt
84
+ /** dark, light or the system's */
85
+ readonly theme = new ThemeSt()
81
86
  page: Page
82
87
  /** the key of the repo picked last: its name on the home, '' for the one
83
88
  * repo of a hub of its own. A global page keeps it (only `shown` says
@@ -90,7 +95,10 @@ export class HomeSt {
90
95
  private prefetching = false
91
96
  private readonly onUrl = (): void => this.fromUrl()
92
97
 
98
+ private readonly base: string | undefined
99
+
93
100
  constructor(o: HomeOpts) {
101
+ this.base = o.base
94
102
  this.projects = new ProjectsSvc()
95
103
  this.planned = new PlannedSt(this)
96
104
  this.activity = new ActivitySt(this)
@@ -100,7 +108,7 @@ export class HomeSt {
100
108
  this.dock = new DockSt(this)
101
109
  this.machine = o.home ? new MachineSt(this, o) : null
102
110
  this.page = o.home ? 'overview' : 'repo'
103
- makeAutoObservable<this, 'disposers' | 'onUrl' | 'prefetching'>(this, { machine: false, projects: false, planned: false, activity: false, insights: false, palette: false, layout: false, dock: false, repos: observableShallow, disposers: false, onUrl: false, prefetching: false })
111
+ makeAutoObservable<this, 'disposers' | 'onUrl' | 'prefetching' | 'base'>(this, { base: false, machine: false, projects: false, planned: false, activity: false, insights: false, palette: false, layout: false, theme: false, dock: false, repos: observableShallow, disposers: false, onUrl: false, prefetching: false })
104
112
  const m = this.machine
105
113
  if (m != null) {
106
114
  this.projects.onSelect = (name) => this.select(name)
@@ -143,7 +151,7 @@ export class HomeSt {
143
151
  repo(key: string): RepoSt {
144
152
  let r = this.repos.get(key)
145
153
  if (r == null) {
146
- r = new RepoSt(this, key, this.machine == null ? '' : repoBase(key))
154
+ r = new RepoSt(this, key, this.base ?? (this.machine == null ? '' : repoBase(key)))
147
155
  this.repos.set(key, r)
148
156
  // what this browser kept of it paints before its hub answers
149
157
  void r.cols.restore()
@@ -151,9 +159,15 @@ export class HomeSt {
151
159
  return r
152
160
  }
153
161
 
162
+ /** the page is the embed page: one repo's page in another tool's frame (embed.ts) */
163
+ get embedded(): boolean {
164
+ return this.base !== undefined
165
+ }
166
+
154
167
  boot(): void {
155
168
  this.machine?.boot()
156
- this.dock.boot()
169
+ // the embed page shows one repo's page alone: no dock, so no request for the house's runs
170
+ if (this.base === undefined) this.dock.boot()
157
171
  this.disposers.push(reaction(() => this.title, (t) => { document.title = t }, { fireImmediately: true }))
158
172
  // back, forward, a pasted link and a hand edited hash all arrive
159
173
  // here; fromUrl is idempotent, so the two events firing together
@@ -15,7 +15,8 @@
15
15
  // while its hub cannot answer, with the fix. Every link of the home is a
16
16
  // page of the same app: a plain click moves it in place.
17
17
  import { observer } from 'mobx-react-lite'
18
- import type { ReactNode } from 'react'
18
+ import type { CSSProperties, ReactNode } from 'react'
19
+ import { idVars } from './projects-svc.ts'
19
20
  import type { OpenHow, ProcessIcon, ResourceView } from '../../home/resources.ts'
20
21
  import type { WorkerView } from '../../home/hubs.ts'
21
22
  import { NOT_INSTALLED } from '../../home/home-words.ts'
@@ -238,7 +239,7 @@ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
238
239
  <span className="hrepo">
239
240
  {/* the icon's box is there before the project list is, so the name never moves */}
240
241
  {p?.icon != null ? <img src={p.icon} alt="" width={18} height={18} /> : <i className="hrico" />}
241
- <b className="hrname" style={{ color: id.colorDark }}>{name}</b>
242
+ <b className="hrname idc" style={idVars(id) as CSSProperties}>{name}</b>
242
243
  </span>
243
244
  )
244
245
  })
@@ -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 data-home>
19
20
  <div id="root"></div>
@@ -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,10 @@
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;
23
+ /* a wiki page's refs (wikis-view.tsx): read on demand, loaded with the page */
24
+ --ref-lazy:#e8984a;--ref-eager:#6fa8f0;
21
25
  --wash:rgba(107,143,179,.15);--wash2:rgba(107,143,179,.11);--hair:rgba(107,143,179,.5);
22
26
  --errbg:#2a1a1a;--errline:#4a2c2c;--errink:#e3bebe;
23
27
  /* a running thing's wash, the green twin of errbg and errline */
@@ -25,6 +29,27 @@
25
29
  /* the ONE spacing scale: every padding, margin and gap in this sheet is a
26
30
  value from it or 0, and tests/hub-css-invariants.test.ts fails the rest */
27
31
  --s0:2px;--s1:4px;--s2:8px;--s3:12px;--s4:16px}
32
+ /* THE LIGHT TOKENS: the same names, read on a light page (theme-st.ts sets
33
+ <html data-theme>). Every token of the block above has its light value
34
+ here, and tests/hub-css-invariants.test.ts fails a missing one */
35
+ :root[data-theme=light]{--bg:#f6f7f9;--panel:#ffffff;
36
+ --bg-side:#eef0f3;--bg-top:#f2f4f7;--bg-dock:#eceff3;--panel2:#e9ecf0;--line:#d6dbe1;--ink:#1c2026;--dim:#5d646d;
37
+ --accent:#3b6896;--sel:#dbe6f2;--chk1:#e9ecf0;--chk2:#f7f8fa;
38
+ --red:#c03f3a;--warn:#b0700f;--ok:#2d8754;--blue:#2b69b0;
39
+ --scrim:rgba(20,24,30,.35);--shadow:rgba(20,24,30,.16);--solid:#000;
40
+ --hi1:rgba(0,0,0,.2);--hi2:rgba(0,0,0,.11);--hi3:rgba(0,0,0,.045);
41
+ --ground:#e3ede6;--groundline:#c6d6cb;--tag:#eef2f6;
42
+ --k1:#3f6690;--k2:#7055a3;--k3:#2b7c86;--k4:#a34e80;--k5:#5d646d;--k6:#4f56a8;
43
+ --review:#c0279d;
44
+ --ref-lazy:#b8600c;--ref-eager:#1f5fc8;
45
+ --wash:rgba(59,104,150,.12);--wash2:rgba(59,104,150,.08);--hair:rgba(59,104,150,.45);
46
+ --errbg:#fbe9e8;--errline:#efc4c2;--errink:#8a2520;
47
+ --okbg:#e6f3eb;--okline:#9ccdaf}
48
+ /* a repo's own colour, the one fit for the theme (projects-svc idVars) */
49
+ .idc{color:var(--idd)}
50
+ :root[data-theme=light] .idc{color:var(--idl)}
51
+ .idb{background:var(--idd)}
52
+ :root[data-theme=light] .idb{background:var(--idl)}
28
53
  *{box-sizing:border-box}html,body,#root{height:100%}
29
54
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
30
55
  /* a link with no class of its own: the browser default is dark blue on this dark page */
@@ -43,7 +68,9 @@ a,a:visited{color:var(--accent)}
43
68
  .repo{flex:1;min-height:0;display:flex;flex-direction:column}
44
69
  .repo[hidden]{display:none}
45
70
  .appbody{flex:1;min-height:0}
46
- .pside{display:flex;flex-direction:column}
71
+ .pside{display:flex;flex-direction:column;container-type:inline-size}
72
+ /* a narrow sidebar keeps the names: the key hints and the filter's letters go first */
73
+ @container (max-width:200px){.tsec .pjkey,.ssearch .seg .sk{display:none}.ssearch{padding:var(--s2)}}
47
74
  .pside > aside{flex:1}
48
75
  .pmain{display:flex;flex-direction:column}
49
76
  .pmain > main{flex:1}
@@ -101,14 +128,21 @@ aside > .tscroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3
101
128
  .rsecn.nred{background:var(--red)}
102
129
  .rsecn.nwarn{background:var(--warn)}
103
130
  .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}
131
+ /* review: its own hue on the icon, the label and the mark, empty or not, so the eye finds it */
132
+ .tsec.s-review,.tsec.s-review:hover,.tsec.s-review.cur{color:var(--review)}
133
+ .tsec.s-review.empty{opacity:.75}
134
+ .tsec.s-review.cur{box-shadow:inset 3px 0 0 var(--review)}
135
+ .tsec.s-review .rsecn.nwarn{background:var(--review)}
136
+ /* THE SELECT SHAPE: every pick on the page, a native select or a segmented
137
+ bar of exclusive buttons (.seg), is one 22px pill with a 1px line border;
138
+ the live segment is the accent on the selection wash. Nothing else
139
+ draws a select: a new one takes .seg or a plain select */
140
+ .seg{display:inline-flex;align-items:stretch;height:22px;border:1px solid var(--line);border-radius:999px;overflow:hidden;flex:none}
141
+ .seg button{display:inline-flex;align-items:center;background:transparent;color:var(--dim);border:0;border-radius:0;
142
+ padding:0 var(--s2);font-size:12px;font-weight:500;line-height:1;text-transform:lowercase;letter-spacing:0;cursor:pointer}
109
143
  .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)}
144
+ .seg button:hover{color:var(--ink);background:var(--panel2)}
145
+ .seg button.on{color:var(--accent);background:var(--sel)}
112
146
  .seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
113
147
  /* a narrow pane puts the severity filter under the input, never over it */
114
148
  .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 +235,6 @@ textarea{width:100%;min-height:64px;background:var(--bg);color:var(--ink);border
201
235
  .toolbar button{padding:var(--s1) var(--s3);font-size:12px}
202
236
  .toolbar .sp{flex:1}
203
237
  .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
238
  /* BUTTONS: small, flat, one pixel border on the panel colour, a 12px
206
239
  lowercase label. The accent is a border and an ink colour on the primary
207
240
  action, never a fill, and nothing here is a big rounded rectangle */
@@ -212,7 +245,9 @@ button.primary{color:var(--accent);border-color:var(--accent)}
212
245
  button.ghost{color:var(--dim)}
213
246
  button.ghost:hover{color:var(--ink)}
214
247
  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)}
248
+ 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}
249
+ select:hover:not(:disabled){border-color:var(--accent)}
250
+ input[type=number]{background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s2)}
216
251
  .status{font-size:12px;color:var(--dim);text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:46vw;flex:none}
217
252
  @keyframes rot{to{transform:rotate(360deg)}}
218
253
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
@@ -239,7 +274,8 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
239
274
  background:conic-gradient(var(--chk1) 90deg,var(--chk2) 0 180deg,var(--chk1) 0 270deg,var(--chk2) 0) 0/34px 34px}
240
275
  .stage img.roll{max-width:100%;max-height:100%;object-fit:contain;filter:drop-shadow(0 6px 20px var(--shadow));z-index:5;position:relative}
241
276
  .stage .ph,.stagewrap .ph{color:var(--dim)}
242
- .stagewrap{flex:1;min-height:0;overflow-y:auto;display:flex;align-items:flex-start;justify-content:center;padding:var(--s4)}
277
+ /* every page card of a repo sits in this column, read from the left, never centred */
278
+ .stagewrap{flex:1;min-height:0;overflow-y:auto;display:flex;align-items:flex-start;justify-content:flex-start;padding:var(--s4)}
243
279
  .stageaud{display:flex;flex-direction:column;align-items:center;gap:var(--s3);font-size:48px;padding:var(--s4)}
244
280
  .stageaud audio{width:min(480px,80vw)}
245
281
 
@@ -343,7 +379,7 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
343
379
  /* panel content inherits the hub's chunky default button — inside a card
344
380
  everything is DENSE, repo panel scripts get this for free */
345
381
  .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}
382
+ .fbody input:not([type=color]){padding:var(--s0) var(--s1);font-size:11.5px}
347
383
  .fbody label{font-size:11.5px}
348
384
  .fbody .jsonta{width:100%;min-height:160px}
349
385
  .vbar{flex:none;border-left:1px solid var(--line);background:var(--panel);display:flex;min-height:0;position:relative}
@@ -671,6 +707,39 @@ a.tok{color:var(--accent)}
671
707
  .gvframe{flex:1;min-height:0;position:relative;display:flex;border:1px solid var(--line);border-radius:3px;background:var(--bg)}
672
708
  .gvwait{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:var(--s2);font-size:12px;color:var(--dim)}
673
709
  .graphview iframe{position:relative;flex:1;min-height:0;width:100%;border:0;background:transparent}
710
+ /* the graph and its preview panel (wikis-view.tsx) */
711
+ .gvbody{flex:1;min-height:0}
712
+ .gvbody .gvframe{height:100%}
713
+ /* the wiki's search bar, its hits dropped under it over the graph */
714
+ .wsearch{position:relative;flex:0 1 360px;min-width:160px;display:flex}
715
+ .wsearch input{flex:1;min-width:0;height:22px;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:999px;padding:0 var(--s3);font:inherit;font-size:12px}
716
+ .wsearch input:focus{outline:none;border-color:var(--accent)}
717
+ .wslist{position:absolute;top:calc(100% + 4px);left:0;z-index:30;width:min(560px,80vw);max-height:60vh;overflow-y:auto;padding:var(--s1) 0;background:var(--panel);border:1px solid var(--line);border-radius:6px;box-shadow:0 6px 20px var(--shadow)}
718
+ .wshit{padding:var(--s1) var(--s3);cursor:pointer}
719
+ .wshit.on,.wshit:hover{background:var(--sel)}
720
+ .wshead{display:flex;align-items:baseline;gap:var(--s2);min-width:0}
721
+ .wshead b{flex:none;font-size:12.5px}
722
+ .wspath{min-width:0;color:var(--dim);font:11px/1.4 ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
723
+ .wssnip{color:var(--dim);font-size:11.5px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
724
+ .wsnone{padding:var(--s1) var(--s3);color:var(--dim);font-size:12px}
725
+ /* the page open beside the graph */
726
+ .wprev{height:100%;display:flex;flex-direction:column;min-width:0;background:var(--panel);border:1px solid var(--line);border-radius:3px}
727
+ .wphead{flex:none;display:flex;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line);min-width:0}
728
+ .wptitle{flex:0 1 auto;min-width:0;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
729
+ .wphead .wspath{flex:1}
730
+ .wpplace{flex:none}
731
+ .wpplace button{padding:0 var(--s1)}
732
+ .wpplace .hico{width:14px;height:14px}
733
+ .wpbtn{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s2);border:1px solid var(--line);border-radius:999px;color:var(--dim);font-size:11.5px;text-decoration:none;background:transparent;cursor:pointer}
734
+ .wpbtn:hover{color:var(--ink);border-color:var(--accent)}
735
+ .wpbtn .hico{width:13px;height:13px}
736
+ .wpclose{flex:none;padding:0 var(--s2);font-size:14px;line-height:1.2}
737
+ /* the refs of a page: on demand (→, a link, a path) orange, loaded with it (@) blue and bold */
738
+ .wpbody a[data-ref=lazy]{color:var(--ref-lazy)}
739
+ .wpbody a[data-ref=lazy] code{color:var(--ref-lazy)}
740
+ .wpbody a[data-ref=eager]{color:var(--ref-eager);font-weight:700}
741
+ .wpbody{flex:1;min-height:0;overflow-y:auto;padding:var(--s2) var(--s4) var(--s4)}
742
+ .wpbody a[data-wiki]{text-decoration:underline;text-decoration-style:dotted;text-underline-offset:2px;cursor:pointer}
674
743
 
675
744
  /* ── comfy item view (comfy-ts's own panel fills the stage) ── */
676
745
  .comfyview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
@@ -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,18 @@ 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
+ // where the wiki preview sits: a frame with its panel's edge
48
+ paneLeft: 'M3 4h14v12H3zM8 4v12M4.5 7h2M4.5 9.5h2',
49
+ paneRight: 'M3 4h14v12H3zM12 4v12M13.5 7h2M13.5 9.5h2',
50
+ paneBottom: 'M3 4h14v12H3zM3 11h14M6 13.5h8',
51
+ copy: 'M7 7h9v10H7zM4 13V3h9',
52
+ editor: 'M13 3l4 3v8l-4 3-7-6 7-5zM3 7.5l3 2.5-3 2.5',
53
+ // the project list's fold: a panel with its rail marked
54
+ rail: 'M3 4h14v12H3zM7.5 4v12',
40
55
  // a process's own switches on the overview
41
56
  play: 'M6.5 4.5v11l9-5.5z',
42
57
  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>