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.
- package/CHANGELOG.md +47 -0
- package/package.json +1 -1
- package/src/agent-time/mask.ts +43 -0
- package/src/agent-time/render.ts +187 -0
- package/src/agent-time/report.ts +335 -0
- package/src/agent-time/shape.ts +271 -0
- package/src/agent-time/signals.ts +84 -0
- package/src/agent-time/store.ts +379 -0
- package/src/agent-time/timeline.ts +218 -0
- package/src/agent-time/turns.ts +135 -0
- package/src/assets/kits/identity-kit.ts +5 -2
- package/src/cli/cmd_Commands.ts +9 -0
- package/src/cli/cmd_Review.ts +974 -0
- package/src/cli/cmd_Sound.ts +209 -0
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +264 -6
- package/src/cli/kit/verb.ts +22 -4
- package/src/cli.ts +8 -4
- package/src/collections/collection-types.ts +6 -1
- package/src/collections/providers.ts +3 -0
- package/src/collections/review.ts +70 -0
- package/src/core/app-icon.ts +10 -6
- package/src/core/layout.ts +2 -0
- package/src/reflect/prompt-corpus.ts +15 -119
- package/src/reflect/transcript.ts +254 -0
- package/src/repo-config.ts +3 -0
- package/src/review/anim.ts +228 -0
- package/src/review/auto.ts +141 -0
- package/src/review/format.ts +158 -0
- package/src/review/http.ts +23 -0
- package/src/review/marks.ts +43 -0
- package/src/review/media.ts +45 -0
- package/src/review/model.ts +896 -0
- package/src/review/project.ts +126 -0
- package/src/review/role.ts +48 -0
- package/src/review/routes.ts +239 -0
- package/src/review/store.ts +1011 -0
- package/src/review/threads.ts +237 -0
- package/src/review/trailer-desk.ts +385 -0
- package/src/review/trailer-model.ts +183 -0
- package/src/review/trailer-routes.ts +136 -0
- package/src/review/unit.ts +183 -0
- package/src/review/watchers.ts +54 -0
- package/src/serve/embed.ts +35 -0
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/activity.css +0 -4
- package/src/serve/hub-app/admin-svc.ts +1 -1
- package/src/serve/hub-app/admin-view.tsx +1 -1
- package/src/serve/hub-app/dock-view.tsx +5 -4
- package/src/serve/hub-app/hash-route.ts +36 -1
- package/src/serve/hub-app/header-bar.tsx +3 -1
- package/src/serve/hub-app/home-st.ts +17 -3
- package/src/serve/hub-app/home-strip.tsx +3 -2
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +80 -11
- package/src/serve/hub-app/hub.css +83 -14
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/insights-view.tsx +4 -3
- package/src/serve/hub-app/keys.ts +14 -10
- package/src/serve/hub-app/layout-st.ts +63 -2
- package/src/serve/hub-app/md.ts +32 -0
- package/src/serve/hub-app/planned-view.tsx +3 -2
- package/src/serve/hub-app/planned.css +0 -1
- package/src/serve/hub-app/project-list.css +26 -6
- package/src/serve/hub-app/project-list.tsx +46 -10
- package/src/serve/hub-app/projects-svc.ts +7 -2
- package/src/serve/hub-app/repo-sections.ts +15 -3
- package/src/serve/hub-app/repo-st.ts +61 -4
- package/src/serve/hub-app/review/clip-st.ts +258 -0
- package/src/serve/hub-app/review/kind-view.tsx +240 -0
- package/src/serve/hub-app/review/kit.tsx +113 -0
- package/src/serve/hub-app/review/review-answer.tsx +269 -0
- package/src/serve/hub-app/review/review-board.tsx +46 -0
- package/src/serve/hub-app/review/review-list.tsx +156 -0
- package/src/serve/hub-app/review/review-page.tsx +96 -0
- package/src/serve/hub-app/review/review-st.ts +902 -0
- package/src/serve/hub-app/review/review-views.tsx +648 -0
- package/src/serve/hub-app/review/trailer-page.tsx +416 -0
- package/src/serve/hub-app/review/trailer-st.ts +602 -0
- package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
- package/src/serve/hub-app/review.css +389 -0
- package/src/serve/hub-app/theme-st.ts +56 -0
- package/src/serve/hub-app/tree-build.ts +4 -0
- package/src/serve/hub-app/tree-row.tsx +1 -1
- package/src/serve/hub-app/tree-svc.ts +4 -0
- package/src/serve/hub-app/wiki-st.ts +190 -0
- package/src/serve/hub-app/wikis-view.tsx +134 -5
- package/src/serve/hub-runtime.ts +9 -7
- package/src/serve/project-gutter-route.ts +25 -1
- package/src/serve/project-gutter.ts +1 -1
- package/src/serve/same-origin.ts +5 -1
- package/src/serve/serve-hub.ts +15 -0
- package/src/shipkit-files.ts +9 -5
- package/src/shot/client.ts +17 -5
- package/src/shot/shooter.ts +19 -3
- package/src/shot/shot-ask.ts +9 -1
- package/src/shot/stepped-clip.ts +53 -0
- package/src/trailer/adapter.ts +90 -6
- package/src/trailer/build.ts +459 -59
- package/src/trailer/ffmpeg.ts +22 -0
- package/src/trailer/fonts.ts +55 -0
- package/src/trailer/footage.ts +112 -43
- package/src/trailer/grammar.ts +864 -127
- package/src/trailer/graph.ts +308 -49
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +20 -3
- package/src/trailer/look.ts +20 -3
- package/src/trailer/motion.ts +207 -0
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +82 -23
- package/src/trailer/prune.ts +204 -0
- package/src/trailer/score/compose.ts +256 -0
- package/src/trailer/score/dsp.ts +272 -0
- package/src/trailer/score/gm-render.ts +43 -0
- package/src/trailer/score/gm.ts +90 -0
- package/src/trailer/score/midi.ts +143 -0
- package/src/trailer/score/palette.ts +188 -0
- package/src/trailer/score/pattern.ts +98 -0
- package/src/trailer/score/score.ts +228 -0
- package/src/trailer/score/sound.ts +123 -0
- package/src/trailer/score/synth.ts +383 -0
- package/src/trailer/sound/audio.ts +148 -0
- package/src/trailer/sound/brand.ts +42 -0
- package/src/trailer/sound/comfy.ts +38 -0
- package/src/trailer/sound/eleven.ts +72 -0
- package/src/trailer/sound/fluid.ts +71 -0
- package/src/trailer/sound/gen.ts +139 -0
- package/src/trailer/sound/index.ts +60 -0
- package/src/trailer/sound/presets.ts +69 -0
- package/src/trailer/sound/run.ts +189 -0
- package/src/trailer/states.ts +76 -0
- package/src/trailer/stitch.ts +92 -9
- package/src/trailer/titles.ts +471 -150
- package/src/trailer/url.ts +18 -3
- package/src/trailer/vocab.ts +34 -4
- package/src/trailer/words.ts +275 -0
- package/src/wiki/graph-mount.ts +3 -0
- package/src/wiki/graph-page.ts +45 -9
- 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="
|
|
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="
|
|
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
|
-
|
|
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.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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: '
|
|
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 = '
|
|
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:
|
|
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)
|
package/src/serve/hub-app/md.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
70
|
-
.pjrow.dim
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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 ? (
|
|
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
|
-
|
|
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=
|
|
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
|
|
134
|
-
* not have the folder open, and a pulsing edge
|
|
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
|
-
|
|
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))
|