rvlib-shipkit 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -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/cli/cmd_Commands.ts +5 -0
- package/src/cli/cmd_Review.ts +948 -0
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +259 -6
- package/src/cli/kit/verb.ts +14 -4
- package/src/cli.ts +6 -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/layout.ts +2 -0
- package/src/reflect/prompt-corpus.ts +15 -119
- package/src/reflect/transcript.ts +254 -0
- package/src/review/anim.ts +228 -0
- package/src/review/auto.ts +141 -0
- package/src/review/format.ts +150 -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 +854 -0
- package/src/review/project.ts +110 -0
- package/src/review/role.ts +48 -0
- package/src/review/routes.ts +228 -0
- package/src/review/store.ts +1002 -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 +165 -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/dock-view.tsx +5 -4
- package/src/serve/hub-app/hash-route.ts +28 -1
- package/src/serve/hub-app/header-bar.tsx +3 -1
- package/src/serve/hub-app/home-st.ts +10 -2
- 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 +45 -13
- package/src/serve/hub-app/icons.tsx +9 -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/planned-view.tsx +3 -2
- package/src/serve/hub-app/planned.css +0 -1
- package/src/serve/hub-app/project-list.css +22 -4
- package/src/serve/hub-app/project-list.tsx +41 -8
- package/src/serve/hub-app/projects-svc.ts +4 -0
- package/src/serve/hub-app/repo-sections.ts +15 -3
- package/src/serve/hub-app/repo-st.ts +45 -3
- package/src/serve/hub-app/review/clip-st.ts +208 -0
- package/src/serve/hub-app/review/kind-view.tsx +171 -0
- package/src/serve/hub-app/review/kit.tsx +113 -0
- package/src/serve/hub-app/review/review-answer.tsx +239 -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 +101 -0
- package/src/serve/hub-app/review/review-st.ts +779 -0
- package/src/serve/hub-app/review/review-views.tsx +503 -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 +329 -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-runtime.ts +5 -3
- package/src/serve/same-origin.ts +5 -1
- package/src/serve/serve-hub.ts +8 -0
- package/src/shipkit-files.ts +9 -5
- package/src/trailer/adapter.ts +58 -3
- package/src/trailer/build.ts +300 -41
- package/src/trailer/ffmpeg.ts +22 -0
- package/src/trailer/footage.ts +109 -40
- package/src/trailer/grammar.ts +444 -69
- package/src/trailer/graph.ts +199 -14
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +20 -3
- package/src/trailer/look.ts +17 -3
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +80 -21
- package/src/trailer/prune.ts +193 -0
- package/src/trailer/score/compose.ts +240 -0
- package/src/trailer/score/dsp.ts +272 -0
- package/src/trailer/score/palette.ts +188 -0
- package/src/trailer/score/pattern.ts +91 -0
- package/src/trailer/score/score.ts +214 -0
- package/src/trailer/score/sound.ts +112 -0
- package/src/trailer/score/synth.ts +361 -0
- package/src/trailer/states.ts +76 -0
- package/src/trailer/stitch.ts +92 -9
- package/src/trailer/titles.ts +123 -18
- package/src/trailer/vocab.ts +23 -4
- package/src/trailer/words.ts +254 -0
|
@@ -35,18 +35,26 @@ 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 }
|
|
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'
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
const ITEM = 'item:'
|
|
@@ -62,6 +70,8 @@ const PLAN = 'page:plan'
|
|
|
62
70
|
const GIT = 'page:git'
|
|
63
71
|
const WHEN = 'page:when'
|
|
64
72
|
const CHECKS = 'page:checks:'
|
|
73
|
+
const REVIEW = 'page:review'
|
|
74
|
+
const TRAILER = 'page:trailer'
|
|
65
75
|
|
|
66
76
|
/** what this selection puts in the url. null only for no selection at all,
|
|
67
77
|
* and then the hash is left alone rather than wiped */
|
|
@@ -81,6 +91,12 @@ export function hashFor(sel: HashSel | null): string | null {
|
|
|
81
91
|
case 'git': return GIT
|
|
82
92
|
case 'when': return WHEN
|
|
83
93
|
case 'checks': return `${CHECKS}${sel.from}`
|
|
94
|
+
case 'review': return sel.id == null ? REVIEW : `${REVIEW}:${sel.id}`
|
|
95
|
+
case 'trailer': {
|
|
96
|
+
// page:trailer:<trailer>/<cut>/<note>?filter=all, each part only when it says something
|
|
97
|
+
const note = sel.trailer != null && sel.note ? `/${sel.cut ?? ''}/${sel.note}` : sel.cut == null ? '' : `/${sel.cut}`
|
|
98
|
+
return `${sel.trailer == null ? TRAILER : `${TRAILER}:${sel.trailer}${note}`}${sel.filter ? `?filter=${sel.filter}` : ''}`
|
|
99
|
+
}
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
|
|
@@ -95,6 +111,15 @@ export function parseHash(raw: string): ParsedHash {
|
|
|
95
111
|
if (raw === PLAN) return { kind: 'plan', col: null, id: '', tools: null }
|
|
96
112
|
if (raw === GIT) return { kind: 'git', col: null, id: '', tools: null }
|
|
97
113
|
if (raw === WHEN) return { kind: 'when', col: null, id: '', tools: null }
|
|
114
|
+
if (raw === REVIEW || raw.startsWith(`${REVIEW}:`)) return { kind: 'review', col: null, id: raw.slice(REVIEW.length + 1), tools: null }
|
|
115
|
+
// the trailer folder rides in `col`, its cut in `id`
|
|
116
|
+
if (raw === TRAILER || raw.startsWith(`${TRAILER}:`) || raw.startsWith(`${TRAILER}?`)) {
|
|
117
|
+
const qi = raw.indexOf('?')
|
|
118
|
+
const path = qi === -1 ? raw : raw.slice(0, qi)
|
|
119
|
+
const [trailer = '', cut = '', note = ''] = path.slice(TRAILER.length + 1).split('/')
|
|
120
|
+
const f = qi === -1 ? null : new URLSearchParams(raw.slice(qi + 1)).get('filter')
|
|
121
|
+
return { kind: 'trailer', col: trailer === '' ? null : trailer, id: cut, tools: null, ...(note ? { note } : {}), ...(f === 'open' || f === 'all' ? { filter: f } : {}) }
|
|
122
|
+
}
|
|
98
123
|
if (raw === `${CHECKS}shipkit` || raw === `${CHECKS}repo`) return { kind: 'checks', col: null, id: raw.slice(CHECKS.length), tools: null }
|
|
99
124
|
if (raw.startsWith(`${PROCESSES}:`)) {
|
|
100
125
|
const id = raw.slice(PROCESSES.length + 1)
|
|
@@ -148,6 +173,8 @@ export function selOf(p: ParsedHash): HashSel | null {
|
|
|
148
173
|
case 'git': return { t: 'git' }
|
|
149
174
|
case 'when': return { t: 'when' }
|
|
150
175
|
case 'checks': return { t: 'checks', from: p.id as CheckOwner }
|
|
176
|
+
case 'review': return { t: 'review', id: p.id === '' ? null : p.id }
|
|
177
|
+
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
178
|
}
|
|
152
179
|
}
|
|
153
180
|
|
|
@@ -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=
|
|
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()
|
|
@@ -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={
|
|
242
|
+
<b className="hrname idc" style={idVars(id) as CSSProperties}>{name}</b>
|
|
242
243
|
</span>
|
|
243
244
|
)
|
|
244
245
|
})
|
|
@@ -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
|
-
|
|
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 === '
|
|
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}
|
|
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
|
|
10
|
-
|
|
9
|
+
the header and the dock read apart at a glance; the light values are the
|
|
10
|
+
next block) */
|
|
11
11
|
--bg-side:#131519;--bg-top:#171a1f;--bg-dock:#10151b;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
|
|
12
12
|
--accent:#6b8fb3;--sel:#1a2430;--chk1:#16181b;--chk2:#121417;
|
|
13
13
|
/* state, and nothing else: red broken · orange taste pending · green measured passing */
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
--ground:#1b2521;--groundline:#2a3630;--tag:#eef2f6;
|
|
19
19
|
/* the KIND hues: a row's type, never its state (tree-build TYPE_MARK) */
|
|
20
20
|
--k1:#8ca8c7;--k2:#a892c9;--k3:#6fb0b8;--k4:#c792b4;--k5:#9198a1;--k6:#8f95cf;
|
|
21
|
+
/* the review section's own hue: the one row of the tree meant to be seen first */
|
|
22
|
+
--review:#e879d4;
|
|
21
23
|
--wash:rgba(107,143,179,.15);--wash2:rgba(107,143,179,.11);--hair:rgba(107,143,179,.5);
|
|
22
24
|
--errbg:#2a1a1a;--errline:#4a2c2c;--errink:#e3bebe;
|
|
23
25
|
/* a running thing's wash, the green twin of errbg and errline */
|
|
@@ -25,6 +27,26 @@
|
|
|
25
27
|
/* the ONE spacing scale: every padding, margin and gap in this sheet is a
|
|
26
28
|
value from it or 0, and tests/hub-css-invariants.test.ts fails the rest */
|
|
27
29
|
--s0:2px;--s1:4px;--s2:8px;--s3:12px;--s4:16px}
|
|
30
|
+
/* THE LIGHT TOKENS: the same names, read on a light page (theme-st.ts sets
|
|
31
|
+
<html data-theme>). Every token of the block above has its light value
|
|
32
|
+
here, and tests/hub-css-invariants.test.ts fails a missing one */
|
|
33
|
+
:root[data-theme=light]{--bg:#f6f7f9;--panel:#ffffff;
|
|
34
|
+
--bg-side:#eef0f3;--bg-top:#f2f4f7;--bg-dock:#eceff3;--panel2:#e9ecf0;--line:#d6dbe1;--ink:#1c2026;--dim:#5d646d;
|
|
35
|
+
--accent:#3b6896;--sel:#dbe6f2;--chk1:#e9ecf0;--chk2:#f7f8fa;
|
|
36
|
+
--red:#c03f3a;--warn:#b0700f;--ok:#2d8754;--blue:#2b69b0;
|
|
37
|
+
--scrim:rgba(20,24,30,.35);--shadow:rgba(20,24,30,.16);--solid:#000;
|
|
38
|
+
--hi1:rgba(0,0,0,.2);--hi2:rgba(0,0,0,.11);--hi3:rgba(0,0,0,.045);
|
|
39
|
+
--ground:#e3ede6;--groundline:#c6d6cb;--tag:#eef2f6;
|
|
40
|
+
--k1:#3f6690;--k2:#7055a3;--k3:#2b7c86;--k4:#a34e80;--k5:#5d646d;--k6:#4f56a8;
|
|
41
|
+
--review:#c0279d;
|
|
42
|
+
--wash:rgba(59,104,150,.12);--wash2:rgba(59,104,150,.08);--hair:rgba(59,104,150,.45);
|
|
43
|
+
--errbg:#fbe9e8;--errline:#efc4c2;--errink:#8a2520;
|
|
44
|
+
--okbg:#e6f3eb;--okline:#9ccdaf}
|
|
45
|
+
/* a repo's own colour, the one fit for the theme (projects-svc idVars) */
|
|
46
|
+
.idc{color:var(--idd)}
|
|
47
|
+
:root[data-theme=light] .idc{color:var(--idl)}
|
|
48
|
+
.idb{background:var(--idd)}
|
|
49
|
+
:root[data-theme=light] .idb{background:var(--idl)}
|
|
28
50
|
*{box-sizing:border-box}html,body,#root{height:100%}
|
|
29
51
|
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
|
|
30
52
|
/* a link with no class of its own: the browser default is dark blue on this dark page */
|
|
@@ -43,7 +65,9 @@ a,a:visited{color:var(--accent)}
|
|
|
43
65
|
.repo{flex:1;min-height:0;display:flex;flex-direction:column}
|
|
44
66
|
.repo[hidden]{display:none}
|
|
45
67
|
.appbody{flex:1;min-height:0}
|
|
46
|
-
.pside{display:flex;flex-direction:column}
|
|
68
|
+
.pside{display:flex;flex-direction:column;container-type:inline-size}
|
|
69
|
+
/* a narrow sidebar keeps the names: the key hints and the filter's letters go first */
|
|
70
|
+
@container (max-width:200px){.tsec .pjkey,.ssearch .seg .sk{display:none}.ssearch{padding:var(--s2)}}
|
|
47
71
|
.pside > aside{flex:1}
|
|
48
72
|
.pmain{display:flex;flex-direction:column}
|
|
49
73
|
.pmain > main{flex:1}
|
|
@@ -101,14 +125,21 @@ aside > .tscroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3
|
|
|
101
125
|
.rsecn.nred{background:var(--red)}
|
|
102
126
|
.rsecn.nwarn{background:var(--warn)}
|
|
103
127
|
.tsec:hover .pjkey,.tsec.cur .pjkey{opacity:1}
|
|
104
|
-
/*
|
|
105
|
-
|
|
106
|
-
.
|
|
107
|
-
.
|
|
108
|
-
|
|
128
|
+
/* review: its own hue on the icon, the label and the mark, empty or not, so the eye finds it */
|
|
129
|
+
.tsec.s-review,.tsec.s-review:hover,.tsec.s-review.cur{color:var(--review)}
|
|
130
|
+
.tsec.s-review.empty{opacity:.75}
|
|
131
|
+
.tsec.s-review.cur{box-shadow:inset 3px 0 0 var(--review)}
|
|
132
|
+
.tsec.s-review .rsecn.nwarn{background:var(--review)}
|
|
133
|
+
/* THE SELECT SHAPE: every pick on the page, a native select or a segmented
|
|
134
|
+
bar of exclusive buttons (.seg), is one 22px pill with a 1px line border;
|
|
135
|
+
the live segment is the accent on the selection wash. Nothing else
|
|
136
|
+
draws a select: a new one takes .seg or a plain select */
|
|
137
|
+
.seg{display:inline-flex;align-items:stretch;height:22px;border:1px solid var(--line);border-radius:999px;overflow:hidden;flex:none}
|
|
138
|
+
.seg button{display:inline-flex;align-items:center;background:transparent;color:var(--dim);border:0;border-radius:0;
|
|
139
|
+
padding:0 var(--s2);font-size:12px;font-weight:500;line-height:1;text-transform:lowercase;letter-spacing:0;cursor:pointer}
|
|
109
140
|
.seg button+button{border-left:1px solid var(--line)}
|
|
110
|
-
.seg button:hover{color:var(--ink)}
|
|
111
|
-
.seg button.on{color:var(--accent);
|
|
141
|
+
.seg button:hover{color:var(--ink);background:var(--panel2)}
|
|
142
|
+
.seg button.on{color:var(--accent);background:var(--sel)}
|
|
112
143
|
.seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
|
|
113
144
|
/* a narrow pane puts the severity filter under the input, never over it */
|
|
114
145
|
.ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line)}
|
|
@@ -201,7 +232,6 @@ textarea{width:100%;min-height:64px;background:var(--bg);color:var(--ink);border
|
|
|
201
232
|
.toolbar button{padding:var(--s1) var(--s3);font-size:12px}
|
|
202
233
|
.toolbar .sp{flex:1}
|
|
203
234
|
.toolbar .xn{display:flex;gap:var(--s0);align-items:center;font-size:12px;color:var(--dim)}
|
|
204
|
-
.toolbar .xn select{padding:var(--s1);font-size:12px}
|
|
205
235
|
/* BUTTONS: small, flat, one pixel border on the panel colour, a 12px
|
|
206
236
|
lowercase label. The accent is a border and an ink colour on the primary
|
|
207
237
|
action, never a fill, and nothing here is a big rounded rectangle */
|
|
@@ -212,7 +242,9 @@ button.primary{color:var(--accent);border-color:var(--accent)}
|
|
|
212
242
|
button.ghost{color:var(--dim)}
|
|
213
243
|
button.ghost:hover{color:var(--ink)}
|
|
214
244
|
button:disabled{opacity:.4;cursor:default;border-color:var(--line);color:var(--dim)}
|
|
215
|
-
select
|
|
245
|
+
select{height:22px;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:999px;padding:0 var(--s2);font-size:12px;cursor:pointer}
|
|
246
|
+
select:hover:not(:disabled){border-color:var(--accent)}
|
|
247
|
+
input[type=number]{background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s2)}
|
|
216
248
|
.status{font-size:12px;color:var(--dim);text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:46vw;flex:none}
|
|
217
249
|
@keyframes rot{to{transform:rotate(360deg)}}
|
|
218
250
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
|
@@ -343,7 +375,7 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
|
|
|
343
375
|
/* panel content inherits the hub's chunky default button — inside a card
|
|
344
376
|
everything is DENSE, repo panel scripts get this for free */
|
|
345
377
|
.fbody button{padding:var(--s0) var(--s2);font-size:11.5px;font-weight:600;border-radius:3px}
|
|
346
|
-
.fbody input:not([type=color])
|
|
378
|
+
.fbody input:not([type=color]){padding:var(--s0) var(--s1);font-size:11.5px}
|
|
347
379
|
.fbody label{font-size:11.5px}
|
|
348
380
|
.fbody .jsonta{width:100%;min-height:160px}
|
|
349
381
|
.vbar{flex:none;border-left:1px solid var(--line);background:var(--panel);display:flex;min-height:0;position:relative}
|
|
@@ -28,8 +28,11 @@ export const ICON = {
|
|
|
28
28
|
secrets: 'M7 7a3 3 0 1 0 0 6a3 3 0 1 0 0-6M10 10h7.5M15 10v2.5M17.5 10v2',
|
|
29
29
|
runs: 'M3 5h8M3 10h12M3 15h6',
|
|
30
30
|
asks: 'M3 4h14v9H8l-4 3.5V13H3z',
|
|
31
|
+
// pkm-multi's rail marks: rate_review, a bubble with a pen, and movie, a clapper
|
|
32
|
+
review: 'M3 3.5h14v10H7.5L3 17zM7 10.5l4.5-4.5 1.5 1.5L8.5 12H7z',
|
|
31
33
|
tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
|
|
32
34
|
media: 'M3 4h14v12H3zM3 13l4-4 4 4 2-2 4 4M13 7.5h.01',
|
|
35
|
+
trailer: 'M2.5 4.5h15v11h-15zM2.5 8.5h15M6 4.5 7.5 8.5M10 4.5l1.5 4M14 4.5l1.5 4',
|
|
33
36
|
wikis: 'M10 5.5C8.5 4 6 3.5 3 4v11c3-.5 5.5 0 7 1.5c1.5-1.5 4-2 7-1.5V4c-3-.5-5.5 0-7 1.5zM10 5.5v11',
|
|
34
37
|
admin: 'M4 5h12M4 10h12M4 15h12M7 3.5v3M13 8.5v3M9 13.5v3',
|
|
35
38
|
health: 'M2 10h3.5l2-5 5 10 2-5H18',
|
|
@@ -37,6 +40,12 @@ export const ICON = {
|
|
|
37
40
|
// the VS Code mark, its ribbon drawn as one outline
|
|
38
41
|
vscode: 'M14 2.5l3.5 1.7v11.6L14 17.5zM14 2.5 4.5 12.3 2.5 11V9l2-1.3L14 17.5M14 6.8 10.2 10l3.8 3.2',
|
|
39
42
|
sort: 'M6 3.5v13M3 13.5l3 3 3-3M14 16.5v-13M11 6.5l3-3 3 3',
|
|
43
|
+
// the theme button: a sun, a moon, and a half filled disc for the system's
|
|
44
|
+
sun: 'M10 6.5a3.5 3.5 0 1 0 .01 0M10 2v2M10 16v2M2 10h2M16 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M4.3 15.7l1.4-1.4M14.3 5.7l1.4-1.4',
|
|
45
|
+
moon: 'M15.5 12.5A6.5 6.5 0 0 1 7.5 4.5a6.5 6.5 0 1 0 8 8z',
|
|
46
|
+
auto: 'M10 3a7 7 0 1 0 .01 0M10 3v14M10 5.5h4M10 8.5h6M10 11.5h6M10 14.5h4',
|
|
47
|
+
// the project list's fold: a panel with its rail marked
|
|
48
|
+
rail: 'M3 4h14v12H3zM7.5 4v12',
|
|
40
49
|
// a process's own switches on the overview
|
|
41
50
|
play: 'M6.5 4.5v11l9-5.5z',
|
|
42
51
|
stop: 'M5.5 5.5h9v9h-9z',
|
|
@@ -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
|