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
@@ -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: 24, m: 32, l: 40 } 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 = 6
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)
@@ -70,3 +70,35 @@ export const renderReadme = (src: string, fileUrl: (rel: string) => string): str
70
70
  },
71
71
  exclusiveFilter: (frame) => frame.tag === 'img' && (frame.attribs.src ?? '') === '',
72
72
  })
73
+
74
+ /** a wiki page in the wikis page's preview: the card allowlist, and every ref
75
+ * the server resolved (`links`, token → node, graph-pages.ts) as an anchor
76
+ * that opens that page in place instead of a new tab, marked by its kind so
77
+ * the sheet tells them apart: `@` loads with the page (eager), `→`, a link or
78
+ * a path in a code span is read on demand (lazy, written `→ path` here
79
+ * whatever the page wrote) */
80
+ export const renderWikiPage = (src: string, links: Record<string, string>): string => {
81
+ const to = (t: string, eager = false): string | null => (links[t] == null ? null : `#wiki${eager ? '-eager' : ''}:${encodeURIComponent(links[t]!)}`)
82
+ // fences stay as written: a ref in a code block is an example, not a link
83
+ const linked = src.split(/(^```[\s\S]*?^```\s*$)/m).map((part, i) => i % 2 === 1 ? part : part
84
+ .replace(/\]\(([^)\s:]+\.md)\)/g, (m, t: string) => (to(t) == null ? m : `](${to(t)})`))
85
+ .replace(/→(\s*)([\w][\w./-]*\.md)\b/g, (m, sp: string, t: string) => (to(t) == null ? m : `→${sp}[${t}](${to(t)})`))
86
+ .replace(/(^|[\s(])@((?:\.{1,2}\/)*[\w][\w./-]*\.md)/gm, (m, pre: string, t: string) => (to(t, true) == null ? m : `${pre}[@${t}](${to(t, true)})`))
87
+ // a code span that is exactly a page's path: a lazy ref, its arrow added unless the page wrote one
88
+ .replace(/(→\s*)?(?<!\[)`([\w.][\w./-]*\.md)`/g, (m, arrow: string | undefined, t: string) => (to(t) == null ? m : `${arrow ?? '→ '}[\`${t}\`](${to(t)})`)),
89
+ ).join('')
90
+ return sanitizeHtml(marked.parse(linked) as string, {
91
+ allowedTags: [...CARD_TAGS, 'h5', 'h6', 'del'],
92
+ allowedAttributes: { a: ['href', 'target', 'rel', 'data-wiki', 'data-ref'] },
93
+ allowedSchemes: ['http', 'https'],
94
+ allowProtocolRelative: false,
95
+ transformTags: {
96
+ a: (tagName, attribs): sanitizeHtml.Tag => {
97
+ const href = attribs.href ?? ''
98
+ const ref = /^#wiki(-eager)?:(.*)$/.exec(href)
99
+ if (ref != null) return { tagName, attribs: { href, 'data-wiki': decodeURIComponent(ref[2]!), 'data-ref': ref[1] != null ? 'eager' : 'lazy' } }
100
+ return { tagName, attribs: { ...attribs, target: '_blank', rel: 'noreferrer' } }
101
+ },
102
+ },
103
+ })
104
+ }
@@ -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 */
@@ -66,8 +69,8 @@
66
69
  .pgi.h5{--c:var(--k5)}
67
70
  .pgi.h6{--c:var(--k6)}
68
71
  /* the row says three things and no more (projects-svc faceOf) */
69
- /* closed in VS Code: the icon and the name dim, the badge does not */
70
- .pjrow.dim img,.pjrow.dim .pgi,.pjrow.dim .pjname{opacity:.5}
72
+ /* closed in VS Code: the name dims, never the icon (the icon is how a repo is found, and the rail has no name) */
73
+ .pjrow.dim .pjname{opacity:.5}
71
74
  /* ⌘1 to ⌘9 select the first nine rows (keys.ts): the hint sits at the row's end */
72
75
  .pjkey{flex:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
73
76
  .pjrow:hover .pjkey,.pjrow.cur .pjkey,.pjpl:hover .pjkey,.pjpl.cur .pjkey{opacity:1}
@@ -119,3 +122,20 @@
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,24px) + 6px)}
132
+ .pjlist.rail .pjrow img,.pjlist.rail .pjrow .pgi{width:var(--ri,24px);height:var(--ri,24px);border-radius:6px}
133
+ .pjlist.rail .pjrow .pgi{font-size:calc(var(--ri,24px) * .5)}
134
+ /* the app icon is a rounded plate already (core/app-icon.ts) */
135
+ .pjlist.rail .pjrow img.pjapp{border-radius:0}
136
+ .pjsize{font:600 10px/1 ui-monospace,Menlo,monospace}
137
+ .pjlist.rail .pga{position:absolute;top:0;right:calc(50% - var(--ri,24px) / 2 - 4px);min-width:14px;height:14px;padding:0 var(--s0);border-radius:7px;font-size:9px;line-height:14px}
138
+ .pjlist.rail .pgd,.pjlist.rail .pgw{position:absolute;bottom:var(--s0);right:calc(50% - var(--ri,24px) / 2 - 2px)}
139
+ .pjlist.rail .pjfoot{flex-direction:column;height:auto;padding:var(--s1) 0}
140
+ .pjlist.rail .pjfoot button{justify-content:center;width:28px;padding:0}
141
+ .pjlist.rail .pjarbtn span{display:none}
@@ -3,7 +3,7 @@
3
3
  // the shown repo's row marked in its identity colour. Its width is the
4
4
  // panel the page gives it (hub-app.tsx, react-resizable-panels). A row says
5
5
  // three things and no more (faceOf): the open asks as its one badge, a dim
6
- // icon while VS Code does not have the folder open, a thin edge breathing
6
+ // name (never the icon) while VS Code does not have the folder open, a thin edge breathing
7
7
  // while Claude sessions run in it; on the home, a dot while its hub starts
8
8
  // or crashed and a mark while a global process it uses is off. Everything
9
9
  // else is the hover card, placed by floating-ui beside the row. A click
@@ -13,7 +13,10 @@
13
13
  // labeled sorts (a pick sorts once, the button names the order the list is
14
14
  // in), archived shows or hides the archived repos just above the foot
15
15
  // (names only, no ⌘ number, a row opens the archived page, nothing loads),
16
- // the foot never moving. On the home a section of four lines on top, in the
16
+ // the foot never moving. ⌘B (or the foot's fold button, or a drag of the
17
+ // separator below the list's min) folds the list to its icon rail: the
18
+ // names, the ⌘ hints and the step hint hide, the badge rides the icon's
19
+ // corner, the hover card still names the row. On the home a section of four lines on top, in the
17
20
  // repo rows' style, opens the picker, the overview, the activity and the
18
21
  // plan, each with its shortcut at the line's end, the page shown lit. It
19
22
  // renders from the first paint with the list this browser kept, so a load
@@ -24,14 +27,17 @@ import { useEffect, type CSSProperties } from 'react'
24
27
  import { middleware, OPEN_MS } from './floating.tsx'
25
28
  import { Icon, ICON } from './icons.tsx'
26
29
  import { plainClick } from './home-st.ts'
27
- import { PICKER_KEY, PLACE_KEY, STEP_KEYS } from './keys.ts'
28
- import { cardModel, faceOf, hueOf, SORTS, type Place, type Project, type ProjectsSvc } from './projects-svc.ts'
30
+ import { PICKER_KEY, PLACE_KEY, RAIL_KEY, STEP_KEYS } from './keys.ts'
31
+ import { cardModel, faceOf, hueOf, idVars, SORTS, type Place, type Project, type ProjectsSvc } from './projects-svc.ts'
32
+ import { THEMES, type ThemePick } from './theme-st.ts'
33
+ import { RAIL_ICONS, type RailIcon } from './layout-st.ts'
29
34
 
30
- /** what the list needs of its page: the hub and the home both carry one */
31
- export type ListHost = { projects: ProjectsSvc }
35
+ /** what the list needs of its page: the hub and the home both carry one;
36
+ * the layout, when the page can fold the list to its rail */
37
+ export type ListHost = { projects: ProjectsSvc; layout?: { listRail: boolean; toggleRail(): void; railIcon: RailIcon; cycleRailIcon(): void }; theme?: { pick: ThemePick; cycle(): void } }
32
38
 
33
39
  /** the lines on top of the list, in their order: the picker, the overview,
34
- * then the places, their shortcuts ⌘J ⌘K ⌘L ⌘' top to bottom */
40
+ * then the places, each with its shortcut (keys.ts) */
35
41
  const PLACES: { id: Place | 'picker'; label: string; title: string }[] = [
36
42
  { id: 'picker', label: 'jump to…', title: 'jump to a page, a repo or one of its resources' },
37
43
  { id: 'overview', label: 'overview', title: 'the overview: shipkit, every global process and every repo on this machine' },
@@ -65,7 +71,8 @@ export const Row = observer(({ app, p, i }: { app: ListHost; p: Project; i: numb
65
71
  const dot = s.dotOf(p.name)
66
72
  const warn = s.warnOf(p.name)
67
73
  // the shown repo's row is marked in its own colour
68
- const mark = cur ? ({ '--pjc': s.identityOf(p.name).colorDark } as CSSProperties) : undefined
74
+ const mark = cur ? (idVars(s.identityOf(p.name)) as CSSProperties) : undefined
75
+ // folded, the rail wears the repo's app icon: its mark on its identity plate
69
76
  return (
70
77
  <>
71
78
  <button ref={f.refs.setReference} className={cls} aria-label={p.name} style={mark} draggable
@@ -89,7 +96,9 @@ export const Row = observer(({ app, p, i }: { app: ListHost; p: Project; i: numb
89
96
  },
90
97
  onDragEnd: () => s.dragEnd(),
91
98
  })}>
92
- <Face p={p} px={ROW_ICON} />
99
+ {app.layout?.listRail === true && p.appIcon != null
100
+ ? <img className="pjapp" src={p.appIcon} alt="" width={ROW_ICON} height={ROW_ICON} draggable={false} />
101
+ : <Face p={p} px={ROW_ICON} />}
93
102
  <span className="pjname">{p.name}</span>
94
103
  {face.badge != null ? <b className="pga">{face.badge}</b> : null}
95
104
  {face.busy ? <i className="pgs" /> : null}
@@ -155,8 +164,10 @@ export const Card = ({ p, now }: { p: Project; now: number }) => {
155
164
  export const ProjectList = observer(({ app }: { app: ListHost }) => {
156
165
  useEffect(() => app.projects.start(), [app])
157
166
  const s = app.projects
167
+ const lay = app.layout
168
+ const rail = lay?.listRail === true
158
169
  return (
159
- <nav className="pjlist" aria-label="projects">
170
+ <nav className={`pjlist${rail ? ' rail' : ''}`} aria-label="projects" style={rail ? ({ '--ri': `${RAIL_ICONS[lay!.railIcon]}px` } as CSSProperties) : undefined}>
160
171
  {s.onPlace == null && s.onPicker == null && s.error === '' ? null : (
161
172
  <div className="pjhead">
162
173
  {PLACES.map((pl) => {
@@ -195,6 +206,17 @@ export const ProjectList = observer(({ app }: { app: ListHost }) => {
195
206
  </div>
196
207
  ) : null}
197
208
  <div className="pjfoot">
209
+ {lay == null ? null : (
210
+ <button className="pjfold" aria-pressed={rail} title={`${rail ? 'show the names' : 'fold the list to its icons'} (${RAIL_KEY})`} onClick={() => lay.toggleRail()}>
211
+ <Icon d={ICON.rail} />
212
+ </button>
213
+ )}
214
+ {lay == null || !rail ? null : (
215
+ <button className="pjfold pjsize" aria-label={`icon size: ${lay.railIcon}`} title={`the icons: ${SIZE_WORDS[lay.railIcon]}; click for the next size`} onClick={() => lay.cycleRailIcon()}>
216
+ {lay.railIcon.toUpperCase()}
217
+ </button>
218
+ )}
219
+ {app.theme == null ? null : <ThemeButton t={app.theme} />}
198
220
  <SortButton s={s} />
199
221
  <span className="pjstep" title={`${STEP_KEYS[0]} and ${STEP_KEYS[1]} open the repo above or below, from any page`}>
200
222
  {STEP_KEYS.map((k) => <kbd key={k}>{k}</kbd>)}
@@ -211,6 +233,20 @@ export const ProjectList = observer(({ app }: { app: ListHost }) => {
211
233
  )
212
234
  })
213
235
 
236
+ const SIZE_WORDS: Record<RailIcon, string> = { s: 'small', m: 'medium', l: 'large' }
237
+
238
+ const THEME_ICON: Record<ThemePick, keyof typeof ICON> = { dark: 'moon', light: 'sun', auto: 'auto' }
239
+
240
+ /** the theme: dark, light or auto (the system's), one click to the next */
241
+ const ThemeButton = observer(({ t }: { t: { pick: ThemePick; cycle(): void } }) => {
242
+ const next = THEMES[(THEMES.indexOf(t.pick) + 1) % THEMES.length]
243
+ return (
244
+ <button className="pjfold" aria-label={`theme: ${t.pick}`} title={`theme: ${t.pick}${t.pick === 'auto' ? ' (follows the system)' : ''}; click for ${next}`} onClick={() => t.cycle()}>
245
+ <Icon d={ICON[THEME_ICON[t.pick]]} />
246
+ </button>
247
+ )
248
+ })
249
+
214
250
  /** the words for the order the list is in */
215
251
  const orderWords = (s: ProjectsSvc): string => {
216
252
  const m = s.sortMode
@@ -7,6 +7,10 @@ import type { ArchivedRow, ProjectRow as Project, Release } from '../project-gut
7
7
  import { identityOfName, type Identity } from '../../core/identity-color.ts'
8
8
  import { ownTrouble, reach } from './reach.ts'
9
9
 
10
+ /** a repo's two colours as the vars a `.idc` element or a list row reads,
11
+ * hub.css picking the one for the page's theme */
12
+ export const idVars = (id: Pick<Identity, 'colorDark' | 'colorLight'>): Record<'--idd' | '--idl', string> => ({ '--idd': id.colorDark, '--idl': id.colorLight })
13
+
10
14
  /** the list never says the page's server is gone, even where no machine
11
15
  * card is (a hub on its own): that hub's trouble card says it */
12
16
  const SAID_ELSEWHERE = true
@@ -130,8 +134,9 @@ export function sortNames(list: Project[], mode: SortMode): string[] {
130
134
  export type HubDot = 'starting' | 'crashed' | null
131
135
 
132
136
  /** what the icon face shows, and nothing more: at most ONE badge (the open
133
- * asks, the only fact that needs a human's hands), dim while VS Code does
134
- * not have the folder open, and a pulsing edge while Claude sessions run */
137
+ * asks, the only fact that needs a human's hands), a dim name while VS Code
138
+ * does not have the folder open (the icon stays whole), and a pulsing edge
139
+ * while Claude sessions run */
135
140
  export type Face = { badge: number | null; dim: boolean; busy: boolean }
136
141
  export const faceOf = (p: Project): Face => ({
137
142
  badge: p.asks > 0 ? p.asks : null,
@@ -8,7 +8,7 @@
8
8
  // own has none of the three. The counts read the severity filter, like everything under them,
9
9
  // and come from what the page already holds (the kept screen on a reload), so
10
10
  // none arrives late and moves the tree.
11
- import { RUNS_ID } from '../../collections/collection-types.ts'
11
+ import { REVIEW_COL, RUNS_ID, TRAILER_COL } from '../../collections/collection-types.ts'
12
12
  import type { CheckOwner, HashSel } from './hash-route.ts'
13
13
  import { sectionLabel, type SectionId } from './keys.ts'
14
14
  import type { RepoSt } from './repo-st.ts'
@@ -45,6 +45,8 @@ export function sectionOfSel(sel: HashSel | null, ownerOf: (checkItemId: string)
45
45
  case 'when': return 'when'
46
46
  case 'checks': return sel.from
47
47
  case 'admin': return 'admin'
48
+ case 'review': return 'review'
49
+ case 'trailer': return 'trailer'
48
50
  case 'tests': return 'tests'
49
51
  case 'asset': case 'cat': return 'media'
50
52
  case 'status': return null
@@ -54,6 +56,8 @@ export function sectionOfSel(sel: HashSel | null, ownerOf: (checkItemId: string)
54
56
  if (col === WIKIS) return 'wikis'
55
57
  if (col === 'contracts') return 'contracts'
56
58
  if (col === 'creds') return 'secrets'
59
+ if (col === REVIEW_COL) return 'review'
60
+ if (col === TRAILER_COL) return 'trailer'
57
61
  if (col === 'checks') return sel.t === 'item' ? ownerOf(sel.id) : null
58
62
  return col === 'asks' || col === 'tests' || col === 'reflections' ? col : null
59
63
  }
@@ -87,11 +91,14 @@ export function openSection(app: RepoSt, id: SectionId): void {
87
91
  }
88
92
  case 'wikis': return openWikis(app)
89
93
  case 'admin': return app.showAdmin()
94
+ case 'review': return app.showReview(app.review.selectedId)
95
+ case 'trailer': return app.showTrailer(app.trailer.trailerId, app.trailer.cutId)
90
96
  }
91
97
  }
92
98
 
93
- /** the sections whose page says it is empty, so an empty row still opens it */
94
- const EMPTY_PAGES = new Set<SectionId>(['processes'])
99
+ /** the sections whose page says it is empty, so an empty row still opens it: the review and trailer pages say how a
100
+ * first card or a first cut comes */
101
+ const EMPTY_PAGES = new Set<SectionId>(['processes', 'review', 'trailer'])
95
102
 
96
103
  export function repoSections(app: RepoSt): RepoSection[] {
97
104
  const has = (col: string): boolean => app.cols.colMeta.some((c) => c.id === col)
@@ -106,6 +113,11 @@ export function repoSections(app: RepoSt): RepoSection[] {
106
113
  const out: RepoSection[] = [line('readme', 'the README, what the repo is', none)]
107
114
  const ownReds = (from: CheckOwner): number => res.filter((r) => r.check?.from === from && r.state === 'red').length
108
115
  const unless = (on: boolean, why: string): string | null => (on ? null : why)
116
+ // review and trailer sit right under the readme: the pages he answers
117
+ out.push(line('review', 'the review cards agents send you: approve, fix, reject, circle a detail', counted(res.filter((r) => r.type === 'card').length, 'warn'),
118
+ unless(has(REVIEW_COL), 'no review card yet: an agent sends one with sk review add')))
119
+ out.push(line('trailer', 'the trailer cuts: play one with its timeline, pin a note on any frame', none,
120
+ unless(has(TRAILER_COL), 'no trailer: write trailer/<name>/main.trailer, then sk trailer')))
109
121
  out.push(line('contracts', 'what a release venue asks of this repo, and what it still lacks', counted(reds('contracts'), 'red'), unless(has('contracts'), 'no contract: no release venue applies to this repo')))
110
122
  const noChecks = unless(has('checks'), 'no checks yet: its hub has not answered')
111
123
  out.push(line('shipkit', 'the checks shipkit ships to every repo, one page each', counted(ownReds('shipkit'), 'red'), noChecks))