rvlib-shipkit 1.1.0 → 1.2.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 +67 -0
- package/package.json +3 -1
- package/src/check.ts +24 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
- package/src/checks/SK023-stores-under-cap.check.ts +49 -0
- package/src/checks/SK024-identity-colors.check.ts +49 -0
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +18 -5
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli.ts +22 -2
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/attach.ts +14 -9
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +40 -18
- package/src/core/discover-checks.ts +36 -18
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/retention.ts +213 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +3 -0
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +86 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +208 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +141 -0
- package/src/home/resources.ts +338 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +149 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +172 -0
- package/src/home/supervise.ts +346 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widgets.ts +102 -0
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +5 -0
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/run-log.ts +58 -23
- package/src/ledger/store.ts +228 -103
- package/src/ledger/world-cache.ts +15 -13
- package/src/plan/cadence.ts +237 -0
- package/src/plan/planner.ts +303 -0
- package/src/plan/repo-source.ts +100 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/repo-config.ts +51 -2
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +1 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +1 -1
- package/src/serve/hub-app/header-bar.tsx +44 -12
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +241 -0
- package/src/serve/hub-app/home-strip.css +70 -0
- package/src/serve/hub-app/home-strip.tsx +245 -0
- package/src/serve/hub-app/home.html +17 -0
- package/src/serve/hub-app/hub-app.tsx +201 -93
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +43 -23
- package/src/serve/hub-app/icons.tsx +14 -0
- package/src/serve/hub-app/index.html +3 -1
- package/src/serve/hub-app/keys.ts +25 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +12 -9
- package/src/serve/hub-app/machine-st.ts +270 -0
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +88 -0
- package/src/serve/hub-app/planned-st.ts +171 -0
- package/src/serve/hub-app/planned-view.tsx +109 -0
- package/src/serve/hub-app/planned.css +39 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +97 -0
- package/src/serve/hub-app/project-list.tsx +167 -0
- package/src/serve/hub-app/projects-svc.ts +139 -17
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/{hub-st.ts → repo-st.ts} +155 -84
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +10 -10
- package/src/serve/hub-app/timeline-chart.tsx +24 -12
- package/src/serve/hub-app/timeline-svc.ts +5 -4
- package/src/serve/hub-app/timeline-view.tsx +13 -13
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +542 -0
- package/src/serve/project-gutter-route.ts +21 -0
- package/src/serve/project-gutter.ts +3 -2
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +142 -44
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +114 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
// THE page (Bun.serve HTML import — no vite, no build step)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// THE page (Bun.serve HTML import — no vite, no build step), one single page
|
|
2
|
+
// app for every page the home serves ('/', '/planned', '/r/<repo>/') and for a
|
|
3
|
+
// hub serving on its own at '/'. mobx+react per build/app-state-tree: HomeSt
|
|
4
|
+
// is the single root (window.app), each repo a RepoSt under it, services own
|
|
5
|
+
// state, components read and call. A repo's page is ONE tree of every repo
|
|
6
|
+
// resource, by path or by type, coloured by state in the label itself, over
|
|
7
|
+
// one severity filter persisted at ?f=needs · ?f=red. A row opens its item
|
|
8
|
+
// view, an asset row opens the atelier.
|
|
7
9
|
import { runInAction } from 'mobx'
|
|
8
10
|
import { observer } from 'mobx-react-lite'
|
|
9
|
-
import { StrictMode, useEffect } from 'react'
|
|
11
|
+
import { StrictMode, useEffect, useRef } from 'react'
|
|
10
12
|
import { createRoot } from 'react-dom/client'
|
|
11
13
|
import { ColCard, ColGrid, StatusCard } from './col-card.tsx'
|
|
12
14
|
import { WIKIS, WikisView } from './wikis-view.tsx'
|
|
13
15
|
import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
|
|
14
|
-
import {
|
|
16
|
+
import type { Filter, RepoSt } from './repo-st.ts'
|
|
17
|
+
import { HomeSt } from './home-st.ts'
|
|
15
18
|
import { TimelineView } from './timeline-view.tsx'
|
|
16
19
|
import { HeaderBar, LIVE_KEY, LiveStrip } from './header-bar.tsx'
|
|
17
20
|
import { FOLLOW_KEY } from './timeline-chart.tsx'
|
|
@@ -22,24 +25,32 @@ import { AssetStage, CatGrid, InfoChips, MediaTopbar, ToolsBar, VariantsBar } fr
|
|
|
22
25
|
import { TYPE_MARK } from './tree-build.ts'
|
|
23
26
|
import { TreeRowView } from './tree-row.tsx'
|
|
24
27
|
import { Help } from './help.tsx'
|
|
25
|
-
import {
|
|
28
|
+
import { ProjectList } from './project-list.tsx'
|
|
29
|
+
import { Group, Panel, Separator, type Layout } from 'react-resizable-panels'
|
|
30
|
+
import { LIST, SIDEBAR } from './layout-st.ts'
|
|
31
|
+
import { HomeStrip, NotUp, Overview } from './home-strip.tsx'
|
|
32
|
+
import { PlannedView } from './planned-view.tsx'
|
|
33
|
+
import { SCREEN_WAIT_MS } from './screen-cache.ts'
|
|
34
|
+
import { pageKey } from './keys.ts'
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
// home.html marks the home's page; index.html is a hub serving on its own
|
|
37
|
+
const home = new HomeSt({ home: document.body.dataset.home != null })
|
|
28
38
|
// the root as a global — app code, console debugging (build/app-state-tree)
|
|
29
|
-
;(window as unknown as { app:
|
|
30
|
-
|
|
39
|
+
;(window as unknown as { app: HomeSt }).app = home
|
|
40
|
+
home.boot()
|
|
31
41
|
|
|
32
42
|
// ── the tree: every resource of the repo, one row deep in its path ──────
|
|
33
43
|
// the row itself is tree-row.tsx, so a test renders it headless
|
|
34
44
|
|
|
35
|
-
const Tree = observer(() => {
|
|
45
|
+
const Tree = observer(({ app }: { app: RepoSt }) => {
|
|
36
46
|
const t = app.tree
|
|
37
47
|
return (
|
|
38
48
|
// roving focus: the arrows belong to the tree only while it HAS the
|
|
39
49
|
// focus, so the atelier keeps its own keys
|
|
40
50
|
<div className="tree" tabIndex={0} onKeyDown={(e) => { if (t.key(e.key)) e.preventDefault() }}>
|
|
41
51
|
{t.rows.map((r) => <TreeRowView key={r.key} app={app} row={r} />)}
|
|
42
|
-
{
|
|
52
|
+
{!app.cols.loaded && !app.cols.cached ? <div className="tempty twait"><span className="spin" />reading the repo…</div> : null}
|
|
53
|
+
{app.cols.loaded && t.rows.length === 0 ? <div className="tempty">{app.filter === 'red' ? 'no reds — nothing is blocked ✨' : app.filter === 'needs' ? 'nothing needs you ✨' : 'no resources yet'}</div> : null}
|
|
43
54
|
</div>
|
|
44
55
|
)
|
|
45
56
|
})
|
|
@@ -55,7 +66,7 @@ const TypeLegend = () => (
|
|
|
55
66
|
} />
|
|
56
67
|
)
|
|
57
68
|
|
|
58
|
-
const TreeTabs = observer(() => (
|
|
69
|
+
const TreeTabs = observer(({ app }: { app: RepoSt }) => (
|
|
59
70
|
<span className="seg">
|
|
60
71
|
{(['path', 'type'] as const).map((t) => (
|
|
61
72
|
<button key={t} className={app.tree.tab === t ? 'on' : ''} onClick={() => app.tree.setTab(t)}>by {t}</button>
|
|
@@ -68,7 +79,7 @@ const TreeTabs = observer(() => (
|
|
|
68
79
|
// the shortcut letter rides INSIDE its own button, so the binding is
|
|
69
80
|
// discoverable without a legend
|
|
70
81
|
const FILTER_KEY = { all: 'a', needs: 'n', red: 'r' } as const
|
|
71
|
-
const SegFilter = observer(() => (
|
|
82
|
+
const SegFilter = observer(({ app }: { app: RepoSt }) => (
|
|
72
83
|
<span className="seg">
|
|
73
84
|
{(['all', 'needs', 'red'] as const).map((f) => (
|
|
74
85
|
<button key={f} className={app.filter === f ? 'on' : ''} onClick={() => app.setFilter(f)}>
|
|
@@ -78,53 +89,56 @@ const SegFilter = observer(() => (
|
|
|
78
89
|
</span>
|
|
79
90
|
))
|
|
80
91
|
|
|
81
|
-
const Sidebar = observer(() => (
|
|
92
|
+
const Sidebar = observer(({ app }: { app: RepoSt }) => (
|
|
82
93
|
<aside>
|
|
83
94
|
<div className="shead">
|
|
84
95
|
resources
|
|
85
|
-
<SegFilter />
|
|
96
|
+
<SegFilter app={app} />
|
|
86
97
|
</div>
|
|
87
98
|
<div className="ssearch">
|
|
88
|
-
<input
|
|
99
|
+
<input ref={(el) => { app.searchEl = el }} placeholder="filter… (/ to focus, ⌘K to jump)" value={app.query}
|
|
89
100
|
onChange={(e) => app.setQuery(e.target.value)}
|
|
90
101
|
onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery('') }} />
|
|
91
102
|
</div>
|
|
92
103
|
<div className="stabs">
|
|
93
|
-
<TreeTabs />
|
|
104
|
+
<TreeTabs app={app} />
|
|
94
105
|
<span className="sp" />
|
|
95
106
|
<TypeLegend />
|
|
96
107
|
<span className="cnt">{app.tree.visible.length}</span>
|
|
97
108
|
</div>
|
|
98
|
-
{
|
|
109
|
+
{/* its line is held while the repo is read, so the tree never drops under it */}
|
|
110
|
+
{app.tree.whenLine === '' && app.cols.loaded ? null : <div className="swhen">{app.tree.whenLine === '' ? '\u00a0' : app.tree.whenLine}</div>}
|
|
99
111
|
{app.media.dead != null ? <div className="tempty"><span className="warnline">media API down: {app.media.dead}</span></div> : null}
|
|
100
|
-
<Tree />
|
|
112
|
+
<Tree app={app} />
|
|
101
113
|
</aside>
|
|
102
114
|
))
|
|
103
115
|
|
|
104
|
-
// ── cmd+
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
// ── the one picker, cmd+k: home and planned, the shown repo's targets, then
|
|
117
|
+
// every other repo ──────────────────────────────────────────────────────
|
|
118
|
+
const Palette = observer(({ app }: { app: HomeSt }) => {
|
|
119
|
+
const p = app.palette
|
|
120
|
+
if (!p.open) return null
|
|
107
121
|
return (
|
|
108
|
-
<div className="palov" onClick={() =>
|
|
122
|
+
<div className="palov" onClick={() => p.close()}>
|
|
109
123
|
<div className="pal" onClick={(e) => e.stopPropagation()}>
|
|
110
|
-
<input autoFocus placeholder="jump to…" value={
|
|
111
|
-
onChange={(e) =>
|
|
124
|
+
<input autoFocus placeholder="jump to…" value={p.query}
|
|
125
|
+
onChange={(e) => p.setQuery(e.target.value)}
|
|
112
126
|
onKeyDown={(e) => {
|
|
113
|
-
if (e.key === 'ArrowDown') { e.preventDefault();
|
|
114
|
-
else if (e.key === 'ArrowUp') { e.preventDefault();
|
|
115
|
-
else if (e.key === 'Enter')
|
|
116
|
-
else if (e.key === 'Escape')
|
|
127
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); p.move(1) }
|
|
128
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); p.move(-1) }
|
|
129
|
+
else if (e.key === 'Enter') p.run()
|
|
130
|
+
else if (e.key === 'Escape') p.close()
|
|
117
131
|
}} />
|
|
118
132
|
<div className="prows">
|
|
119
|
-
{
|
|
120
|
-
<div key={jp.id} className={`prow${n ===
|
|
121
|
-
onMouseEnter={() =>
|
|
122
|
-
onClick={() =>
|
|
133
|
+
{p.results.map((jp, n) => (
|
|
134
|
+
<div key={jp.id} className={`prow${n === p.idx ? ' on' : ''}`}
|
|
135
|
+
onMouseEnter={() => p.setIdx(n)}
|
|
136
|
+
onClick={() => p.run(jp)}>
|
|
123
137
|
<span className="plabel">{jp.label}</span>
|
|
124
138
|
<span className="pchip">{jp.section}</span>
|
|
125
139
|
</div>
|
|
126
140
|
))}
|
|
127
|
-
{
|
|
141
|
+
{p.results.length === 0 ? <div className="prow none">no match</div> : null}
|
|
128
142
|
</div>
|
|
129
143
|
</div>
|
|
130
144
|
</div>
|
|
@@ -133,7 +147,7 @@ const Palette = observer(() => {
|
|
|
133
147
|
|
|
134
148
|
// remark box on the ASSET view too: remarks land in .shipkit/assets/notes.md
|
|
135
149
|
// and come back as `remarks` (exchange both ways, media included)
|
|
136
|
-
const AssetRemarks = observer(() => {
|
|
150
|
+
const AssetRemarks = observer(({ app }: { app: RepoSt }) => {
|
|
137
151
|
const m = app.media
|
|
138
152
|
const c = app.cols
|
|
139
153
|
if (m.curId == null) return null
|
|
@@ -151,7 +165,7 @@ const AssetRemarks = observer(() => {
|
|
|
151
165
|
)
|
|
152
166
|
})
|
|
153
167
|
|
|
154
|
-
const Main = observer(() => {
|
|
168
|
+
const Main = observer(({ app }: { app: RepoSt }) => {
|
|
155
169
|
const m = app.media
|
|
156
170
|
const errs = m.errors.length > 0 ? <div className="errs">{m.errors.join('\n')}</div> : null
|
|
157
171
|
if (app.sel?.t === 'item') return (
|
|
@@ -184,7 +198,7 @@ const Main = observer(() => {
|
|
|
184
198
|
<AssetStage app={app} />
|
|
185
199
|
<VariantsBar app={app} />
|
|
186
200
|
</div>
|
|
187
|
-
<AssetRemarks />
|
|
201
|
+
<AssetRemarks app={app} />
|
|
188
202
|
<div className="help"><kbd>←</kbd><kbd>→</kbd> roll · <kbd>Enter</kbd> pick · <kbd>⇧A</kbd> alt · <kbd>X</kbd> discard — picks ship via the kit's ship hook when present · category title = grid view</div>
|
|
189
203
|
</main>
|
|
190
204
|
)
|
|
@@ -203,68 +217,162 @@ const Main = observer(() => {
|
|
|
203
217
|
|
|
204
218
|
const FILTER_BY_KEY: Record<string, Filter | undefined> = { a: 'all', n: 'needs', r: 'red' }
|
|
205
219
|
|
|
220
|
+
/** a panel never scrolls itself: its content scrolls where it means to (the
|
|
221
|
+
* list's rows, the tree, the page), and a panel's own scrollbar is a dark
|
|
222
|
+
* band in WebKit, the macOS app's engine, which draws classic scrollbars.
|
|
223
|
+
* overflow is the one inline style the library lets a panel set */
|
|
224
|
+
const PANEL = { overflow: 'hidden' } as const
|
|
225
|
+
|
|
226
|
+
/** a drag of a separator ended: the width of the panel on its left, in px,
|
|
227
|
+
* from the group's layout (a percentage of the group's width) */
|
|
228
|
+
const leftPx = (el: HTMLElement | null, layout: Layout, id: string): number | null => {
|
|
229
|
+
const pct = layout[id]
|
|
230
|
+
const w = el?.getBoundingClientRect().width
|
|
231
|
+
return pct == null || w == null || w === 0 ? null : (pct / 100) * w
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** a repo's sidebar and its main area, the sidebar's width dragged by the
|
|
235
|
+
* separator between them and kept per browser */
|
|
236
|
+
const Body = observer(({ app }: { app: RepoSt }) => {
|
|
237
|
+
const l = app.home.layout
|
|
238
|
+
const el = useRef<HTMLDivElement | null>(null)
|
|
239
|
+
return (
|
|
240
|
+
<Group className="appbody" orientation="horizontal" elementRef={el}
|
|
241
|
+
onLayoutChanged={(layout, meta) => {
|
|
242
|
+
if (!meta.isUserInteraction) return
|
|
243
|
+
const px = leftPx(el.current, layout, 'sidebar')
|
|
244
|
+
if (px != null) l.setSidebarWidth(px)
|
|
245
|
+
}}>
|
|
246
|
+
<Panel id="sidebar" defaultSize={l.sidebarWidth} minSize={SIDEBAR.min} maxSize={SIDEBAR.max} groupResizeBehavior="preserve-pixel-size" className="pside" style={PANEL}>
|
|
247
|
+
<Sidebar app={app} />
|
|
248
|
+
</Panel>
|
|
249
|
+
<Separator className="rsep" />
|
|
250
|
+
<Panel id="main" minSize={320} className="pmain" style={PANEL}>
|
|
251
|
+
<Main app={app} />
|
|
252
|
+
</Panel>
|
|
253
|
+
</Group>
|
|
254
|
+
)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
/** the repo's hub has not answered on the home: its reason and its fix show
|
|
258
|
+
* instead of the page. A screen this browser kept shows meanwhile, until
|
|
259
|
+
* the machine says the hub is not up */
|
|
260
|
+
const notUp = (app: RepoSt): boolean => {
|
|
261
|
+
const m = app.home.machine
|
|
262
|
+
if (m == null || app.started) return false
|
|
263
|
+
if (!app.cols.cached) return true
|
|
264
|
+
return m.fresh && m.worker(app.key)?.status !== 'up'
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** one repo's page. Every repo visited keeps its region mounted, hidden while
|
|
268
|
+
* another one shows, so its scroll, its frames and its component state are
|
|
269
|
+
* there when it comes back; a hidden repo polls nothing, so it repaints
|
|
270
|
+
* nothing */
|
|
271
|
+
const RepoView = observer(({ app }: { app: RepoSt }) => {
|
|
272
|
+
const on = app.isShown
|
|
273
|
+
return (
|
|
274
|
+
<div className="repo" hidden={!on}>
|
|
275
|
+
{notUp(app) ? <NotUp app={app.home} /> : (
|
|
276
|
+
<>
|
|
277
|
+
<HeaderBar app={app} />
|
|
278
|
+
<LiveStrip app={app} />
|
|
279
|
+
<Body app={app} />
|
|
280
|
+
{on ? <ComfyOverlay app={app} /> : null}
|
|
281
|
+
</>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
/** the keys of the page: they act on the repo shown, and on nothing else */
|
|
288
|
+
function onKey(e: KeyboardEvent): void {
|
|
289
|
+
// the command keys beat the focus check: they work from anywhere, any page
|
|
290
|
+
const pk = pageKey(e)
|
|
291
|
+
if (pk != null) {
|
|
292
|
+
e.preventDefault()
|
|
293
|
+
if (pk.t === 'picker') home.palette.toggle()
|
|
294
|
+
else if (pk.t === 'home' || pk.t === 'planned') {
|
|
295
|
+
home.palette.close()
|
|
296
|
+
if (home.machine != null) home.navigate(pk.t === 'home' ? '/' : '/planned')
|
|
297
|
+
} else {
|
|
298
|
+
const p = home.projects.list[pk.n - 1]
|
|
299
|
+
home.palette.close()
|
|
300
|
+
if (p != null) home.projects.click(p.name)
|
|
301
|
+
}
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
if (home.palette.open) return // the picker's input owns its keys
|
|
305
|
+
const app = home.shown
|
|
306
|
+
if (app == null) return
|
|
307
|
+
const t = e.target as HTMLElement
|
|
308
|
+
if (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') return
|
|
309
|
+
// the atelier owns its roll keys while an asset is open, and
|
|
310
|
+
// says so by consuming them
|
|
311
|
+
if (app.sel?.t === 'asset' && app.media.key(e.key)) {
|
|
312
|
+
e.preventDefault()
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if (e.key === '/') {
|
|
316
|
+
e.preventDefault()
|
|
317
|
+
app.searchEl?.focus()
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
321
|
+
if (e.key === LIVE_KEY) {
|
|
322
|
+
e.preventDefault()
|
|
323
|
+
app.live.toggle()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
if (e.key === FOLLOW_KEY && !app.live.chart.follow) {
|
|
327
|
+
e.preventDefault()
|
|
328
|
+
app.live.chart.attach()
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
const f = FILTER_BY_KEY[e.key.toLowerCase()]
|
|
332
|
+
if (f != null) {
|
|
333
|
+
e.preventDefault()
|
|
334
|
+
app.setFilter(f)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
206
338
|
const App = observer(() => {
|
|
207
339
|
useEffect(() => {
|
|
208
|
-
const onKey = (e: KeyboardEvent): void => {
|
|
209
|
-
// ⌘J/^J beats the focus check — the palette must open from anywhere
|
|
210
|
-
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'j') {
|
|
211
|
-
e.preventDefault()
|
|
212
|
-
app.palToggle()
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
const t = e.target as HTMLElement
|
|
216
|
-
if (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') return
|
|
217
|
-
if (app.palOpen) return // the palette input owns its keys
|
|
218
|
-
// the atelier owns its roll keys while an asset is open, and
|
|
219
|
-
// says so by consuming them
|
|
220
|
-
if (app.sel?.t === 'asset' && app.media.key(e.key)) {
|
|
221
|
-
e.preventDefault()
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
if (e.key === '/') {
|
|
225
|
-
e.preventDefault()
|
|
226
|
-
document.getElementById('treeq')?.focus()
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
230
|
-
if (e.key === LIVE_KEY) {
|
|
231
|
-
e.preventDefault()
|
|
232
|
-
app.live.toggle()
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
if (e.key === FOLLOW_KEY && !app.live.chart.follow) {
|
|
236
|
-
e.preventDefault()
|
|
237
|
-
app.live.chart.attach()
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
const f = FILTER_BY_KEY[e.key.toLowerCase()]
|
|
241
|
-
if (f != null) {
|
|
242
|
-
e.preventDefault()
|
|
243
|
-
app.setFilter(f)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
340
|
document.addEventListener('keydown', onKey)
|
|
247
341
|
return () => document.removeEventListener('keydown', onKey)
|
|
248
342
|
}, [])
|
|
343
|
+
const l = home.layout
|
|
344
|
+
const el = useRef<HTMLDivElement | null>(null)
|
|
249
345
|
return (
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
346
|
+
// two panels: the project list, its width dragged by the separator
|
|
347
|
+
// and kept per browser, then the page. The list and the strip stay
|
|
348
|
+
// mounted across pages; only the region under the strip changes
|
|
349
|
+
<>
|
|
350
|
+
<Group className="app" orientation="horizontal" elementRef={el}
|
|
351
|
+
onLayoutChanged={(layout, meta) => {
|
|
352
|
+
if (!meta.isUserInteraction) return
|
|
353
|
+
const px = leftPx(el.current, layout, 'projects')
|
|
354
|
+
if (px != null) l.setListWidth(px)
|
|
355
|
+
}}>
|
|
356
|
+
<Panel id="projects" defaultSize={l.listWidth} minSize={LIST.min} maxSize={LIST.max} groupResizeBehavior="preserve-pixel-size" className="pjpanel" style={PANEL}>
|
|
357
|
+
<ProjectList app={home} />
|
|
358
|
+
</Panel>
|
|
359
|
+
<Separator className="rsep" />
|
|
360
|
+
<Panel id="page" minSize={320} className="appcol" style={PANEL}>
|
|
361
|
+
{home.machine == null ? null : <HomeStrip app={home} />}
|
|
362
|
+
{home.page === 'overview' ? <Overview app={home} /> : null}
|
|
363
|
+
{home.page === 'planned' ? <PlannedView app={home} /> : null}
|
|
364
|
+
{home.visited.map((r) => <RepoView key={r.key} app={r} />)}
|
|
365
|
+
</Panel>
|
|
366
|
+
</Group>
|
|
367
|
+
<Palette app={home} />
|
|
368
|
+
</>
|
|
265
369
|
)
|
|
266
370
|
})
|
|
267
371
|
|
|
372
|
+
// the shown repo's kept screen is read before the first paint, so a refresh
|
|
373
|
+
// paints it at once; a slow or absent store costs at most SCREEN_WAIT_MS
|
|
374
|
+
await Promise.race([home.shown?.cols.restore(), new Promise((ok) => setTimeout(ok, SCREEN_WAIT_MS))])
|
|
375
|
+
|
|
268
376
|
createRoot(document.getElementById('root')!).render(
|
|
269
377
|
<StrictMode>
|
|
270
378
|
<App />
|
|
@@ -126,9 +126,6 @@ export type CheckFacts = {
|
|
|
126
126
|
* (serve/hub-problems.ts): the header shows it, never the tree */
|
|
127
127
|
export type HubProblem = { id: string; text: string; severity: 'red' | 'yellow' }
|
|
128
128
|
|
|
129
|
-
/** the hub's daily health pass timer, as the resources feed sends it
|
|
130
|
-
* (serve/health-schedule.ts). null = this hub runs no timer */
|
|
131
|
-
export type Schedule = { everyMs: number; lastPassAt: number | null; nextAt: number; running: boolean }
|
|
132
129
|
|
|
133
130
|
/** one declared slow suite, with the case test-speed doctrine demands */
|
|
134
131
|
export type SlowSuite = { file: string; catches: string; whyNotFast: string }
|
|
@@ -21,18 +21,34 @@
|
|
|
21
21
|
--s0:2px;--s1:4px;--s2:8px;--s3:12px;--s4:16px}
|
|
22
22
|
*{box-sizing:border-box}html,body,#root{height:100%}
|
|
23
23
|
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
|
|
24
|
-
/* the page is
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.appcol{
|
|
29
|
-
.
|
|
24
|
+
/* the page is two panels (hub-app.tsx): the project list, then .appcol,
|
|
25
|
+
which stacks the strip, the header, the live strip and the body */
|
|
26
|
+
.app{height:100vh}
|
|
27
|
+
/* a panel's class lands on the div inside the panel (react-resizable-panels) */
|
|
28
|
+
.pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
|
|
29
|
+
.pjpanel{display:flex}
|
|
30
|
+
.appcol{display:flex;flex-direction:column}
|
|
31
|
+
/* one region per repo visited, the shown one alone displayed */
|
|
32
|
+
.repo{flex:1;min-height:0;display:flex;flex-direction:column}
|
|
33
|
+
.repo[hidden]{display:none}
|
|
34
|
+
.appbody{flex:1;min-height:0}
|
|
35
|
+
.pside{display:flex;flex-direction:column}
|
|
36
|
+
.pside > aside{flex:1}
|
|
37
|
+
.pmain{display:flex;flex-direction:column}
|
|
38
|
+
.pmain > main{flex:1}
|
|
30
39
|
|
|
31
40
|
/* ── the header: repo name, the core pages, the two runs and the run state ── */
|
|
41
|
+
/* the repo's processes (serve.processes): a dot, its id, a link once up */
|
|
42
|
+
.hprocs{display:inline-flex;gap:var(--s1);align-items:center}
|
|
43
|
+
.hproc{display:inline-flex;align-items:center;gap:var(--s0)}
|
|
44
|
+
.hproc button{padding:var(--s0) var(--s2);font-size:12px}
|
|
45
|
+
.hproc a{color:var(--accent);text-decoration:none;font-size:12px}
|
|
46
|
+
.pdot{display:inline-block;width:7px;height:7px;border-radius:4px;margin-right:var(--s1);background:var(--line)}
|
|
47
|
+
.pdot.up{background:var(--ok)}.pdot.starting,.pdot.stopping{background:var(--warn)}.pdot.crashed{background:var(--red)}
|
|
32
48
|
.hdr{display:flex;gap:var(--s2);align-items:center;height:36px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--panel)}
|
|
33
49
|
.hdr .sp{flex:1}
|
|
34
50
|
.hname{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);font-weight:600;margin-right:var(--s2);white-space:nowrap}
|
|
35
|
-
.hnav{display:flex;gap:var(--s1);overflow:hidden}
|
|
51
|
+
.hnav{display:flex;gap:var(--s1);overflow:hidden;align-items:center}
|
|
36
52
|
.hnav button{border-color:transparent;color:var(--dim);border-radius:0;border-bottom:1px solid transparent;white-space:nowrap}
|
|
37
53
|
.hnav button:hover{color:var(--ink);border-color:transparent;border-bottom-color:var(--line)}
|
|
38
54
|
.hnav button.on{color:var(--accent);border-bottom-color:var(--accent)}
|
|
@@ -40,17 +56,17 @@ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans
|
|
|
40
56
|
.hstatus{max-width:420px;flex:0 1 auto;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
41
57
|
.hstatus.red{color:var(--red)}
|
|
42
58
|
.hstatus.warn{color:var(--warn)}
|
|
43
|
-
.hstate{display:inline-flex;gap:var(--s1);align-items:center;width:260px;min-width:260px;flex:none;justify-content:flex-
|
|
59
|
+
.hstate{display:inline-flex;gap:var(--s1);align-items:center;width:260px;min-width:260px;flex:none;justify-content:flex-start;font-size:11.5px;color:var(--dim);overflow:hidden}
|
|
44
60
|
.hstate.on{color:var(--ink)}
|
|
45
61
|
.hstate .htext{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
62
|
+
.hstate .spin.idle,.hnav .spin.idle{visibility:hidden;animation:none}
|
|
46
63
|
|
|
47
64
|
/* ── the live strip: the one run chart. Folded, one thin row of ONE height
|
|
48
65
|
whatever runs; unfolded, the chart at the height its lanes hold, set
|
|
49
66
|
inline from a held value that grows at once and shrinks only after a
|
|
50
|
-
while, so it never jumps every second.
|
|
51
|
-
|
|
67
|
+
while, so it never jumps every second. Its (?) and the chart tooltip float
|
|
68
|
+
in a portal (floating.tsx), so no box of the strip clips them ── */
|
|
52
69
|
.live{display:flex;flex-direction:column;height:30px;flex:none;border-bottom:1px solid var(--line);background:var(--bg);font-size:11px;color:var(--dim)}
|
|
53
|
-
.live .hbub{top:calc(100% + 6px);bottom:auto;z-index:30}
|
|
54
70
|
.lrow{display:flex;gap:var(--s2);align-items:center;height:29px;flex:none;padding:0 var(--s3)}
|
|
55
71
|
.lrow .sp{flex:1}
|
|
56
72
|
.lfold{display:inline-flex;gap:var(--s1);align-items:center;border-color:transparent;color:var(--dim);padding:var(--s0) var(--s1)}
|
|
@@ -135,10 +151,11 @@ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto
|
|
|
135
151
|
.tnote b{font-weight:600;color:var(--red)}
|
|
136
152
|
.stabs{display:flex;gap:var(--s2);align-items:center;padding:var(--s1) var(--s3);border-bottom:1px solid var(--line)}
|
|
137
153
|
.stabs .sp{flex:1}
|
|
138
|
-
.stabs .cnt{font-size:11px;color:var(--dim)}
|
|
154
|
+
.stabs .cnt{min-width:4ch;text-align:right;font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums}
|
|
139
155
|
.catgen{display:inline-flex;gap:var(--s1)}
|
|
140
156
|
.catgen button{padding:var(--s0) var(--s2);font-size:11px;font-weight:700}
|
|
141
157
|
.tempty{padding:var(--s2) var(--s3);font-size:12px;color:var(--dim);white-space:pre-wrap}
|
|
158
|
+
.twait{display:flex;align-items:center;gap:var(--s2)}
|
|
142
159
|
.warnline{color:var(--red)}
|
|
143
160
|
.fold{cursor:pointer;color:var(--dim);font-size:15px;display:inline-block;width:22px;text-align:center;
|
|
144
161
|
padding:var(--s0);border-radius:3px;user-select:none}
|
|
@@ -388,9 +405,9 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
|
|
|
388
405
|
.tlgap.live text{fill:var(--ink)}
|
|
389
406
|
.tlgap text{font-size:10px;pointer-events:none}
|
|
390
407
|
.tlnow{stroke:var(--accent)}
|
|
391
|
-
/*
|
|
392
|
-
clips its overflow, and a tooltip inside them is cut to a sliver */
|
|
393
|
-
.tlaxtip{
|
|
408
|
+
/* placed by floating-ui under the pointer, in a portal: every box of the
|
|
409
|
+
strip clips its overflow, and a tooltip inside them is cut to a sliver */
|
|
410
|
+
.tlaxtip{z-index:30;display:flex;flex-direction:column;gap:0;padding:var(--s1) var(--s2);pointer-events:none;
|
|
394
411
|
background:var(--panel);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 12px var(--shadow);font-size:11px;white-space:nowrap}
|
|
395
412
|
.tlaxtip .dim{color:var(--dim)}
|
|
396
413
|
/* the runs page: one tab bar, then one list. Every line on a grid of fixed
|
|
@@ -542,17 +559,18 @@ a.tok{color:var(--accent)}
|
|
|
542
559
|
height, so a streaming run never jumps the card it sits in */
|
|
543
560
|
.wlog{margin:0;min-height:120px;max-height:320px;overflow-y:auto;background:var(--panel2);border:1px solid var(--line);
|
|
544
561
|
border-radius:3px;padding:var(--s2);font:11.5px/1.5 ui-monospace,Menlo,monospace;color:var(--ink);white-space:pre-wrap}
|
|
545
|
-
/* THE HELP: a `(?)` that says its one sentence
|
|
546
|
-
|
|
562
|
+
/* THE HELP: a `(?)` that says its one sentence on hover and on focus, the
|
|
563
|
+
bubble placed by floating-ui (floating.tsx) */
|
|
547
564
|
.hlp{display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;flex:none;
|
|
548
565
|
border:1px solid var(--line);border-radius:50%;color:var(--dim);font-size:9px;line-height:1;cursor:help;
|
|
549
|
-
|
|
566
|
+
outline:none;user-select:none}
|
|
550
567
|
.hlp:hover,.hlp:focus{color:var(--accent);border-color:var(--accent)}
|
|
551
|
-
|
|
568
|
+
/* out of sight, still read by a screen reader and found by a page search */
|
|
569
|
+
.vh{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
|
|
570
|
+
.hbub,.ftip{z-index:40;width:max-content;
|
|
552
571
|
max-width:260px;background:var(--panel2);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 10px var(--shadow);
|
|
553
572
|
padding:var(--s1) var(--s2);font:11.5px/1.4 system-ui,sans-serif;text-transform:none;letter-spacing:0;
|
|
554
|
-
color:var(--ink);white-space:normal;
|
|
555
|
-
.hlp:hover .hbub,.hlp:focus .hbub{display:block}
|
|
573
|
+
color:var(--ink);white-space:normal;pointer-events:none}
|
|
556
574
|
/* the type legend: every glyph of the tree with its word, in one bubble */
|
|
557
575
|
.legend{display:flex;flex-direction:column;gap:var(--s1)}
|
|
558
576
|
.legend .lrow{display:flex;gap:var(--s2);align-items:baseline;white-space:normal}
|
|
@@ -596,7 +614,9 @@ a.tok{color:var(--accent)}
|
|
|
596
614
|
.graphview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
|
|
597
615
|
.gvhead{display:flex;gap:var(--s2);align-items:center;font-size:13px;min-width:0}
|
|
598
616
|
.gvhead .gvtext{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
599
|
-
.
|
|
617
|
+
.gvframe{flex:1;min-height:0;position:relative;display:flex;border:1px solid var(--line);border-radius:3px;background:var(--bg)}
|
|
618
|
+
.gvwait{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:var(--s2);font-size:12px;color:var(--dim)}
|
|
619
|
+
.graphview iframe{position:relative;flex:1;min-height:0;width:100%;border:0;background:transparent}
|
|
600
620
|
|
|
601
621
|
/* ── comfy item view (comfy-ts's own panel fills the stage) ── */
|
|
602
622
|
.comfyview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
|
|
@@ -646,7 +666,7 @@ a.mint{color:var(--accent);font-size:12px;text-decoration:none}
|
|
|
646
666
|
a.mint:hover{text-decoration:underline}
|
|
647
667
|
.colcard h3 .stchip,.colcard h3 a.mint{margin-left:var(--s2)}
|
|
648
668
|
|
|
649
|
-
/* ── cmd+
|
|
669
|
+
/* ── the cmd+k picker ── */
|
|
650
670
|
.palov{position:fixed;inset:0;background:var(--scrim);z-index:100;display:flex;justify-content:center;align-items:flex-start;
|
|
651
671
|
padding-top:14vh}
|
|
652
672
|
.pal{width:min(560px,90vw);background:var(--panel);border:1px solid var(--line);border-radius:3px;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// the few line icons of the home (the overview's section headers, the two
|
|
2
|
+
// places on top of the project list): one 20 unit path each, stroked in the text colour, so
|
|
3
|
+
// the page's tokens colour them and nothing else does.
|
|
4
|
+
export const ICON = {
|
|
5
|
+
shipkit: 'M3 7.5 10 4l7 3.5v5L10 16l-7-3.5zM3 7.5 10 11l7-3.5M10 11v5',
|
|
6
|
+
process: 'M2 10h3.5l2-5 5 10 2-5H18',
|
|
7
|
+
monitor: 'M3 4h14v9H3zM7 16.5h6M10 13v3.5',
|
|
8
|
+
home: 'M3 9.5 10 3.5l7 6M5 8v8.5h10V8M8.5 16.5v-4.5h3v4.5',
|
|
9
|
+
planned: 'M5.5 3h9M5.5 17h9M6.5 3c0 4 7 4 7 7s-7 3-7 7M13.5 3c0 4-7 4-7 7s7 3 7 7',
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
export const Icon = ({ d }: { d: string }) => (
|
|
13
|
+
<svg className="hico" viewBox="0 0 20 20" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d={d} /></svg>
|
|
14
|
+
)
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>hub</title>
|
|
7
7
|
<link rel="stylesheet" href="./hub.css" />
|
|
8
|
-
<link rel="stylesheet" href="./project-
|
|
8
|
+
<link rel="stylesheet" href="./project-list.css" />
|
|
9
9
|
<link rel="stylesheet" href="./prose-hits.css" />
|
|
10
|
+
<link rel="stylesheet" href="./home-strip.css" />
|
|
11
|
+
<link rel="stylesheet" href="./planned.css" />
|
|
10
12
|
</head>
|
|
11
13
|
<body>
|
|
12
14
|
<div id="root"></div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// the page's command keys, read the same on every layout: ⌘1 to ⌘9 select the
|
|
2
|
+
// nth repo of the project list, ⌘J shows the overview, ⌘P the plan, ⌘K opens
|
|
3
|
+
// the picker.
|
|
4
|
+
// A digit is read from the physical key (`code`), so an AZERTY keyboard,
|
|
5
|
+
// where the key under 1 types `&`, selects the same repo. Ctrl stands in for
|
|
6
|
+
// ⌘ off a Mac. The macOS app binds none of these to a menu item, so they
|
|
7
|
+
// reach the page (tests/home-strip.test.tsx reads ShipkitGui.swift).
|
|
8
|
+
export type PageKey = { t: 'repo'; n: number } | { t: 'home' } | { t: 'planned' } | { t: 'picker' }
|
|
9
|
+
|
|
10
|
+
type KeyLike = { key: string; code: string; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean }
|
|
11
|
+
|
|
12
|
+
export function pageKey(e: KeyLike): PageKey | null {
|
|
13
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey) return null
|
|
14
|
+
const digit = /^Digit([1-9])$/.exec(e.code)
|
|
15
|
+
if (digit != null) return { t: 'repo', n: Number(digit[1]) }
|
|
16
|
+
if (e.shiftKey) return null
|
|
17
|
+
const k = e.key.toLowerCase()
|
|
18
|
+
if (k === 'j') return { t: 'home' }
|
|
19
|
+
if (k === 'p') return { t: 'planned' }
|
|
20
|
+
if (k === 'k') return { t: 'picker' }
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** the shortcut each place of the home carries on its button */
|
|
25
|
+
export const PLACE_KEY = { overview: '⌘J', planned: '⌘P' } as const
|