rvlib-shipkit 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +15 -10
- package/src/check.ts +26 -10
- package/src/checks/SK001-package-json-shape.check.ts +34 -5
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +103 -61
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK014-prose-ste.check.ts +16 -6
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +50 -0
- package/src/checks/SK028-readme.check.ts +83 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +119 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/collection-types.ts +15 -0
- package/src/collections/providers.ts +7 -3
- package/src/collections/test-time.ts +10 -1
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +66 -17
- package/src/core/discover-checks.ts +10 -5
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +1 -1
- package/src/core/layout.ts +2 -0
- package/src/core/motherhome.ts +88 -66
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +8 -7
- package/src/core/run-ci.ts +46 -15
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +49 -9
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +138 -0
- package/src/home/activity.ts +256 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +10 -2
- package/src/home/home-routes.ts +34 -6
- package/src/home/home-start.ts +33 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +40 -7
- package/src/home/process-rows.ts +222 -0
- package/src/home/resources.ts +99 -20
- package/src/home/run-daemon.ts +12 -7
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +32 -5
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +20 -6
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +30 -2
- package/src/ledger/world-cache.ts +63 -14
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +17 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-words.ts +2 -0
- package/src/serve/hub-app/checks-svc.ts +2 -1
- package/src/serve/hub-app/col-tables-view.tsx +124 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +28 -0
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +50 -13
- package/src/serve/hub-app/home-strip.css +81 -23
- package/src/serve/hub-app/home-strip.tsx +243 -102
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +103 -161
- package/src/serve/hub-app/hub-types.ts +6 -3
- package/src/serve/hub-app/hub.css +101 -39
- package/src/serve/hub-app/icons.tsx +27 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +44 -9
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +65 -13
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +13 -9
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +26 -7
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +29 -9
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +34 -13
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/serve/warm-cell.ts +49 -16
- package/src/shipkit-files.ts +20 -4
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// A repo's admin page, as state: what administers the repo rather than works
|
|
2
|
+
// in it. Archive asks first (the confirmation is this state, so it survives a
|
|
3
|
+
// re-render and a test drives it headless), reload and keep active go to the
|
|
4
|
+
// machine by the repo's key. On a hub of its own there is no machine, and no
|
|
5
|
+
// admin page.
|
|
6
|
+
import { makeAutoObservable } from 'mobx'
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
|
+
|
|
9
|
+
export class AdminSvc {
|
|
10
|
+
/** the archive button was pressed: its confirmation shows in its place */
|
|
11
|
+
confirming = false
|
|
12
|
+
|
|
13
|
+
constructor(private st: RepoSt) {
|
|
14
|
+
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
askArchive(): void {
|
|
18
|
+
this.confirming = true
|
|
19
|
+
}
|
|
20
|
+
cancel(): void {
|
|
21
|
+
this.confirming = false
|
|
22
|
+
}
|
|
23
|
+
/** archived, the repo leaves the project list and its page shows the
|
|
24
|
+
* archived note with its way back */
|
|
25
|
+
archive(): void {
|
|
26
|
+
this.confirming = false
|
|
27
|
+
void this.st.home.projects.setArchived(this.st.key, true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** its hub, stopped in the daemon and loaded again with its config read fresh */
|
|
31
|
+
reload(): Promise<boolean> {
|
|
32
|
+
return this.st.home.machine?.hub(this.st.key, 'restart') ?? Promise.resolve(false)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get active(): boolean {
|
|
36
|
+
return this.st.home.machine?.isActive(this.st.key) === true
|
|
37
|
+
}
|
|
38
|
+
setActive(on: boolean): Promise<boolean> {
|
|
39
|
+
return this.st.home.machine?.setActive(this.st.key, on) ?? Promise.resolve(false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// THE ADMIN PAGE of a repo (`#page:admin`): what administers the repo rather
|
|
2
|
+
// than works in it, one row per control, each with the one sentence that says
|
|
3
|
+
// what it does. State in admin-svc.ts. Only on the home: a hub of its own has
|
|
4
|
+
// no machine to administer it.
|
|
5
|
+
import { observer } from 'mobx-react-lite'
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
|
+
|
|
9
|
+
const Row = ({ title, says, children }: { title: string; says: string; children: ReactNode }) => (
|
|
10
|
+
<div className="adrow">
|
|
11
|
+
<div className="adtext">
|
|
12
|
+
<b>{title}</b>
|
|
13
|
+
<span className="dim">{says}</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div className="adctl">{children}</div>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export const AdminPage = observer(({ app }: { app: RepoSt }) => {
|
|
20
|
+
const a = app.admin
|
|
21
|
+
const w = app.home.machine?.worker(app.key)
|
|
22
|
+
const name = app.name
|
|
23
|
+
return (
|
|
24
|
+
<div className="colcard checkcard">
|
|
25
|
+
<div className="chead">
|
|
26
|
+
<span className="tid">admin</span>
|
|
27
|
+
<h3 className="ctitle">{name}</h3>
|
|
28
|
+
</div>
|
|
29
|
+
<Row title="reload the hub" says={`stops ${name}'s hub in the daemon and loads it again, its repo.config.ts read fresh and its processes started again. The daemon already does it on its own when repo.config.ts changes: this is for a hub that is stuck.${w == null ? '' : ` Its hub is ${w.status} now.`}`}>
|
|
30
|
+
<button className="ghost" onClick={() => void a.reload()}>reload</button>
|
|
31
|
+
</Row>
|
|
32
|
+
<Row title="keep active" says="an active repo runs the checks the plan holds for it and gets its red pings. It is active on its own while an agent works in it or VS Code has it open: this keeps it active without either.">
|
|
33
|
+
<label className="adflag"><input type="checkbox" checked={a.active} onChange={(e) => void a.setActive(e.currentTarget.checked)} />active</label>
|
|
34
|
+
</Row>
|
|
35
|
+
<Row title="archive" says="it leaves the project list for the archived page, and nothing runs there: no hub, no watch, no plan rows, no processes, no widgets. Written to repo.config.ts as archived: true, and undone from the archived page.">
|
|
36
|
+
{a.confirming
|
|
37
|
+
? <span className="adconfirm"><span>archive {name}?</span><button className="primary" onClick={() => a.archive()}>archive it</button><button className="ghost" onClick={() => a.cancel()}>cancel</button></span>
|
|
38
|
+
: <button className="ghost" onClick={() => a.askArchive()}>archive</button>}
|
|
39
|
+
</Row>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
})
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// The view state of THE run chart: its zoom, the width it has on screen and
|
|
2
2
|
// what is under the pointer, over the geometry (timeline-geometry.ts) and the
|
|
3
|
-
// drawing (timeline-chart.tsx).
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// drawing (timeline-chart.tsx). What differs is the source: the dock's, a
|
|
4
|
+
// rolling window that ends now with one group of lanes per repo; a repo's
|
|
5
|
+
// runs page's, its whole record opened on its last minutes.
|
|
6
6
|
import { makeAutoObservable, observableRef } from 'mobx'
|
|
7
7
|
import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
|
|
8
8
|
import {
|
|
9
|
-
anchorScroll, buildAxis, fitScale, fitWindow, layoutTimeline, maxZoom, PAD_PX, windowAxis, ZOOM_MIN, zoomStep, type Axis, type Layout, type Span,
|
|
9
|
+
anchorScroll, buildAxis, fitScale, fitWindow, layoutGroups, layoutTimeline, maxZoom, PAD_PX, windowAxis, ZOOM_MIN, zoomStep, type Axis, type ChartGroup, type Layout, type Span,
|
|
10
10
|
} from './timeline-geometry.ts'
|
|
11
11
|
|
|
12
12
|
/** what a chart draws. `windowMs` null = the whole record, with idle
|
|
@@ -23,6 +23,11 @@ export type ChartSource = {
|
|
|
23
23
|
/** the span the view opens on: at its home zoom this much time, ending
|
|
24
24
|
* now, fills the width. Absent = home is zoom 1, the whole source */
|
|
25
25
|
homeSpanMs?: number
|
|
26
|
+
/** one group of lanes per key, over a rolling window only. `invocations`
|
|
27
|
+
* then holds every group's, and `laneCap` bounds each group */
|
|
28
|
+
groups?: ChartGroup[]
|
|
29
|
+
/** the fewest lanes a group takes, so its label fits beside them */
|
|
30
|
+
minLanes?: number
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/** what the tooltip says, and the viewport point it is fixed at */
|
|
@@ -117,7 +122,9 @@ export class ChartView {
|
|
|
117
122
|
get layout(): Layout | null {
|
|
118
123
|
const s = this.src
|
|
119
124
|
if (s == null || this.viewW === 0) return null
|
|
120
|
-
|
|
125
|
+
const from = this.from
|
|
126
|
+
if (s.groups != null && from != null) return layoutGroups(s.groups, this.now, this.scale, { from, laneCap: s.laneCap, minLanes: s.minLanes, cap: s.cap })
|
|
127
|
+
return layoutTimeline(s.invocations, this.now, this.scale, { from: from ?? undefined, laneCap: s.laneCap, cap: s.cap })
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
private axisAt(scale: number): Axis {
|
|
@@ -35,6 +35,8 @@ export function rememberedFact(u: CachePop): RememberedFact {
|
|
|
35
35
|
return { v: 'until a file it reads changes', inputs, help: `keyed on ${list(inputs)}: the answer is ${CACHED} until a change under ${inputs.length > 1 ? 'them' : 'it'}, then it runs again` }
|
|
36
36
|
if (u.reads === true && inputs.length === 0 && (u.ttl == null || u.ttl === '') && u.never == null)
|
|
37
37
|
return { v: 'until a file it read changes', inputs: [], help: `keyed on what its last run read: the answer is ${CACHED} until one of those files, existence probes or listings changes, then it runs again. \`shipkit keys --why\` names them` }
|
|
38
|
+
if (u.ttl === 'forever')
|
|
39
|
+
return { v: 'until its declaration changes', inputs: [], help: 'a decided fact (`ttl: \'forever\'`): a clean answer is kept until the value it checks is declared differently, and it runs only while no answer exists for that value' }
|
|
38
40
|
if (u.ttl != null && u.ttl !== '') {
|
|
39
41
|
let every = u.ttl
|
|
40
42
|
try { every = spanWords(parseDuration(u.ttl)) } catch { /* an unparsable ttl is shown as written */ }
|
|
@@ -12,6 +12,7 @@ import type { RepoSt } from './repo-st.ts'
|
|
|
12
12
|
import { applyRunEvent, idleRun, startRun, stripText, type RunProgress } from './run-strip.ts'
|
|
13
13
|
import type { GateBlock, Resource, ResourceState, SlowSuite, CheckEvent, WhenAt } from './hub-types.ts'
|
|
14
14
|
import { WHEN_OPTION } from './check-words.ts'
|
|
15
|
+
import { lineText } from '../../core/wire.ts'
|
|
15
16
|
|
|
16
17
|
/** the colour rule, the SAME one the server applies, because a row must not
|
|
17
18
|
* change meaning between a live result and the poll that confirms it */
|
|
@@ -199,7 +200,7 @@ export class ChecksSvc {
|
|
|
199
200
|
state: stateOf(e),
|
|
200
201
|
severity: e.status === 'fail' ? (e.advisory ? 'yellow' : 'red') : e.status === 'warn' ? 'yellow' : undefined,
|
|
201
202
|
check: { ...u, status: e.status, replayed: e.replayed, lastMs: e.replayed ? null : Math.round(e.ms), lastAt: Date.now(), advisory: e.advisory },
|
|
202
|
-
md: e.lines.length === 0 ? r.md : `\`\`\`\n${e.lines.map((l) => (l.kind === 'fix-fail' || l.kind === 'fix-warn' ? ` fix: ${l
|
|
203
|
+
md: e.lines.length === 0 ? r.md : `\`\`\`\n${e.lines.map((l) => (l.kind === 'fix-fail' || l.kind === 'fix-warn' ? ` fix: ${lineText(l)}` : l.kind === 'fail' ? `FAIL: ${lineText(l)}` : l.kind === 'warn' ? `WARN ${lineText(l)}` : l.kind === 'skip' ? `skip: ${lineText(l)}` : `ok: ${lineText(l)}`)).join('\n')}\n\`\`\``,
|
|
203
204
|
}
|
|
204
205
|
})
|
|
205
206
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// The checks page and the asks page: one dense table each, in the activity
|
|
2
|
+
// page's table style, one line per item under group headers that count their
|
|
3
|
+
// lines. What needs attention comes first; the quiet groups (passing, off,
|
|
4
|
+
// done) fold by default and each viewer's fold choice is kept. The filter
|
|
5
|
+
// input and the severity keys narrow the lines, since both read
|
|
6
|
+
// `visibleInstances`. Groups, order and words: col-tables.ts.
|
|
7
|
+
import { observer } from 'mobx-react-lite'
|
|
8
|
+
import type { ReactNode } from 'react'
|
|
9
|
+
import { checkName } from './check-words.ts'
|
|
10
|
+
import { askGroups, checkGroups, dayMs, findingLine, planWord, type TableGroup } from './col-tables.ts'
|
|
11
|
+
import { durationTone, fmtMs } from './dur.ts'
|
|
12
|
+
import type { Resource } from './hub-types.ts'
|
|
13
|
+
import type { RepoSt } from './repo-st.ts'
|
|
14
|
+
import { TimeAgo } from './time-ago.tsx'
|
|
15
|
+
|
|
16
|
+
const emptyWords = (app: RepoSt): string => (app.filter === 'red' ? 'no reds here' : app.filter === 'needs' ? 'nothing here needs you' : app.query.trim() !== '' ? 'nothing matches the filter' : 'nothing here yet')
|
|
17
|
+
|
|
18
|
+
/** the page frame: the collection's title and count, then the table */
|
|
19
|
+
const Page = ({ title, n, cols, empty, children }: { title: string; n: number; cols: ReactNode; empty: string; children: ReactNode }) => (
|
|
20
|
+
<div className="ctpage">
|
|
21
|
+
<div className="cghead">{title}<span className="cnt">{n}</span></div>
|
|
22
|
+
{n === 0 ? <div className="ph">{empty}</div> : (
|
|
23
|
+
<table className="actable cttable">
|
|
24
|
+
<colgroup>{cols}</colgroup>
|
|
25
|
+
{children}
|
|
26
|
+
</table>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/** one group: its header line (a click folds it), then its lines unless folded */
|
|
32
|
+
const Group = observer(<K extends string>({ app, col, g, span, row }: { app: RepoSt; col: string; g: TableGroup<K>; span: number; row: (r: Resource) => ReactNode }) => {
|
|
33
|
+
const key = `${col}:${g.key}`
|
|
34
|
+
const folded = app.cols.groupFolded(key, g.foldedByDefault)
|
|
35
|
+
return (
|
|
36
|
+
<tbody className="ctgroup">
|
|
37
|
+
<tr className={`ctgrp g-${g.key}`} data-k={g.key} onClick={() => app.cols.toggleGroup(key, g.foldedByDefault)}>
|
|
38
|
+
<td colSpan={span}><span className="ctcaret">{folded ? '▸' : '▾'}</span>{g.label}</td>
|
|
39
|
+
</tr>
|
|
40
|
+
{folded ? null : g.rows.map(row)}
|
|
41
|
+
</tbody>
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const DOT: Record<string, { tone: string; mark: string }> = {
|
|
46
|
+
fail: { tone: 't-red', mark: '●' },
|
|
47
|
+
warn: { tone: 't-warn', mark: '●' },
|
|
48
|
+
pass: { tone: 't-ok', mark: '●' },
|
|
49
|
+
skip: { tone: 'acdim', mark: '●' },
|
|
50
|
+
never: { tone: 'acdim', mark: '○' },
|
|
51
|
+
off: { tone: 'acdim', mark: '○' },
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CheckRow = observer(({ app, r }: { app: RepoSt; r: Resource }) => {
|
|
55
|
+
const c = r.check!
|
|
56
|
+
const name = checkName(c.checkId, r.title)
|
|
57
|
+
const dot = c.when === 'off' ? DOT.off! : c.status == null ? DOT.never! : DOT[c.status]!
|
|
58
|
+
const finding = findingLine(c)
|
|
59
|
+
const tone = durationTone(c.lastMs, c.timeoutMs, c.budgetMs)
|
|
60
|
+
return (
|
|
61
|
+
<tr className={`ctrow s-${c.status ?? 'never'}`} data-k={c.checkId} onClick={() => app.cols.select(r.collection, r.id)} title={c.description}>
|
|
62
|
+
<td className={`ctdot ${dot.tone}`}>{dot.mark}</td>
|
|
63
|
+
<td className={`ctid ${c.status === 'fail' ? 't-red' : c.status === 'warn' ? 't-warn' : ''}`}>{c.checkId}</td>
|
|
64
|
+
<td className="cttitle">{name.title === c.checkId ? '' : name.title}</td>
|
|
65
|
+
<td className="acwhen">{c.lastAt == null ? '' : <TimeAgo clock={app.clock} at={c.lastAt} />}</td>
|
|
66
|
+
<td className="ctdur">
|
|
67
|
+
{c.lastMs == null ? null : <span className={`dnum ${tone ?? ''}`}>{fmtMs(c.lastMs)}</span>}
|
|
68
|
+
<span className="ctbudget">{` / ${fmtMs(c.budgetMs)}`}</span>
|
|
69
|
+
</td>
|
|
70
|
+
<td className="ctfind" title={c.lastLines.length > 0 ? c.lastLines.join('\n') : undefined}>{finding}</td>
|
|
71
|
+
<td className="ctplan">{planWord(c)}</td>
|
|
72
|
+
</tr>
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const CheckTable = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
77
|
+
const col = app.cols.sidebarCols.find((x) => x.id === id)
|
|
78
|
+
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
79
|
+
const items = app.cols.visibleInstances(col)
|
|
80
|
+
const cols = (
|
|
81
|
+
<>
|
|
82
|
+
<col className="c-dot" /><col className="c-id" /><col className="c-title" /><col className="c-age" /><col className="c-dur" /><col /><col className="c-plan" />
|
|
83
|
+
</>
|
|
84
|
+
)
|
|
85
|
+
return (
|
|
86
|
+
<Page title={col.title} n={items.length} cols={cols} empty={emptyWords(app)}>
|
|
87
|
+
{checkGroups(items).map((g) => <Group key={g.key} app={app} col={col.id} g={g} span={7} row={(r) => <CheckRow key={r.id} app={app} r={r} />} />)}
|
|
88
|
+
</Page>
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const AskRow = observer(({ app, r }: { app: RepoSt; r: Resource }) => {
|
|
93
|
+
const a = r.ask
|
|
94
|
+
const done = r.id.startsWith('ask:done/')
|
|
95
|
+
const at = dayMs(done ? (a?.closed ?? a?.created) : a?.created)
|
|
96
|
+
const blocks = a == null || a.blocks === '-' ? '' : a.blocks
|
|
97
|
+
return (
|
|
98
|
+
<tr className={`ctrow s-${done ? 'done' : 'open'}`} data-k={r.id} onClick={() => app.cols.select(r.collection, r.id)} title={r.file}>
|
|
99
|
+
<td className={`ctkind ${done ? 'acdim' : ''}`}>{a?.kind ?? r.slot ?? ''}</td>
|
|
100
|
+
<td className={`cttitle ${done ? 'acdim' : ''}`}>{r.title}</td>
|
|
101
|
+
<td className="acwhen">{at == null ? '' : <TimeAgo clock={app.clock} at={at} />}</td>
|
|
102
|
+
<td className="ctblocks" title={blocks === '' ? undefined : blocks}>{blocks}</td>
|
|
103
|
+
{done ? <td /> : a?.probe != null
|
|
104
|
+
? <td className="ctprobe t-ok" title={`closes itself once \`${a.probe}\` exits 0`}>auto close</td>
|
|
105
|
+
: <td className="ctprobe acdim" title="no probe: done by hand">manual</td>}
|
|
106
|
+
</tr>
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export const AskTable = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
111
|
+
const col = app.cols.sidebarCols.find((x) => x.id === id)
|
|
112
|
+
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
113
|
+
const items = app.cols.visibleInstances(col)
|
|
114
|
+
const cols = (
|
|
115
|
+
<>
|
|
116
|
+
<col className="c-kind" /><col /><col className="c-age" /><col className="c-blocks" /><col className="c-probe" />
|
|
117
|
+
</>
|
|
118
|
+
)
|
|
119
|
+
return (
|
|
120
|
+
<Page title={col.title} n={items.length} cols={cols} empty={emptyWords(app)}>
|
|
121
|
+
{askGroups(items).map((g) => <Group key={g.key} app={app} col={col.id} g={g} span={5} row={(r) => <AskRow key={r.id} app={app} r={r} />} />)}
|
|
122
|
+
</Page>
|
|
123
|
+
)
|
|
124
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The checks page and the asks page as dense tables: which group each line
|
|
2
|
+
// belongs to, the order inside a group, and every word a line says. Pure, so
|
|
3
|
+
// a test pins them rather than a screenshot (col-tables-view.tsx draws them).
|
|
4
|
+
import { parseDuration } from '../../core/wire.ts'
|
|
5
|
+
import { spanShort } from '../../plan/plan-timeline.ts'
|
|
6
|
+
import type { CachePop } from './check-words.ts'
|
|
7
|
+
import type { CheckFacts, Resource } from './hub-types.ts'
|
|
8
|
+
|
|
9
|
+
/** one group of lines under its header. `key` is what the viewer's fold
|
|
10
|
+
* choice is remembered under, with the collection before it */
|
|
11
|
+
export type TableGroup<K extends string> = { key: K; label: string; rows: Resource[]; foldedByDefault: boolean }
|
|
12
|
+
|
|
13
|
+
export type CheckGroupKey = 'fail' | 'warn' | 'never' | 'pass' | 'off'
|
|
14
|
+
|
|
15
|
+
/** the groups in the order a reader wants them: what needs attention first */
|
|
16
|
+
const CHECK_GROUPS: { key: CheckGroupKey; word: string; folded: boolean }[] = [
|
|
17
|
+
{ key: 'fail', word: 'failing', folded: false },
|
|
18
|
+
{ key: 'warn', word: 'warning', folded: false },
|
|
19
|
+
{ key: 'never', word: 'never ran', folded: false },
|
|
20
|
+
{ key: 'pass', word: 'passing', folded: true },
|
|
21
|
+
{ key: 'off', word: 'off', folded: true },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
/** a skipped check had nothing to look at, which is nothing to fix: it sits
|
|
25
|
+
* with the passing ones, its dot grey */
|
|
26
|
+
export function checkGroupOf(c: CheckFacts): CheckGroupKey {
|
|
27
|
+
if (c.when === 'off') return 'off'
|
|
28
|
+
if (c.status === 'fail') return 'fail'
|
|
29
|
+
if (c.status === 'warn') return 'warn'
|
|
30
|
+
if (c.status == null) return 'never'
|
|
31
|
+
return 'pass'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const idOf = (r: Resource): string => r.check?.checkId ?? r.id
|
|
35
|
+
|
|
36
|
+
/** most recent first; a check with no result by its id */
|
|
37
|
+
const byRecent = (a: Resource, b: Resource): number => (b.check?.lastAt ?? -1) - (a.check?.lastAt ?? -1) || idOf(a).localeCompare(idOf(b))
|
|
38
|
+
|
|
39
|
+
export function checkGroups(rows: readonly Resource[]): TableGroup<CheckGroupKey>[] {
|
|
40
|
+
const withFacts = rows.filter((r) => r.check != null)
|
|
41
|
+
return CHECK_GROUPS.map((g) => {
|
|
42
|
+
const mine = withFacts.filter((r) => checkGroupOf(r.check!) === g.key).sort(byRecent)
|
|
43
|
+
return { key: g.key, label: `${mine.length} ${g.word}`, rows: mine, foldedByDefault: g.folded }
|
|
44
|
+
}).filter((g) => g.rows.length > 0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** a wire line as plain text: the markdown ticks it is rendered with go */
|
|
48
|
+
const plain = (l: string): string => l.replace(/`/g, '').trim()
|
|
49
|
+
|
|
50
|
+
const escape = (x: string): string => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
51
|
+
|
|
52
|
+
/** a line without what its row already shows: the verdict word (the dot
|
|
53
|
+
* says it) and the check's own id (the id column says it) */
|
|
54
|
+
export function strippedLine(line: string, id: string): string {
|
|
55
|
+
const lead = new RegExp(`^(?:(?:FAIL|WARN|✗|⚠):?\\s*|\\[${escape(id)}\\]\\s*|${escape(id)}(?:\\s+—\\s+|:\\s+|\\s+|$))`)
|
|
56
|
+
let l = line
|
|
57
|
+
for (let m = l.match(lead); m != null && m[0] !== ''; m = l.match(lead)) l = l.slice(m[0].length)
|
|
58
|
+
return l.trim()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** the first line of the last run that says something: a failure or a
|
|
62
|
+
* warning first, else any line that is not the bare `ok: <id>` header */
|
|
63
|
+
export function findingLine(c: CheckFacts): string {
|
|
64
|
+
const lines = c.lastLines.map(plain).filter((l) => l !== '')
|
|
65
|
+
const loud = lines.filter((l) => /^(FAIL|WARN|✗|⚠)/.test(l))
|
|
66
|
+
const rest = lines.filter((l) => !/^(FAIL|WARN|✗|⚠|ok\b|skip\b)/.test(l))
|
|
67
|
+
for (const l of [...loud, ...rest]) {
|
|
68
|
+
const s = strippedLine(l, c.checkId)
|
|
69
|
+
if (s !== '') return s
|
|
70
|
+
}
|
|
71
|
+
return ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** how a check is planned, as one short word a column scans: what makes it
|
|
75
|
+
* run again (a file change, a span of time, never again) or how often it
|
|
76
|
+
* runs when it remembers nothing */
|
|
77
|
+
export function planWord(u: CachePop): string {
|
|
78
|
+
if (u.when === 'off') return 'off'
|
|
79
|
+
if ((u.inputs ?? []).length > 0 || u.reads === true) return 'on change'
|
|
80
|
+
if (u.ttl === 'forever') return 'once'
|
|
81
|
+
if (u.ttl != null && u.ttl !== '') {
|
|
82
|
+
try { return `every ${spanShort(parseDuration(u.ttl))}` } catch { return `every ${u.ttl}` }
|
|
83
|
+
}
|
|
84
|
+
return u.when === 'release' ? 'every release' : u.when === 'health' ? 'health pass' : 'every run'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type AskGroupKey = 'open' | 'done'
|
|
88
|
+
|
|
89
|
+
/** a `YYYY-MM-DD` stamp as ms, null when the file carries none */
|
|
90
|
+
export const dayMs = (d: string | null | undefined): number | null => {
|
|
91
|
+
if (d == null) return null
|
|
92
|
+
const t = Date.parse(d)
|
|
93
|
+
return Number.isNaN(t) ? null : t
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const NOTHING = new Set(['', '-'])
|
|
97
|
+
const blocksOf = (r: Resource): string => r.ask?.blocks ?? '-'
|
|
98
|
+
|
|
99
|
+
/** open asks by what they block (an ask that blocks nothing last), the
|
|
100
|
+
* oldest first inside one; done asks after, the last closed first */
|
|
101
|
+
export function askGroups(rows: readonly Resource[]): TableGroup<AskGroupKey>[] {
|
|
102
|
+
const isDone = (r: Resource): boolean => r.id.startsWith('ask:done/')
|
|
103
|
+
const open = rows.filter((r) => !isDone(r)).sort((a, b) => {
|
|
104
|
+
const na = NOTHING.has(blocksOf(a)) ? 1 : 0
|
|
105
|
+
const nb = NOTHING.has(blocksOf(b)) ? 1 : 0
|
|
106
|
+
return na - nb || blocksOf(a).localeCompare(blocksOf(b)) || (dayMs(a.ask?.created) ?? 0) - (dayMs(b.ask?.created) ?? 0) || a.id.localeCompare(b.id)
|
|
107
|
+
})
|
|
108
|
+
const closedAt = (r: Resource): number => dayMs(r.ask?.closed) ?? dayMs(r.ask?.created) ?? 0
|
|
109
|
+
const done = rows.filter(isDone).sort((a, b) => closedAt(b) - closedAt(a) || a.id.localeCompare(b.id))
|
|
110
|
+
const groups: TableGroup<AskGroupKey>[] = [
|
|
111
|
+
{ key: 'open', label: `${open.length} open`, rows: open, foldedByDefault: false },
|
|
112
|
+
{ key: 'done', label: `${done.length} done`, rows: done, foldedByDefault: true },
|
|
113
|
+
]
|
|
114
|
+
return groups.filter((g) => g.rows.length > 0)
|
|
115
|
+
}
|
|
@@ -9,6 +9,20 @@ import type { Col, ColMeta, HubProblem, Resource } from './hub-types.ts'
|
|
|
9
9
|
import type { PlanRow } from '../../plan/planner.ts'
|
|
10
10
|
import { readScreen, SCREEN_WRITE_MS, writeScreen } from './screen-cache.ts'
|
|
11
11
|
|
|
12
|
+
/** where a viewer's fold choices on the checks and asks tables are kept,
|
|
13
|
+
* one per browser and not per repo: `checks:pass` open is a habit */
|
|
14
|
+
export const TABLE_FOLDS_KEY = 'hub:table-folds'
|
|
15
|
+
|
|
16
|
+
const readFolds = (): Map<string, boolean> => {
|
|
17
|
+
try {
|
|
18
|
+
const v = JSON.parse(globalThis.localStorage?.getItem(TABLE_FOLDS_KEY) ?? '{}') as unknown
|
|
19
|
+
if (v == null || typeof v !== 'object') return new Map()
|
|
20
|
+
return new Map(Object.entries(v).filter((e): e is [string, boolean] => typeof e[1] === 'boolean'))
|
|
21
|
+
} catch {
|
|
22
|
+
return new Map()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
/** what /api/resources.json answers */
|
|
13
27
|
type Answer = { collections: ColMeta[]; resources: Resource[]; planned?: PlanRow[] | null; hub?: HubProblem[] }
|
|
14
28
|
|
|
@@ -43,6 +57,9 @@ export class ColsSvc {
|
|
|
43
57
|
proofDraft = ''
|
|
44
58
|
/** user fold choices win; default = open when the section holds needs */
|
|
45
59
|
foldOverride = new Map<string, boolean>()
|
|
60
|
+
/** the viewer's fold choices on the table groups (`checks:pass`,
|
|
61
|
+
* `asks:done`), true = folded; a group with none folds by its default */
|
|
62
|
+
tableFolds = readFolds()
|
|
46
63
|
private timer: ReturnType<typeof setInterval> | null = null
|
|
47
64
|
|
|
48
65
|
constructor(private st: RepoSt) {
|
|
@@ -177,6 +194,17 @@ export class ColsSvc {
|
|
|
177
194
|
openSection(c: Col): void {
|
|
178
195
|
this.foldOverride.set(c.id, true)
|
|
179
196
|
}
|
|
197
|
+
/** a table group is folded: a live search shows every match, and a group
|
|
198
|
+
* the viewer never touched folds by its default */
|
|
199
|
+
groupFolded(key: string, byDefault: boolean): boolean {
|
|
200
|
+
if (this.st.query.trim() !== '') return false
|
|
201
|
+
return this.tableFolds.get(key) ?? byDefault
|
|
202
|
+
}
|
|
203
|
+
toggleGroup(key: string, byDefault: boolean): void {
|
|
204
|
+
if (this.st.query.trim() !== '') return
|
|
205
|
+
this.tableFolds.set(key, !this.groupFolded(key, byDefault))
|
|
206
|
+
try { globalThis.localStorage?.setItem(TABLE_FOLDS_KEY, JSON.stringify(Object.fromEntries(this.tableFolds))) } catch { /* private window: the choice lasts the tab */ }
|
|
207
|
+
}
|
|
180
208
|
/** rail red dot — the section holds at least one red item */
|
|
181
209
|
hasRed(c: Col): boolean {
|
|
182
210
|
return c.instances.some((i) => i.severity === 'red')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* small controls any page can use. Colours are hub.css tokens, spacing its
|
|
2
|
+
scale (tests/hub-css-invariants.test.ts reads this sheet too) */
|
|
3
|
+
|
|
4
|
+
/* the cycling button (cycle-button.tsx): the current label, one dot per
|
|
5
|
+
option with the current one in the ink colour, then its shortcut */
|
|
6
|
+
.cycle{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:18px;padding:0 var(--s1);background:var(--panel2);border:1px solid var(--line);border-radius:9px;
|
|
7
|
+
color:var(--ink);font-size:10px;line-height:1;cursor:pointer}
|
|
8
|
+
.cycle:hover{border-color:var(--accent)}
|
|
9
|
+
.cyclbl{min-width:26px;text-align:center}
|
|
10
|
+
.cycdots{display:inline-flex;align-items:center;gap:var(--s0)}
|
|
11
|
+
.cycdot{width:4px;height:4px;border-radius:2px;background:var(--dim);opacity:.5}
|
|
12
|
+
.cycdot.on{background:var(--ink);opacity:1}
|
|
13
|
+
.cyckey{font:9px/1 ui-monospace,Menlo,monospace;color:var(--dim);background:none;border:0;padding:0}
|
|
14
|
+
|
|
15
|
+
/* a row of pills (pill-row.tsx): one row always, the rest folded into +N.
|
|
16
|
+
The probe is an invisible copy that only gives the widths */
|
|
17
|
+
.pchips{position:relative;display:flex;align-items:center;gap:var(--s1);min-width:0;overflow:hidden;white-space:nowrap}
|
|
18
|
+
.pchipw{flex:none;display:inline-flex}
|
|
19
|
+
.pchipprobe{position:absolute;left:0;top:0;width:max-content;overflow:visible;visibility:hidden;pointer-events:none}
|
|
20
|
+
.pchip{display:inline-flex;align-items:center;gap:var(--s1);height:16px;padding:0 var(--s1);border:1px solid var(--line);border-radius:8px;
|
|
21
|
+
background:var(--panel2);color:var(--dim);font-size:10px;line-height:1}
|
|
22
|
+
.pchip.accent{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,var(--line))}
|
|
23
|
+
.pchip.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,var(--line))}
|
|
24
|
+
.pchip img,.pchip .dgico{flex:none;border-radius:2px}
|
|
25
|
+
.pcmark{font-weight:700}
|
|
26
|
+
.pcmark.ok{color:var(--ok)}
|
|
27
|
+
.pcmark.red{color:var(--red)}
|
|
28
|
+
.pcmark.warn{color:var(--warn)}
|
|
29
|
+
.pcmark.dim{color:var(--dim)}
|
|
30
|
+
.pctext{color:var(--ink);opacity:.8}
|
|
31
|
+
.pchip.accent .pctext,.pchip.warn .pctext{color:inherit;opacity:1}
|
|
32
|
+
.pcage{color:var(--dim);opacity:.75}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// a button that cycles through a few options: it shows only the current
|
|
2
|
+
// option's label, then one dot per option (the current one lit), then its
|
|
3
|
+
// shortcut. A click moves to the next option, the last one wrapping to the
|
|
4
|
+
// first; the page's key handler calls the same move for the shortcut.
|
|
5
|
+
|
|
6
|
+
export type CycleOption<T extends string> = { readonly value: T; readonly label: string }
|
|
7
|
+
|
|
8
|
+
/** the option after `value`, the last one wrapping to the first */
|
|
9
|
+
export function nextOption<T extends string>(options: readonly CycleOption<T>[], value: T): T {
|
|
10
|
+
const i = options.findIndex((o) => o.value === value)
|
|
11
|
+
return options[(i + 1) % options.length]!.value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CycleButton<T extends string>(p: {
|
|
15
|
+
options: readonly CycleOption<T>[]
|
|
16
|
+
value: T
|
|
17
|
+
onChange: (next: T) => void
|
|
18
|
+
/** the key that cycles it too, shown as a badge */
|
|
19
|
+
shortcut?: string
|
|
20
|
+
/** what the button is for, on hover */
|
|
21
|
+
title?: string
|
|
22
|
+
className?: string
|
|
23
|
+
}) {
|
|
24
|
+
const cur = p.options.find((o) => o.value === p.value)
|
|
25
|
+
const next = p.options.find((o) => o.value === nextOption(p.options, p.value))
|
|
26
|
+
const hint = `${p.title == null ? '' : `${p.title}\n`}click${p.shortcut == null ? '' : ` or ${p.shortcut}`}: ${next?.label ?? ''}`
|
|
27
|
+
return (
|
|
28
|
+
<button className={`cycle${p.className == null ? '' : ` ${p.className}`}`} title={hint} onClick={() => p.onChange(nextOption(p.options, p.value))}>
|
|
29
|
+
<span className="cyclbl">{cur?.label ?? p.value}</span>
|
|
30
|
+
<span className="cycdots" aria-hidden="true">
|
|
31
|
+
{p.options.map((o) => <i key={o.value} className={`cycdot${o.value === p.value ? ' on' : ''}`} />)}
|
|
32
|
+
</span>
|
|
33
|
+
{p.shortcut == null ? null : <kbd className="cyckey">{p.shortcut}</kbd>}
|
|
34
|
+
</button>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// the dock's size change, eased: the class on the page's vertical split
|
|
2
|
+
// carries a flex-grow transition (dock.css), put on only while a size change
|
|
3
|
+
// resizes the panels, so a drag, a window resize and the first paint stay
|
|
4
|
+
// instant. prefers-reduced-motion drops the transition in the sheet.
|
|
5
|
+
|
|
6
|
+
/** how long the dock takes to reach its new size */
|
|
7
|
+
export const DOCK_ANIM_MS = 180
|
|
8
|
+
/** the class that eases the split's panels */
|
|
9
|
+
export const DOCK_ANIM = 'dockanim'
|
|
10
|
+
|
|
11
|
+
const timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>()
|
|
12
|
+
|
|
13
|
+
/** runs `resize` with the class on `group`, and takes it off once the
|
|
14
|
+
* transition ended; a second call meanwhile keeps it on until its own end */
|
|
15
|
+
export function animateDock(group: HTMLElement | null, resize: () => void, ms = DOCK_ANIM_MS + 40): void {
|
|
16
|
+
if (group == null) return resize()
|
|
17
|
+
const was = timers.get(group)
|
|
18
|
+
if (was != null) clearTimeout(was)
|
|
19
|
+
group.classList.add(DOCK_ANIM)
|
|
20
|
+
resize()
|
|
21
|
+
timers.set(group, setTimeout(() => {
|
|
22
|
+
timers.delete(group)
|
|
23
|
+
group.classList.remove(DOCK_ANIM)
|
|
24
|
+
}, ms))
|
|
25
|
+
}
|