rvlib-shipkit 2.1.1 → 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 +50 -0
- package/package.json +2 -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 +7 -3
- package/src/assets/run-assets.ts +18 -4
- package/src/checks/SK004-gitignore-policy.check.ts +11 -6
- package/src/checks/SK013-one-way-in-policy.check.ts +13 -8
- package/src/cli/cmd_Calls.ts +53 -0
- package/src/cli/cmd_Commands.ts +163 -43
- package/src/cli/cmd_Review.ts +948 -0
- package/src/cli/cmd_Shot.ts +63 -0
- package/src/cli/cmd_Start.ts +8 -4
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +361 -0
- package/src/cli/kit/builtins.ts +40 -0
- package/src/cli/kit/calls.ts +177 -0
- package/src/cli/kit/families.ts +67 -0
- package/src/cli/kit/index.ts +8 -0
- package/src/cli/kit/pages-check.ts +36 -0
- package/src/cli/kit/project-cli.ts +73 -0
- package/src/cli/kit/verb.ts +167 -0
- package/src/cli.ts +15 -2
- 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 +12 -2
- package/src/core/migrate-layout.ts +31 -8
- package/src/core/one-way-in-policy.ts +12 -4
- package/src/reflect/prompt-corpus.ts +15 -114
- 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/scaffold/cli-skeleton.ts +149 -0
- package/src/scaffold/files/agent-index.md +2 -2
- package/src/scaffold/files/tsconfig.json +3 -0
- package/src/scaffold/files/verify-skill.md +1 -1
- package/src/scaffold/project-scaffold.ts +90 -12
- 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 +12 -4
- package/src/shot/client.ts +67 -0
- package/src/shot/clip-encoder.ts +146 -0
- package/src/shot/dom-clip.ts +124 -0
- package/src/shot/serve-shot.ts +54 -0
- package/src/shot/shooter.ts +343 -0
- package/src/shot/shot-ask.ts +189 -0
- package/src/shot/shot.shipkit.ts +14 -0
- package/src/smoke/run-smoke.ts +2 -2
- package/src/start-repo.ts +29 -14
- package/src/suite/judge-suites.ts +56 -10
- package/src/suite/select.ts +2 -5
- package/src/suite/suite-cli.ts +3 -2
- package/src/trailer/adapter.ts +78 -0
- package/src/trailer/build.ts +603 -0
- package/src/trailer/ffmpeg.ts +72 -0
- package/src/trailer/footage.ts +173 -0
- package/src/trailer/grammar.ts +1290 -0
- package/src/trailer/graph.ts +508 -0
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +31 -0
- package/src/trailer/look.ts +124 -0
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +192 -0
- 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 +233 -0
- package/src/trailer/titles.ts +379 -0
- package/src/trailer/url.ts +108 -0
- package/src/trailer/vertical-draw.ts +286 -0
- package/src/trailer/vertical.ts +148 -0
- package/src/trailer/vocab.ts +59 -0
- package/src/trailer/words.ts +254 -0
|
@@ -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
|
|
@@ -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: 18, m: 24, l: 32 } 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 = 13
|
|
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)
|
|
@@ -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 */
|
|
@@ -119,3 +122,18 @@
|
|
|
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,18px) + 10px)}
|
|
132
|
+
.pjlist.rail .pjrow img,.pjlist.rail .pjrow .pgi{width:var(--ri,18px);height:var(--ri,18px)}
|
|
133
|
+
.pjlist.rail .pjrow .pgi{font-size:calc(var(--ri,18px) * .55)}
|
|
134
|
+
.pjsize{font:600 10px/1 ui-monospace,Menlo,monospace}
|
|
135
|
+
.pjlist.rail .pga{position:absolute;top:var(--s0);left:50%;margin-left:var(--s1);min-width:14px;height:14px;padding:0 var(--s0);border-radius:7px;font-size:9px;line-height:14px}
|
|
136
|
+
.pjlist.rail .pgd,.pjlist.rail .pgw{position:absolute;bottom:var(--s1);left:50%;margin-left:var(--s1)}
|
|
137
|
+
.pjlist.rail .pjfoot{flex-direction:column;height:auto;padding:var(--s1) 0}
|
|
138
|
+
.pjlist.rail .pjfoot button{justify-content:center;width:28px;padding:0}
|
|
139
|
+
.pjlist.rail .pjarbtn span{display:none}
|