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,65 @@
|
|
|
1
|
+
// a row of small boxes that stays one row at any width: the ones that do not
|
|
2
|
+
// fit fold into a `+N` box at its end, whose hover lists them. An invisible
|
|
3
|
+
// copy of every box, out of the flow, gives their widths, so what shows is
|
|
4
|
+
// decided before the paint and never wraps. Each box's hover is its title.
|
|
5
|
+
import { observer, useLocalObservable } from 'mobx-react-lite'
|
|
6
|
+
import { useLayoutEffect, useRef, type ReactNode } from 'react'
|
|
7
|
+
import { Tip } from './floating.tsx'
|
|
8
|
+
|
|
9
|
+
export type PillItem = { key: string; node: ReactNode; title: string }
|
|
10
|
+
|
|
11
|
+
/** how many boxes of these widths fit in `avail`, a `+N` box of `plusW`
|
|
12
|
+
* taking its room as soon as one does not */
|
|
13
|
+
export function fitCount(widths: number[], avail: number, plusW: number, gap: number): number {
|
|
14
|
+
const all = widths.reduce((a, w) => a + w, 0) + gap * Math.max(0, widths.length - 1)
|
|
15
|
+
if (all <= avail) return widths.length
|
|
16
|
+
let used = plusW
|
|
17
|
+
for (let i = 0; i < widths.length; i++) {
|
|
18
|
+
used += widths[i]! + gap
|
|
19
|
+
if (used > avail) return i
|
|
20
|
+
}
|
|
21
|
+
return widths.length
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const lines = (text: string): ReactNode => text.split('\n').map((l, i) => <div key={i}>{l}</div>)
|
|
25
|
+
|
|
26
|
+
export const PillRow = observer(({ items, className }: { items: PillItem[]; className?: string }) => {
|
|
27
|
+
const st = useLocalObservable(() => ({ n: Number.POSITIVE_INFINITY, set(v: number) { this.n = v } }))
|
|
28
|
+
const box = useRef<HTMLDivElement | null>(null)
|
|
29
|
+
const probe = useRef<HTMLDivElement | null>(null)
|
|
30
|
+
const measure = (): void => {
|
|
31
|
+
const b = box.current
|
|
32
|
+
const p = probe.current
|
|
33
|
+
if (b == null || p == null) return
|
|
34
|
+
const kids = [...p.children] as HTMLElement[]
|
|
35
|
+
const plus = kids.pop()
|
|
36
|
+
if (plus == null) return
|
|
37
|
+
const gap = Number.parseFloat(getComputedStyle(b).columnGap) || 0
|
|
38
|
+
st.set(fitCount(kids.map((k) => k.getBoundingClientRect().width), b.clientWidth, plus.getBoundingClientRect().width, gap))
|
|
39
|
+
}
|
|
40
|
+
// after every render: the boxes' words change with the clock
|
|
41
|
+
useLayoutEffect(measure)
|
|
42
|
+
useLayoutEffect(() => {
|
|
43
|
+
const b = box.current
|
|
44
|
+
if (b == null || typeof ResizeObserver === 'undefined') return
|
|
45
|
+
const ro = new ResizeObserver(() => measure())
|
|
46
|
+
ro.observe(b)
|
|
47
|
+
return () => ro.disconnect()
|
|
48
|
+
}, [])
|
|
49
|
+
const n = Math.min(st.n, items.length)
|
|
50
|
+
const hidden = items.slice(n)
|
|
51
|
+
return (
|
|
52
|
+
<div ref={box} className={`pchips${className == null ? '' : ` ${className}`}`}>
|
|
53
|
+
{items.slice(0, n).map((it) => <Tip key={it.key} className="pchipw" content={lines(it.title)}>{it.node}</Tip>)}
|
|
54
|
+
{hidden.length === 0 ? null : (
|
|
55
|
+
<Tip className="pchipw" content={hidden.map((h) => <div key={h.key}>{lines(h.title)}</div>)}>
|
|
56
|
+
<span className="pchip dim">+{hidden.length}</span>
|
|
57
|
+
</Tip>
|
|
58
|
+
)}
|
|
59
|
+
<div ref={probe} className="pchips pchipprobe" aria-hidden="true">
|
|
60
|
+
{items.map((it) => <span key={it.key} className="pchipw">{it.node}</span>)}
|
|
61
|
+
<span className="pchip dim">+{items.length}</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
})
|
|
@@ -1,100 +1,25 @@
|
|
|
1
1
|
// the planned page's state: the machine's one plan as the daemon answers it,
|
|
2
|
-
// as ONE list ordered by time (now,
|
|
2
|
+
// as ONE list ordered by time (now, next), filtered by a text that
|
|
3
3
|
// matches the project or the check. A check that only waits for a save has no
|
|
4
4
|
// time, so it is not listed. The order and every word a row says are pure
|
|
5
5
|
// functions, so a test pins them rather than a screenshot.
|
|
6
6
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
7
7
|
import type { PlannedAnswer } from '../../home/home-routes.ts'
|
|
8
|
-
import type {
|
|
9
|
-
import
|
|
8
|
+
import type { PlanRow } from '../../plan/planner.ts'
|
|
9
|
+
import { sectionOf, timeline, titleWords, waitingRows, waitingWords } from '../../plan/plan-timeline.ts'
|
|
10
10
|
import type { HomeSt } from './home-st.ts'
|
|
11
11
|
|
|
12
12
|
export const PLANNED_POLL_MS = 1_000
|
|
13
13
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
/** the section a row sits in; null for a check with no time of its own */
|
|
17
|
-
export function sectionOf(r: PlanRow): Section | null {
|
|
18
|
-
if (r.state === 'running' || r.state === 'due') return 'now'
|
|
19
|
-
if (r.state === 'late') return 'late'
|
|
20
|
-
if (r.state === 'next') return 'next'
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** a row matches a text filter when its project or check holds every word
|
|
25
|
-
* of it, in any case */
|
|
26
|
-
export function matches(r: PlanRow, query: string): boolean {
|
|
27
|
-
const words = query.toLowerCase().split(/\s+/).filter((w) => w !== '')
|
|
28
|
-
const hay = `${r.repo} ${r.check}`.toLowerCase()
|
|
29
|
-
return words.every((w) => hay.includes(w))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** the one list: running first, then due, the late ones most late first,
|
|
33
|
-
* then the next ones soonest first. Each row carries its section */
|
|
34
|
-
export function timeline(rows: PlanRow[], query = ''): { section: Section; row: PlanRow }[] {
|
|
35
|
-
const rank = (r: PlanRow): number => (r.state === 'running' ? 0 : r.state === 'due' ? 1 : r.state === 'late' ? 2 : 3)
|
|
36
|
-
return rows
|
|
37
|
-
.filter((r) => sectionOf(r) != null && matches(r, query))
|
|
38
|
-
.sort((a, b) => rank(a) - rank(b) || (a.dueAt ?? Infinity) - (b.dueAt ?? Infinity) || a.repo.localeCompare(b.repo) || a.check.localeCompare(b.check))
|
|
39
|
-
.map((row) => ({ section: sectionOf(row)!, row }))
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const MIN = 60_000
|
|
43
|
-
const HOUR = 60 * MIN
|
|
44
|
-
const DAY = 24 * HOUR
|
|
45
|
-
|
|
46
|
-
/** a span in its largest unit: `<1m`, `25m`, `2h`, `3d` */
|
|
47
|
-
export function spanShort(ms: number): string {
|
|
48
|
-
if (ms < MIN) return '<1m'
|
|
49
|
-
if (ms < HOUR) return `${Math.round(ms / MIN)}m`
|
|
50
|
-
if (ms < DAY) return `${Math.round(ms / HOUR)}h`
|
|
51
|
-
return `${Math.round(ms / DAY)}d`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** when the row runs, relative to now: `now`, `in 4m`, `late 2h` */
|
|
55
|
-
export function whenWords(r: PlanRow, now: number): string {
|
|
56
|
-
if (r.dueAt == null || r.state === 'running' || r.state === 'due') return 'now'
|
|
57
|
-
const d = r.dueAt - now
|
|
58
|
-
if (r.state === 'late') return `late ${spanShort(Math.max(0, -d))}`
|
|
59
|
-
return d > 0 ? `in ${spanShort(d)}` : 'now'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export type Mark = 'running' | 'failed' | 'late' | 'due' | 'next'
|
|
63
|
-
|
|
64
|
-
/** the one mark a row shows and its few words: running wins, then a failed
|
|
65
|
-
* last result, then late with why it waits */
|
|
66
|
-
export function markOf(r: PlanRow): { mark: Mark; words: string } {
|
|
67
|
-
if (r.state === 'running') return { mark: 'running', words: 'running' }
|
|
68
|
-
if (r.last?.status === 'fail') return { mark: 'failed', words: 'last failed' }
|
|
69
|
-
if (r.state === 'late') return { mark: 'late', words: 'waits: repo inactive' }
|
|
70
|
-
if (r.state === 'due') return { mark: 'due', words: 'due' }
|
|
71
|
-
return { mark: 'next', words: '' }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** why it is due or planned, in words */
|
|
75
|
-
export const REASON_WORDS: Record<PlanReason, string> = {
|
|
76
|
-
changed: 'files changed',
|
|
77
|
-
scheduled: 'scheduled',
|
|
78
|
-
late: 'overdue',
|
|
79
|
-
asked: 'asked by hand',
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const reasonWords = (r: PlanRow): string => (r.reason == null ? '' : REASON_WORDS[r.reason])
|
|
83
|
-
|
|
84
|
-
/** the tone of a last result: red failing, warn warning, ok passing */
|
|
85
|
-
export const LAST_TONE: Record<LastStatus, 'red' | 'warn' | 'ok' | 'dim'> = { fail: 'red', warn: 'warn', pass: 'ok', skip: 'dim' }
|
|
86
|
-
|
|
87
|
-
/** a title worth showing beside the id: a title that repeats the id loses it */
|
|
88
|
-
export function titleWords(title: string | null, check: string): string | null {
|
|
89
|
-
const t = title?.startsWith(check) === true ? title.slice(check.length).replace(/^[\s:·]+/, '') : title
|
|
90
|
-
return t == null || t === '' ? null : t
|
|
91
|
-
}
|
|
14
|
+
export * from '../../plan/plan-timeline.ts'
|
|
92
15
|
|
|
93
16
|
export class PlannedSt {
|
|
94
17
|
data: PlannedAnswer | null = null
|
|
95
18
|
error = ''
|
|
96
19
|
/** the text filter, the `?q=` of /planned: every word must be in the project or the check */
|
|
97
20
|
query = ''
|
|
21
|
+
/** the checks waiting for their repo to be active, listed under their line */
|
|
22
|
+
waitingOpen = false
|
|
98
23
|
private timer: ReturnType<typeof setInterval> | null = null
|
|
99
24
|
|
|
100
25
|
constructor(private st: HomeSt) {
|
|
@@ -105,6 +30,20 @@ export class PlannedSt {
|
|
|
105
30
|
return timeline(this.data?.rows ?? [], this.query)
|
|
106
31
|
}
|
|
107
32
|
|
|
33
|
+
/** the checks waiting for their repo to be active, the filter applied */
|
|
34
|
+
get waiting(): PlanRow[] {
|
|
35
|
+
return waitingRows(this.data?.rows ?? [], this.query)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** their one line, or '' */
|
|
39
|
+
get waitingLine(): string {
|
|
40
|
+
return waitingWords(this.data?.rows ?? [], this.query)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
toggleWaiting(): void {
|
|
44
|
+
this.waitingOpen = !this.waitingOpen
|
|
45
|
+
}
|
|
46
|
+
|
|
108
47
|
/** how many checks have a time of their own, filter aside */
|
|
109
48
|
get timed(): number {
|
|
110
49
|
return (this.data?.rows ?? []).filter((r) => sectionOf(r) != null).length
|
|
@@ -127,11 +66,8 @@ export class PlannedSt {
|
|
|
127
66
|
this.st.select(repo)
|
|
128
67
|
}
|
|
129
68
|
|
|
130
|
-
/** the check: its own page in its repo. The selection is written before
|
|
131
|
-
* the repo is shown, so the one history entry already names the check */
|
|
132
69
|
openCheck(repo: string, check: string): void {
|
|
133
|
-
this.st.
|
|
134
|
-
this.st.select(repo)
|
|
70
|
+
this.st.openCheck(repo, check)
|
|
135
71
|
}
|
|
136
72
|
|
|
137
73
|
/** the check's title as its repo's hub names it, once that hub was read */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// `/planned`: every check the machine runs on its own time, in ONE list
|
|
2
|
-
// ordered by time under
|
|
2
|
+
// ordered by time under two small labels (now, next), then one quiet line
|
|
3
|
+
// counting the checks that wait for their repo to be active, which opens on a
|
|
4
|
+
// click. A row says
|
|
3
5
|
// when, which project and check, and one mark; everything else (the rule,
|
|
4
6
|
// the reason, the last result, the inactive policy, an error, run now) is in
|
|
5
7
|
// the card that floats over the row on hover. The project opens its repo in
|
|
@@ -8,16 +10,11 @@ import { observer } from 'mobx-react-lite'
|
|
|
8
10
|
import type { CSSProperties, MouseEvent, ReactNode } from 'react'
|
|
9
11
|
import type { PlanRow } from '../../plan/planner.ts'
|
|
10
12
|
import { Tip } from './floating.tsx'
|
|
11
|
-
import { hashFor } from './hash-route.ts'
|
|
12
13
|
import type { HomeSt } from './home-st.ts'
|
|
13
|
-
import { plainClick, repoPage } from './home-st.ts'
|
|
14
|
+
import { checkAddress, plainClick, repoPage } from './home-st.ts'
|
|
14
15
|
import { LAST_TONE, markOf, reasonWords, whenWords, type Section } from './planned-st.ts'
|
|
15
16
|
import { ageWords, stampWords } from './time-ago.tsx'
|
|
16
17
|
|
|
17
|
-
/** where a check's own page answers on the home */
|
|
18
|
-
export const checkAddress = (repo: string, check: string): string =>
|
|
19
|
-
`${repoPage(repo)}#${hashFor({ t: 'item', col: 'checks', id: `check:${check}` })}`
|
|
20
|
-
|
|
21
18
|
/** a link that works as one (a new tab on cmd+click) and moves in place on a plain click */
|
|
22
19
|
const InPlace = ({ href, className, go, children }: { href: string; className: string; go: () => void; children: ReactNode }) => (
|
|
23
20
|
<a className={className} href={href} onClick={(e: MouseEvent) => {
|
|
@@ -94,6 +91,7 @@ export const PlannedView = observer(({ app }: { app: HomeSt }) => {
|
|
|
94
91
|
<div className="hall planned">
|
|
95
92
|
<input className="plfind" type="search" placeholder="filter" aria-label="filter by project or check" value={st.query} onChange={(e) => st.setQuery(e.currentTarget.value)} />
|
|
96
93
|
{st.error !== '' ? <div className="hnote">{st.error}</div> : null}
|
|
94
|
+
{(d?.held ?? []).map((h) => <div key={h} className="pldim">{h}</div>)}
|
|
97
95
|
{(d?.errors ?? []).map((e) => <div key={e.repo} className="hnote">{e.repo}: {e.error}</div>)}
|
|
98
96
|
<div className="pllist">
|
|
99
97
|
{st.rows.flatMap(({ section, row }) => {
|
|
@@ -104,6 +102,13 @@ export const PlannedView = observer(({ app }: { app: HomeSt }) => {
|
|
|
104
102
|
})}
|
|
105
103
|
{d == null ? <div className="pldim">loading…</div> : st.rows.length === 0 ? <div className="pldim">{st.timed === 0 ? 'nothing is planned' : 'nothing matches'}</div> : null}
|
|
106
104
|
</div>
|
|
105
|
+
{st.waitingLine !== '' ? (
|
|
106
|
+
<div className="plwait">
|
|
107
|
+
<button className="plwaitline" aria-expanded={st.waitingOpen} onClick={() => st.toggleWaiting()}>{st.waitingOpen ? '▾' : '▸'} {st.waitingLine}</button>
|
|
108
|
+
{st.waitingOpen ? <div className="pllist">{st.waiting.map((r) => <Row key={`${r.repo}\0${r.check}`} app={app} r={r} now={now} />)}</div> : null}
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
<p className="hdim">in the console: shipkit planned</p>
|
|
107
112
|
</div>
|
|
108
113
|
)
|
|
109
114
|
})
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
.plr{display:grid;grid-template-columns:64px minmax(0,1fr) auto;align-items:center;column-gap:var(--s3);padding:var(--s0) var(--s2);border-radius:3px;white-space:nowrap}
|
|
12
12
|
.plr:hover{background:var(--panel)}
|
|
13
13
|
.plwhen{color:var(--dim);font-variant-numeric:tabular-nums}
|
|
14
|
-
.m-
|
|
14
|
+
.m-waiting .plwhen{color:var(--dim)}
|
|
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}
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
.plsym{display:inline-block;width:7px;height:7px;border-radius:50%;flex:none;box-sizing:border-box}
|
|
25
25
|
.s-next{border:1px solid var(--dim)}
|
|
26
26
|
.s-due{background:var(--ink)}
|
|
27
|
-
.s-
|
|
27
|
+
.s-waiting{border:1px dashed var(--dim)}
|
|
28
|
+
.plwait{margin-top:var(--s3)}
|
|
29
|
+
.plwaitline{all:unset;cursor:pointer;color:var(--dim);font-size:12px}
|
|
30
|
+
.plwaitline:hover{color:var(--ink)}
|
|
28
31
|
.s-failed{width:auto;height:auto;border-radius:0;font-style:normal;color:var(--red)}
|
|
29
32
|
.s-failed::before{content:'✕'}
|
|
30
33
|
.m-failed .plmark{color:var(--red)}
|
|
@@ -5,19 +5,53 @@
|
|
|
5
5
|
tests/hub-css-invariants.test.ts reads this sheet too. Every icon is 18px
|
|
6
6
|
here AND as attributes on the img (ROW_ICON), so a sheet that fails to
|
|
7
7
|
apply still draws a row, never a 512px icon. */
|
|
8
|
-
.pjlist{flex:1;width:100%;height:100%;display:flex;flex-direction:column;background:var(--bg);min-width:0}
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
.pjlist{flex:1;width:100%;height:100%;display:flex;flex-direction:column;background:var(--bg-side);min-width:0}
|
|
9
|
+
/* the home's global pages: a section of lines in the repo rows' style, then
|
|
10
|
+
a thin divider before the repos */
|
|
11
|
+
.pjhead{flex:none;display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
|
|
11
12
|
/* the rows scroll, the head does not, and no scrollbar draws a band beside
|
|
12
13
|
them: WebKit (the macOS app) ignores scrollbar-width, hence the pseudo */
|
|
14
|
+
.pjfoot{flex:none;display:flex;align-items:center;gap:var(--s1);height:32px;padding:0 var(--s2);border-top:1px solid var(--line)}
|
|
15
|
+
/* the archived rows, just above the foot while its archived button is on:
|
|
16
|
+
the rows panel gives up the room, the foot stays put */
|
|
17
|
+
.pjarch{flex:none;border-top:1px solid var(--line);max-height:40%;overflow-y:auto;scrollbar-width:none;padding:var(--s1) 0}
|
|
18
|
+
.pjarch::-webkit-scrollbar{display:none}
|
|
19
|
+
.pjarow{text-decoration:none;display:flex;align-items:center;gap:var(--s2);width:100%;height:22px;padding:0 var(--s2) 0 var(--s3);border:0;border-radius:0;background:none;color:var(--dim);font-size:12px;text-align:left;cursor:pointer;opacity:.6}
|
|
20
|
+
.pjarow:hover,.pjarow.cur{opacity:1;background:var(--panel2)}
|
|
21
|
+
.pjarow span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
22
|
+
.pjarow img{flex:none;border-radius:3px;object-fit:contain}
|
|
23
|
+
.pjaini{flex:none;width:14px;height:14px;border-radius:3px;background:var(--panel2)}
|
|
24
|
+
/* the foot's two buttons: sort names the order the list is in, archived
|
|
25
|
+
carries its count and stays lit while its rows show */
|
|
26
|
+
.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;
|
|
27
|
+
color:var(--dim);font-size:11px;cursor:pointer}
|
|
28
|
+
.pgsort{flex:0 1 auto;min-width:0}
|
|
29
|
+
.pgsortl{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
30
|
+
.pgsort .hico,.pjarbtn .hico{flex:none;width:14px;height:14px}
|
|
31
|
+
.pgsort:hover,.pjarbtn:hover{color:var(--ink);border-color:var(--accent)}
|
|
32
|
+
.pjarbtn.on{color:var(--ink);background:var(--sel);border-color:var(--accent)}
|
|
33
|
+
.pjarbtn span{font-variant-numeric:tabular-nums}
|
|
34
|
+
/* the sort menu: over the foot, placed by floating-ui (floating.tsx) */
|
|
35
|
+
.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}
|
|
36
|
+
.pgmhead{padding:var(--s1) var(--s2);color:var(--dim);font-size:11px}
|
|
37
|
+
.pgmi{display:flex;align-items:center;gap:var(--s1);width:100%;height:24px;padding:0 var(--s2);background:none;border:0;border-radius:0;text-align:left;color:var(--ink);font-size:12px;cursor:pointer}
|
|
38
|
+
.pgmi:hover:not(:disabled){background:var(--panel2)}
|
|
39
|
+
.pgmi.on{font-weight:600}
|
|
40
|
+
.pgmi.manual{color:var(--dim);cursor:default;border-top:1px solid var(--line)}
|
|
41
|
+
.pgmi small{margin-left:auto;color:var(--dim);font-size:10px;font-weight:400}
|
|
42
|
+
.pgmck{flex:none;width:12px;color:var(--accent)}
|
|
13
43
|
.pjrows{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:var(--s1) 0;scrollbar-width:none}
|
|
14
44
|
.pjrows::-webkit-scrollbar{display:none}
|
|
15
45
|
.pjwait{display:flex;align-items:center;gap:var(--s2);padding:var(--s1) var(--s3);font-size:12px;color:var(--dim)}
|
|
16
|
-
.pjrow{position:relative;display:flex;align-items:center;gap:var(--s2);width:100%;height:28px;padding:0 var(--s3);
|
|
46
|
+
.pjrow,.pjpl{position:relative;display:flex;align-items:center;gap:var(--s2);width:100%;height:28px;padding:0 var(--s3);
|
|
17
47
|
background:transparent;border:0;border-radius:0;text-align:left;color:var(--dim);font-size:13px;cursor:pointer}
|
|
18
|
-
.pjrow:hover{background:var(--panel2);color:var(--ink)}
|
|
19
|
-
/* the shown repo: its row lit, marked on the left in its identity colour
|
|
20
|
-
|
|
48
|
+
.pjrow:hover,.pjpl:hover{background:var(--panel2);color:var(--ink)}
|
|
49
|
+
/* the shown repo: its row lit, marked on the left in its identity colour
|
|
50
|
+
(a global page's line: in the accent) */
|
|
51
|
+
.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}
|
|
52
|
+
.pjpl .hico{flex:none;width:18px;height:18px;color:inherit}
|
|
53
|
+
/* the selected repo while a global page is up: still marked, dimmer */
|
|
54
|
+
.pjrow.cur.away{background:none;font-weight:500}
|
|
21
55
|
.pjrow img,.pjrow .pgi{flex:none;width:18px;height:18px;border-radius:4px;object-fit:contain;pointer-events:none}
|
|
22
56
|
.pjname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
23
57
|
/* the initial on a hue hashed from the name (projects-svc hueOf) */
|
|
@@ -34,7 +68,7 @@
|
|
|
34
68
|
.pjrow.dim img,.pjrow.dim .pgi,.pjrow.dim .pjname{opacity:.5}
|
|
35
69
|
/* ⌘1 to ⌘9 select the first nine rows (keys.ts): the hint sits at the row's end */
|
|
36
70
|
.pjkey{flex:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
|
|
37
|
-
.pjrow:hover .pjkey,.pjrow.cur .pjkey{opacity:1}
|
|
71
|
+
.pjrow:hover .pjkey,.pjrow.cur .pjkey,.pjpl:hover .pjkey,.pjpl.cur .pjkey{opacity:1}
|
|
38
72
|
/* the one badge: open asks, at the end of the row */
|
|
39
73
|
.pga{flex:none;min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;
|
|
40
74
|
font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg);background:var(--warn)}
|
|
@@ -70,19 +104,7 @@
|
|
|
70
104
|
.pgcfacts .wk{flex-basis:64px;white-space:nowrap}
|
|
71
105
|
.pgcfacts .wv{text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;word-break:normal}
|
|
72
106
|
.pgcfacts .wv.warn{color:var(--warn)}
|
|
73
|
-
.pgerr{font-size:12px;font-weight:700;color:var(--red);cursor:help}
|
|
74
|
-
/* the home only: the overview and the plan, one button each, the page shown
|
|
75
|
-
marked, its shortcut small in the bottom right corner */
|
|
76
|
-
.pgplace{position:relative;flex:none;width:40px;height:26px;display:flex;align-items:center;justify-content:center;padding:0;
|
|
77
|
-
background:transparent;border:1px solid var(--line);border-radius:4px;color:var(--dim);cursor:pointer}
|
|
78
|
-
.pgplace:hover{color:var(--ink);background:var(--panel2)}
|
|
79
|
-
.pgplace.on{color:var(--accent);background:var(--sel);border-color:var(--accent)}
|
|
80
|
-
.pgplace .hico{flex:none;color:inherit;margin-right:var(--s2)}
|
|
81
|
-
.pgplace kbd{position:absolute;right:var(--s0);bottom:0;font:8px/1 ui-monospace,Menlo,monospace;color:var(--dim);background:none;border:0;padding:0}
|
|
82
|
-
/* the one time sort by last activity */
|
|
83
|
-
.pgsort{flex:none;width:28px;height:20px;padding:0;background:transparent;border:1px solid var(--line);border-radius:3px;
|
|
84
|
-
color:var(--dim);font-size:12px;line-height:18px;cursor:pointer}
|
|
85
|
-
.pgsort:hover{color:var(--ink);border-color:var(--accent)}
|
|
107
|
+
.pgerr{padding:0 var(--s3);font-size:12px;font-weight:700;color:var(--red);cursor:help;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
86
108
|
/* a drag: the row being moved fades, and the gap it would land in is a line
|
|
87
109
|
drawn on a row edge */
|
|
88
110
|
.pjrow.dragging{opacity:.4}
|
|
@@ -9,24 +9,33 @@
|
|
|
9
9
|
// else is the hover card, placed by floating-ui beside the row. A click
|
|
10
10
|
// selects the repo in place (so does ⌘1 to ⌘9 for the first nine rows, the
|
|
11
11
|
// hint at each row's end), a double click opens the folder in VS Code, a
|
|
12
|
-
// drag reorders the list
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
12
|
+
// drag reorders the list. The foot holds two buttons: sort opens a menu of
|
|
13
|
+
// labeled sorts (a pick sorts once, the button names the order the list is
|
|
14
|
+
// in), archived shows or hides the archived repos just above the foot
|
|
15
|
+
// (names only, no ⌘ number, a row opens the archived page, nothing loads),
|
|
16
|
+
// the foot never moving. On the home a section of four lines on top, in the
|
|
17
|
+
// repo rows' style, opens the picker, the overview, the activity and the
|
|
18
|
+
// plan, each with its shortcut at the line's end, the page shown lit. It
|
|
19
|
+
// renders from the first paint with the list this browser kept, so a load
|
|
20
|
+
// moves nothing.
|
|
21
|
+
import { autoUpdate, FloatingPortal, useDismiss, useFloating, useHover, useInteractions, useRole } from '@floating-ui/react'
|
|
17
22
|
import { observer } from 'mobx-react-lite'
|
|
18
23
|
import { useEffect, type CSSProperties } from 'react'
|
|
19
24
|
import { middleware, OPEN_MS } from './floating.tsx'
|
|
20
25
|
import { Icon, ICON } from './icons.tsx'
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
26
|
+
import { plainClick } from './home-st.ts'
|
|
27
|
+
import { PICKER_KEY, PLACE_KEY } from './keys.ts'
|
|
28
|
+
import { cardModel, faceOf, hueOf, SORTS, type Place, type Project, type ProjectsSvc } from './projects-svc.ts'
|
|
23
29
|
|
|
24
30
|
/** what the list needs of its page: the hub and the home both carry one */
|
|
25
31
|
export type ListHost = { projects: ProjectsSvc }
|
|
26
32
|
|
|
27
|
-
/** the
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
/** the lines on top of the list, in their order: the picker, the overview,
|
|
34
|
+
* then the places, their shortcuts ⌘J ⌘K ⌘L ⌘' top to bottom */
|
|
35
|
+
const PLACES: { id: Place | 'picker'; label: string; title: string }[] = [
|
|
36
|
+
{ id: 'picker', label: 'jump to…', title: 'jump to a page, a repo or one of its resources' },
|
|
37
|
+
{ id: 'overview', label: 'overview', title: 'the overview: shipkit, every global process and every repo on this machine' },
|
|
38
|
+
{ id: 'activity', label: 'activity', title: 'what every repo committed lately, what it holds uncommitted, its branch and whether it is pushed' },
|
|
30
39
|
{ id: 'planned', label: 'planned', title: 'every check the machine runs on its own: now, next, late, and why' },
|
|
31
40
|
]
|
|
32
41
|
|
|
@@ -51,7 +60,7 @@ export const Row = observer(({ app, p, i }: { app: ListHost; p: Project; i: numb
|
|
|
51
60
|
// the drop line rides on a row edge, so showing it moves nothing
|
|
52
61
|
const edge = gap === i ? ' dropb' : gap === i + 1 && i === s.list.length - 1 ? ' dropa' : ''
|
|
53
62
|
const cur = s.isCurrent(p)
|
|
54
|
-
const cls = `pjrow${cur ? ' cur' : ''}${face.dim ? ' dim' : ''}${s.drag === p.name ? ' dragging' : ''}${edge}`
|
|
63
|
+
const cls = `pjrow${cur ? ' cur' : ''}${cur && s.away ? ' away' : ''}${face.dim ? ' dim' : ''}${s.drag === p.name ? ' dragging' : ''}${edge}`
|
|
55
64
|
const dot = s.dotOf(p.name)
|
|
56
65
|
const warn = s.warnOf(p.name)
|
|
57
66
|
// the shown repo's row is marked in its own colour
|
|
@@ -100,7 +109,7 @@ export const Row = observer(({ app, p, i }: { app: ListHost; p: Project; i: numb
|
|
|
100
109
|
|
|
101
110
|
// the img carries its size as attributes too: an icon file is 512px, and
|
|
102
111
|
// without them a sheet that fails to apply draws it at that size
|
|
103
|
-
const Face = ({ p, px }: { p: Project; px: number }) => (p.icon != null
|
|
112
|
+
export const Face = ({ p, px }: { p: Project; px: number }) => (p.icon != null
|
|
104
113
|
? <img src={p.icon} alt="" width={px} height={px} draggable={false} />
|
|
105
114
|
: <span className={`pgi h${hueOf(p.name)}`}>{(p.name.match(/[a-zA-Z]/)?.[0] ?? p.name.charAt(0)).toUpperCase()}</span>)
|
|
106
115
|
|
|
@@ -147,21 +156,111 @@ export const ProjectList = observer(({ app }: { app: ListHost }) => {
|
|
|
147
156
|
const s = app.projects
|
|
148
157
|
return (
|
|
149
158
|
<nav className="pjlist" aria-label="projects">
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
{s.onPlace == null && s.onPicker == null && s.error === '' ? null : (
|
|
160
|
+
<div className="pjhead">
|
|
161
|
+
{PLACES.map((pl) => {
|
|
162
|
+
const id = pl.id
|
|
163
|
+
const run = id === 'picker' ? s.onPicker : s.onPlace == null ? null : () => s.onPlace?.(id)
|
|
164
|
+
if (run == null) return null
|
|
165
|
+
const on = id === 'picker' ? s.pickerOpen() : s.place === id
|
|
166
|
+
const key = id === 'picker' ? PICKER_KEY : PLACE_KEY[id]
|
|
167
|
+
return (
|
|
168
|
+
<button key={id} className={`pjpl${on ? ' cur' : ''}`} aria-label={pl.label} title={`${pl.title} (${key})`} onClick={run}>
|
|
169
|
+
<Icon d={ICON[id === 'picker' ? 'search' : id === 'overview' ? 'home' : id]} />
|
|
170
|
+
<span className="pjname">{pl.label}</span>
|
|
171
|
+
<kbd className="pjkey">{key}</kbd>
|
|
172
|
+
</button>
|
|
173
|
+
)
|
|
174
|
+
})}
|
|
175
|
+
{s.error !== '' ? <span className="pgerr" title={s.error}>! {s.error}</span> : null}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
161
178
|
<div className="pjrows">
|
|
162
179
|
{s.list.length === 0 && s.error === '' ? <span className="pjwait"><span className="spin" />reading the repos…</span> : null}
|
|
163
180
|
{s.list.map((p, i) => <Row key={p.name} app={app} p={p} i={i} />)}
|
|
164
181
|
</div>
|
|
182
|
+
{s.archivedOpen && s.archived.length > 0 && s.onArchived != null ? (
|
|
183
|
+
<div className="pjarch">
|
|
184
|
+
{s.archived.map((p) => (
|
|
185
|
+
<a key={p.name} className="pjarow" href="/archived" title={`${p.name} is archived: nothing runs there`} onClick={(e) => {
|
|
186
|
+
if (!plainClick(e)) return
|
|
187
|
+
e.preventDefault()
|
|
188
|
+
s.onArchived?.()
|
|
189
|
+
}}>
|
|
190
|
+
{p.icon != null ? <img src={p.icon} alt="" width={14} height={14} draggable={false} /> : <i className="pjaini" />}
|
|
191
|
+
<span>{p.name}</span>
|
|
192
|
+
</a>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
<div className="pjfoot">
|
|
197
|
+
<SortButton s={s} />
|
|
198
|
+
{s.archived.length === 0 || s.onArchived == null ? null : (
|
|
199
|
+
<button className={`pjarbtn${s.archivedOpen ? ' on' : ''}`} aria-pressed={s.archivedOpen}
|
|
200
|
+
title={`${s.archivedOpen ? 'hide' : 'show'} the ${s.archived.length} archived repos: nothing runs there, a row opens the archived page`}
|
|
201
|
+
onClick={() => s.toggleArchivedOpen()}>
|
|
202
|
+
<Icon d={ICON.archived} /><span>{s.archived.length}</span>
|
|
203
|
+
</button>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
165
206
|
</nav>
|
|
166
207
|
)
|
|
167
208
|
})
|
|
209
|
+
|
|
210
|
+
/** the words for the order the list is in */
|
|
211
|
+
const orderWords = (s: ProjectsSvc): string => {
|
|
212
|
+
const m = s.sortMode
|
|
213
|
+
return m === 'manual' ? 'manual order' : SORTS.find((x) => x.id === m)!.label
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** the sort button: it names the order the list is in and opens the menu,
|
|
217
|
+
* and sorts nothing itself */
|
|
218
|
+
const SortButton = observer(({ s }: { s: ProjectsSvc }) => {
|
|
219
|
+
const f = useFloating({
|
|
220
|
+
open: s.sortMenuOpen,
|
|
221
|
+
onOpenChange: (v) => s.setSortMenu(v),
|
|
222
|
+
placement: 'top-start',
|
|
223
|
+
strategy: 'fixed',
|
|
224
|
+
middleware: middleware(6),
|
|
225
|
+
whileElementsMounted: autoUpdate,
|
|
226
|
+
})
|
|
227
|
+
const dismiss = useDismiss(f.context)
|
|
228
|
+
const role = useRole(f.context, { role: 'menu' })
|
|
229
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, role])
|
|
230
|
+
return (
|
|
231
|
+
<>
|
|
232
|
+
<button ref={f.refs.setReference} className="pgsort" title={`the list is in ${orderWords(s)}: pick a sort`}
|
|
233
|
+
{...getReferenceProps({ onClick: () => s.toggleSortMenu() })}>
|
|
234
|
+
<Icon d={ICON.sort} /><span className="pgsortl">{orderWords(s)}</span>
|
|
235
|
+
</button>
|
|
236
|
+
{s.sortMenuOpen ? (
|
|
237
|
+
<FloatingPortal>
|
|
238
|
+
<div ref={f.refs.setFloating} className="pgmenu" style={f.floatingStyles} {...getFloatingProps()}>
|
|
239
|
+
<SortMenu s={s} />
|
|
240
|
+
</div>
|
|
241
|
+
</FloatingPortal>
|
|
242
|
+
) : null}
|
|
243
|
+
</>
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
/** the menu's lines: one per sort, the order the list is in checked. The
|
|
248
|
+
* manual order is a state a drag makes, never a pick */
|
|
249
|
+
export const SortMenu = observer(({ s }: { s: ProjectsSvc }) => {
|
|
250
|
+
const cur = s.sortMode
|
|
251
|
+
return (
|
|
252
|
+
<>
|
|
253
|
+
<div className="pgmhead">sort the list once by</div>
|
|
254
|
+
{SORTS.map((m) => (
|
|
255
|
+
<button key={m.id} role="menuitemradio" aria-checked={cur === m.id} className={`pgmi${cur === m.id ? ' on' : ''}`}
|
|
256
|
+
title={m.title} onClick={() => s.sortBy(m.id)}>
|
|
257
|
+
<span className="pgmck">{cur === m.id ? '✓' : ''}</span>{m.label}
|
|
258
|
+
</button>
|
|
259
|
+
))}
|
|
260
|
+
<button role="menuitemradio" aria-checked={cur === 'manual'} className={`pgmi manual${cur === 'manual' ? ' on' : ''}`} disabled
|
|
261
|
+
title="a drag of a row makes the manual order, and every sort ends in it">
|
|
262
|
+
<span className="pgmck">{cur === 'manual' ? '✓' : ''}</span>manual order<small>drag a row</small>
|
|
263
|
+
</button>
|
|
264
|
+
</>
|
|
265
|
+
)
|
|
266
|
+
})
|