rvlib-shipkit 1.1.1 → 1.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 +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- 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 +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -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 +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -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 +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- 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 +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -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-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/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/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- 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 +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/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 +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// THE `<name>.shipkit.ts` file: a repo drops one anywhere and default-exports
|
|
2
|
+
// what it declares, and the kind says who reads it.
|
|
3
|
+
//
|
|
4
|
+
// check({...}) a gate check, read by discovery like a `.check.ts`
|
|
5
|
+
// globalProcess({...}) a long running process for the whole machine: pm
|
|
6
|
+
// supervises it, the home page switches it
|
|
7
|
+
// widget({...}) html the daemon renders every few seconds, mounted
|
|
8
|
+
// on the home page
|
|
9
|
+
//
|
|
10
|
+
// the one constraint a reader breaks first: every kind is IMPORTED by the
|
|
11
|
+
// process that reads it (discovery, pm's config child, the daemon), so a file
|
|
12
|
+
// keeps its top level cheap and puts heavy work inside a function.
|
|
13
|
+
import { check, type CheckDef } from './check.ts'
|
|
14
|
+
|
|
15
|
+
export { check }
|
|
16
|
+
|
|
17
|
+
/** one button beside a global process: a command, or a function of the file */
|
|
18
|
+
export type ProcessAction = {
|
|
19
|
+
/** the words on the button */
|
|
20
|
+
label: string
|
|
21
|
+
/** argv, run from the repo root, output appended to the process log */
|
|
22
|
+
cmd?: string[]
|
|
23
|
+
/** a function run in its own bun process that imports this file, from
|
|
24
|
+
* the repo root. Its output lands in the process log too */
|
|
25
|
+
run?: () => void | Promise<void>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type GlobalProcessDef = {
|
|
29
|
+
/** kebab-case, unique on the machine: what a repo names in `serve.uses` */
|
|
30
|
+
id: string
|
|
31
|
+
/** the name on the switch (default the id) */
|
|
32
|
+
label?: string
|
|
33
|
+
/** one line: what it is for */
|
|
34
|
+
description?: string
|
|
35
|
+
/** the machine it runs on, as a hint beside the switch */
|
|
36
|
+
where?: string
|
|
37
|
+
/** argv, run from the repo root (or `cwd`). Without `down` it IS the
|
|
38
|
+
* service: pm keeps it running, restarts it on a crash, signals it on
|
|
39
|
+
* stop. With `down` it is a bring-up that exits once the service answers */
|
|
40
|
+
cmd: string[]
|
|
41
|
+
/** argv that takes a bring-up down again. Absent: `cmd` is the service */
|
|
42
|
+
down?: string[]
|
|
43
|
+
/** where both run, repo-relative (default the repo root) */
|
|
44
|
+
cwd?: string
|
|
45
|
+
/** the url it serves: shown as a link, probed when `probe` is absent */
|
|
46
|
+
url?: string
|
|
47
|
+
/** an http url that answers 2xx while it is up (default `url`). A
|
|
48
|
+
* bring-up needs one of the two: its probe is the only truth */
|
|
49
|
+
probe?: string
|
|
50
|
+
/** a button each beside the switch, by key */
|
|
51
|
+
actions?: Record<string, ProcessAction>
|
|
52
|
+
/** what its card wears: an emoji, or a repo-relative image path
|
|
53
|
+
* (png, svg, ico, jpg, webp). Absent: the declaring repo's own icon */
|
|
54
|
+
icon?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type WidgetDef = {
|
|
58
|
+
/** kebab-case, unique in its repo */
|
|
59
|
+
id: string
|
|
60
|
+
/** the heading over it (default the id) */
|
|
61
|
+
title?: string
|
|
62
|
+
/** how often the daemon renders it again (default 5000, at least 1000) */
|
|
63
|
+
everyMs?: number
|
|
64
|
+
/** the html it shows, rendered in the daemon. A throw shows as its error */
|
|
65
|
+
render: () => string | Promise<string>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type GlobalProcessUnit = GlobalProcessDef & { kind: 'globalProcess' }
|
|
69
|
+
export type WidgetUnit = WidgetDef & { kind: 'widget' }
|
|
70
|
+
|
|
71
|
+
export const UNIT_KINDS = ['globalProcess', 'widget'] as const
|
|
72
|
+
|
|
73
|
+
const KEBAB = /^[a-z0-9][a-z0-9-]*$/
|
|
74
|
+
const words = (v: unknown): boolean => Array.isArray(v) && v.length > 0 && v.every((w) => typeof w === 'string')
|
|
75
|
+
const httpUrl = (v: unknown): boolean => typeof v === 'string' && /^https?:\/\//.test(v)
|
|
76
|
+
|
|
77
|
+
/** the image types an icon file can be, by extension */
|
|
78
|
+
export const ICON_IMAGE_TYPES: Record<string, string> = { png: 'image/png', svg: 'image/svg+xml', ico: 'image/x-icon', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp' }
|
|
79
|
+
|
|
80
|
+
/** the image an icon names (its extension), or null when it is text */
|
|
81
|
+
export const iconImageType = (icon: string): string | null => ICON_IMAGE_TYPES[/\.([a-z]+)$/i.exec(icon)?.[1]?.toLowerCase() ?? ''] ?? null
|
|
82
|
+
|
|
83
|
+
function iconProblem(icon: unknown): string | null {
|
|
84
|
+
if (typeof icon !== 'string' || icon.trim() === '') return 'icon is an emoji or a repo relative image path'
|
|
85
|
+
if (iconImageType(icon) != null) return icon.startsWith('/') || icon.split(/[\\/]/).includes('..') ? `icon '${icon}' must be a repo relative path` : null
|
|
86
|
+
if (/[\\/.]/.test(icon) || [...icon].length > 8) return `icon '${icon}' is neither an emoji nor an image path (${Object.keys(ICON_IMAGE_TYPES).join(', ')})`
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** why a global process declaration cannot run, or null */
|
|
91
|
+
export function globalProcessProblem(d: GlobalProcessDef): string | null {
|
|
92
|
+
const at = `globalProcess '${String(d.id)}'`
|
|
93
|
+
if (!KEBAB.test(d.id ?? '')) return `${at}: id must be kebab-case`
|
|
94
|
+
if (!words(d.cmd)) return `${at}: cmd is a list of words`
|
|
95
|
+
if (d.down != null && !words(d.down)) return `${at}: down is a list of words`
|
|
96
|
+
if (d.url != null && !httpUrl(d.url)) return `${at}: url must be an http url`
|
|
97
|
+
if (d.probe != null && !httpUrl(d.probe)) return `${at}: probe must be an http url`
|
|
98
|
+
if (d.down != null && d.probe == null && d.url == null) return `${at}: a bring-up (one with down) needs probe or url, the only truth of whether it is up`
|
|
99
|
+
if (d.icon !== undefined) {
|
|
100
|
+
const why = iconProblem(d.icon)
|
|
101
|
+
if (why != null) return `${at}: ${why}`
|
|
102
|
+
}
|
|
103
|
+
for (const [k, a] of Object.entries(d.actions ?? {})) {
|
|
104
|
+
if (!KEBAB.test(k)) return `${at}: action '${k}' must be kebab-case`
|
|
105
|
+
if ((a.cmd != null) === (a.run != null)) return `${at}: action '${k}' declares exactly one of cmd or run`
|
|
106
|
+
if (a.cmd != null && !words(a.cmd)) return `${at}: action '${k}'.cmd is a list of words`
|
|
107
|
+
}
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function globalProcess(d: GlobalProcessDef): GlobalProcessUnit {
|
|
112
|
+
const why = globalProcessProblem(d)
|
|
113
|
+
if (why != null) throw new Error(why)
|
|
114
|
+
return { ...d, kind: 'globalProcess' }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function widget(d: WidgetDef): WidgetUnit {
|
|
118
|
+
if (!KEBAB.test(d.id ?? '')) throw new Error(`widget '${String(d.id)}': id must be kebab-case`)
|
|
119
|
+
if (typeof d.render !== 'function') throw new Error(`widget '${d.id}': render is a function returning html`)
|
|
120
|
+
if (d.everyMs != null && !(d.everyMs >= 1000)) throw new Error(`widget '${d.id}': everyMs is at least 1000`)
|
|
121
|
+
return { ...d, kind: 'widget' }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** the kind a default export says it is: a unit, else a check */
|
|
125
|
+
export function unitKind(d: unknown): 'globalProcess' | 'widget' | 'check' {
|
|
126
|
+
const k = (d as { kind?: unknown } | null)?.kind
|
|
127
|
+
return k === 'globalProcess' || k === 'widget' ? k : 'check'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type { CheckDef }
|
package/src/shipkit.ts
CHANGED
|
@@ -15,7 +15,8 @@ export { contractsConfigFrom, activeContracts, applyContractsResult } from './co
|
|
|
15
15
|
export { serveHub, hubTools } from './serve/serve-hub.ts'
|
|
16
16
|
export type { Hub, HubDeps, HubTool } from './serve/serve-hub.ts'
|
|
17
17
|
export type { GraphMount } from './wiki/graph-mount.ts'
|
|
18
|
-
export { runServe
|
|
18
|
+
export { runServe } from './serve/run-serve.ts'
|
|
19
|
+
export { resolveHubDeps, defaultHubPort, baseWikiRoot, loadHub } from './serve/hub-runtime.ts'
|
|
19
20
|
export { projectNeeds, redCount } from './collections/collection-types.ts'
|
|
20
21
|
export type { Collection, Instance, NeedItem, NeedSeverity, CurationVerb } from './collections/collection-types.ts'
|
|
21
22
|
export { buildCollections } from './collections/providers.ts'
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { copyFileSync, mkdirSync, readFileSync } from 'fs'
|
|
16
16
|
import { dirname, join } from 'path'
|
|
17
17
|
import { atomicWrite } from '../core/locked-write.ts'
|
|
18
|
-
import { shipkitRoot } from '../ledger/
|
|
18
|
+
import { shipkitRoot } from '../ledger/db.ts'
|
|
19
19
|
|
|
20
20
|
/** where a unit's durations live, beside everything else the repo remembers */
|
|
21
21
|
export const timingsHome = (root: string, unit: string): string => join(shipkitRoot(root), 'suite-timings', `${unit === '' ? 'root' : unit}.json`)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { mkdirSync, readFileSync } from 'fs'
|
|
5
5
|
import { dirname, join } from 'path'
|
|
6
6
|
import { atomicWrite } from '../core/locked-write.ts'
|
|
7
|
-
import { shipkitRoot } from '../ledger/
|
|
7
|
+
import { shipkitRoot } from '../ledger/db.ts'
|
|
8
8
|
import type { MdGraph } from './md-graph.ts'
|
|
9
9
|
|
|
10
10
|
export type Positions = Record<string, [number, number]>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// the contracts check — `.shipkit/*` release-contract fulfillness. It shares ONE
|
|
2
|
-
// evaluation with check SK002 (the `contractsResult` scan), and it tags with
|
|
3
|
-
// that id, because the invariant has one owner and two surfaces report it. A
|
|
4
|
-
// venue deploy step deps on it BY NAME, which needs nothing from this end.
|
|
5
|
-
import { applyContractsResult, contractsResult } from '../contracts/apply-contracts.ts'
|
|
6
|
-
import { check } from '../check.ts'
|
|
7
|
-
import { contractsFulfillness } from './SK002-contracts-fulfillness.check.ts'
|
|
8
|
-
|
|
9
|
-
export const contractsCheck = check({
|
|
10
|
-
id: 'contracts',
|
|
11
|
-
title: 'contracts',
|
|
12
|
-
description: 'every declared venue has the `.shipkit/*` release contract it demands',
|
|
13
|
-
owner: 'meta/contracts.md',
|
|
14
|
-
blocking: true,
|
|
15
|
-
run: async (c) => {
|
|
16
|
-
applyContractsResult(c, await c.use(contractsResult), contractsFulfillness)
|
|
17
|
-
},
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
export default contractsCheck
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
// THE HEALTH PASS TIMER. While the hub serves, the whole health pass runs once
|
|
2
|
-
// a day, and once at startup when the last whole pass on record is older than
|
|
3
|
-
// that. One timer for the repo, no per check schedule: a check's own `ttl` is
|
|
4
|
-
// its cadence, and this pass is only the heartbeat that replays an answer
|
|
5
|
-
// still fresh and re-measures one that expired.
|
|
6
|
-
//
|
|
7
|
-
// The run is the hub's own in process runner, the same path as the button, so
|
|
8
|
-
// its rows land in the same run record. "When was the last pass" is read off
|
|
9
|
-
// that record, which is what makes a `shipkit check` in a terminal count too.
|
|
10
|
-
|
|
11
|
-
/** how often the health pass runs while the hub serves */
|
|
12
|
-
export const HEALTH_EVERY_MS = 24 * 60 * 60 * 1000
|
|
13
|
-
|
|
14
|
-
/** how often the timer looks. The pass itself is daily, this only bounds how
|
|
15
|
-
* late after its due time it starts */
|
|
16
|
-
export const SCHEDULE_LOOK_MS = 5 * 60 * 1000
|
|
17
|
-
|
|
18
|
-
export type ScheduleState = {
|
|
19
|
-
everyMs: number
|
|
20
|
-
/** when the last whole health pass on record started, null = none */
|
|
21
|
-
lastPassAt: number | null
|
|
22
|
-
/** when the next one starts: now when it is overdue */
|
|
23
|
-
nextAt: number
|
|
24
|
-
/** the timer's own pass is running right now */
|
|
25
|
-
running: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type ScheduleDeps = {
|
|
29
|
-
now: () => number
|
|
30
|
-
/** the last whole health pass on record, whoever ran it */
|
|
31
|
-
lastPassAt: () => number | null
|
|
32
|
-
/** another run in this process is going: the timer never stacks on it */
|
|
33
|
-
busy: () => boolean
|
|
34
|
-
/** the pass itself */
|
|
35
|
-
run: () => Promise<void>
|
|
36
|
-
everyMs?: number
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type TickResult = 'ran' | 'fresh' | 'busy'
|
|
40
|
-
|
|
41
|
-
export class HealthSchedule {
|
|
42
|
-
readonly everyMs: number
|
|
43
|
-
running = false
|
|
44
|
-
/** when this timer last STARTED a pass. A pass that dies before it writes
|
|
45
|
-
* a record would otherwise be due again at the very next look */
|
|
46
|
-
private attemptedAt: number | null = null
|
|
47
|
-
private timer: ReturnType<typeof setInterval> | null = null
|
|
48
|
-
|
|
49
|
-
constructor(private readonly d: ScheduleDeps) {
|
|
50
|
-
this.everyMs = d.everyMs ?? HEALTH_EVERY_MS
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** the newest of the record and this timer's own last attempt */
|
|
54
|
-
private lastAt(): number | null {
|
|
55
|
-
const rec = this.d.lastPassAt()
|
|
56
|
-
if (rec == null) return this.attemptedAt
|
|
57
|
-
return this.attemptedAt == null ? rec : Math.max(rec, this.attemptedAt)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
due(): boolean {
|
|
61
|
-
const last = this.lastAt()
|
|
62
|
-
return last == null || this.d.now() - last >= this.everyMs
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
state(): ScheduleState {
|
|
66
|
-
const now = this.d.now()
|
|
67
|
-
const last = this.lastAt()
|
|
68
|
-
return {
|
|
69
|
-
everyMs: this.everyMs,
|
|
70
|
-
lastPassAt: this.d.lastPassAt(),
|
|
71
|
-
nextAt: last == null ? now : Math.max(now, last + this.everyMs),
|
|
72
|
-
running: this.running,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** one look: run the pass when it is due and nothing else is running */
|
|
77
|
-
async tick(): Promise<TickResult> {
|
|
78
|
-
if (this.running || this.d.busy()) return 'busy'
|
|
79
|
-
if (!this.due()) return 'fresh'
|
|
80
|
-
this.running = true
|
|
81
|
-
this.attemptedAt = this.d.now()
|
|
82
|
-
try {
|
|
83
|
-
await this.d.run()
|
|
84
|
-
} finally {
|
|
85
|
-
this.running = false
|
|
86
|
-
}
|
|
87
|
-
return 'ran'
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** look now, then every SCHEDULE_LOOK_MS. A second start is a no-op */
|
|
91
|
-
start(onError: (e: Error) => void, lookMs = SCHEDULE_LOOK_MS): void {
|
|
92
|
-
if (this.timer != null) return
|
|
93
|
-
const look = (): void => void this.tick().catch((e: Error) => onError(e))
|
|
94
|
-
look()
|
|
95
|
-
this.timer = setInterval(look, lookMs)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
stop(): void {
|
|
99
|
-
if (this.timer != null) clearInterval(this.timer)
|
|
100
|
-
this.timer = null
|
|
101
|
-
}
|
|
102
|
-
}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
// HubSt — the ONE root store (build/app-state-tree: one central tree,
|
|
2
|
-
// instantiated once at boot, services with backrefs, exposed as
|
|
3
|
-
// window.app). Owns cross-service state: selection, the severity filter
|
|
4
|
-
// (an ADDRESS: /?f=needs · /?f=red), and the hash router: every page and
|
|
5
|
-
// every tab is a selection, every selection has an address (hash-route.ts),
|
|
6
|
-
// and setSel is the one place both move together.
|
|
7
|
-
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
8
|
-
import { makeAutoObservable } from 'mobx'
|
|
9
|
-
import { ClockSvc } from './clock-svc.ts'
|
|
10
|
-
import { ColsSvc } from './cols-svc.ts'
|
|
11
|
-
import { canonicalSel, currentHash, hashFor, parseHash, selOf, type HashSel } from './hash-route.ts'
|
|
12
|
-
import { ComfySvc } from './comfy-svc.ts'
|
|
13
|
-
import { HealthSvc } from './health-svc.ts'
|
|
14
|
-
import { LiveSvc } from './live-svc.ts'
|
|
15
|
-
import { MediaSvc } from './media-svc.ts'
|
|
16
|
-
import { TimelineSvc } from './timeline-svc.ts'
|
|
17
|
-
import { TreeSvc } from './tree-svc.ts'
|
|
18
|
-
import { ChecksSvc } from './checks-svc.ts'
|
|
19
|
-
import { ProjectsSvc } from './projects-svc.ts'
|
|
20
|
-
import { TestsSvc } from './tests-svc.ts'
|
|
21
|
-
import type { Resource } from './hub-types.ts'
|
|
22
|
-
|
|
23
|
-
export type Sel = HashSel | null
|
|
24
|
-
|
|
25
|
-
/** one cmd+j target — run() fires the SAME action clicking would */
|
|
26
|
-
export type Jump = { id: string; label: string; section: string; run: () => void }
|
|
27
|
-
|
|
28
|
-
/** all · needs (yellow+red) · red — one severity ladder for every section */
|
|
29
|
-
export type Filter = 'all' | 'needs' | 'red'
|
|
30
|
-
const parseFilter = (search: string): Filter => {
|
|
31
|
-
const f = new URLSearchParams(search).get('f')
|
|
32
|
-
return f === 'needs' || f === 'red' ? f : 'all'
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class HubSt {
|
|
36
|
-
name = ''
|
|
37
|
-
/** where this repo sits on disk, from /hub.json: what makes a source
|
|
38
|
-
* path on a page a link that opens his editor */
|
|
39
|
-
root = ''
|
|
40
|
-
/** the one second tick every age on the page reads (clock-svc.ts) */
|
|
41
|
-
clock = new ClockSvc()
|
|
42
|
-
media = new MediaSvc(this)
|
|
43
|
-
cols = new ColsSvc(this)
|
|
44
|
-
tree = new TreeSvc(this)
|
|
45
|
-
health = new HealthSvc(this)
|
|
46
|
-
checks = new ChecksSvc(this)
|
|
47
|
-
comfy = new ComfySvc(this)
|
|
48
|
-
timeline = new TimelineSvc(this)
|
|
49
|
-
/** the runs in flight, whoever started them (live-svc.ts) */
|
|
50
|
-
live = new LiveSvc(this)
|
|
51
|
-
projects = new ProjectsSvc(this)
|
|
52
|
-
/** the pages behind the test groups of the tree (tests-svc.ts) */
|
|
53
|
-
tests = new TestsSvc(this)
|
|
54
|
-
sel: Sel = null
|
|
55
|
-
filter: Filter = parseFilter(location.search)
|
|
56
|
-
/** sidebar search — live substring filter over asset + collection rows */
|
|
57
|
-
query = ''
|
|
58
|
-
palOpen = false
|
|
59
|
-
palQuery = ''
|
|
60
|
-
palIdx = 0
|
|
61
|
-
private booted = false
|
|
62
|
-
|
|
63
|
-
constructor() {
|
|
64
|
-
makeAutoObservable<this, 'booted'>(this, { media: false, cols: false, tree: false, health: false, checks: false, comfy: false, timeline: false, live: false, projects: false, tests: false, booted: false })
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
boot(): void {
|
|
68
|
-
void fetch('/hub.json')
|
|
69
|
-
.then((r) => r.json() as Promise<{ name: string; root?: string }>)
|
|
70
|
-
.then((h) => { this.setHub(h.name, h.root ?? '') })
|
|
71
|
-
.catch(() => {})
|
|
72
|
-
this.clock.start()
|
|
73
|
-
this.media.start()
|
|
74
|
-
this.cols.start()
|
|
75
|
-
this.live.start()
|
|
76
|
-
void this.checks.loadGate()
|
|
77
|
-
void this.health.load()
|
|
78
|
-
// back, forward, a pasted link and a hand edited hash all arrive
|
|
79
|
-
// here; applyHash is idempotent, so the two events firing together
|
|
80
|
-
// for one move cost nothing
|
|
81
|
-
window.addEventListener('hashchange', () => this.applyHash())
|
|
82
|
-
window.addEventListener('popstate', () => this.applyHash())
|
|
83
|
-
this.applyHash()
|
|
84
|
-
}
|
|
85
|
-
private setHub(n: string, root: string): void {
|
|
86
|
-
this.name = n
|
|
87
|
-
this.root = root
|
|
88
|
-
document.title = `${n} · hub`
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
setFilter(v: Filter): void {
|
|
92
|
-
this.filter = v
|
|
93
|
-
const u = new URL(location.href)
|
|
94
|
-
v === 'all' ? u.searchParams.delete('f') : u.searchParams.set('f', v)
|
|
95
|
-
history.replaceState(null, '', u)
|
|
96
|
-
}
|
|
97
|
-
setQuery(v: string): void {
|
|
98
|
-
this.query = v
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── cmd+j palette ─────────────────────────────────────────────────────
|
|
102
|
-
palToggle(): void {
|
|
103
|
-
this.palOpen = !this.palOpen
|
|
104
|
-
this.palQuery = ''
|
|
105
|
-
this.palIdx = 0
|
|
106
|
-
}
|
|
107
|
-
palClose(): void {
|
|
108
|
-
this.palOpen = false
|
|
109
|
-
}
|
|
110
|
-
setPalQuery(v: string): void {
|
|
111
|
-
this.palQuery = v
|
|
112
|
-
this.palIdx = 0
|
|
113
|
-
}
|
|
114
|
-
setPalIdx(n: number): void {
|
|
115
|
-
this.palIdx = n
|
|
116
|
-
}
|
|
117
|
-
palMove(d: number): void {
|
|
118
|
-
const n = this.palResults.length
|
|
119
|
-
if (n > 0) this.palIdx = (this.palIdx + d + n) % n
|
|
120
|
-
}
|
|
121
|
-
palRun(): void {
|
|
122
|
-
const j = this.palResults[this.palIdx]
|
|
123
|
-
if (j == null) return
|
|
124
|
-
this.palOpen = false
|
|
125
|
-
j.run()
|
|
126
|
-
}
|
|
127
|
-
/** every jump target: section titles, media assets, collection instances */
|
|
128
|
-
get jumps(): Jump[] {
|
|
129
|
-
const out: Jump[] = []
|
|
130
|
-
for (const cat of new Set(this.media.assets.map((a) => a.cat)))
|
|
131
|
-
out.push({ id: `cat:${cat}`, label: cat, section: 'media', run: () => this.showCat(cat) })
|
|
132
|
-
for (const a of this.media.assets)
|
|
133
|
-
out.push({ id: `asset:${a.id}`, label: a.label, section: a.cat, run: () => this.selectAsset(a.id) })
|
|
134
|
-
// the runs page is a page, not a resource, so no collection lists it
|
|
135
|
-
out.push({ id: 'page:runs', label: 'runs', section: 'page', run: () => this.cols.select('runs', RUNS_ID) })
|
|
136
|
-
for (const col of this.cols.sidebarCols) {
|
|
137
|
-
out.push({ id: `col:${col.id}`, label: col.title, section: 'section', run: () => this.showCol(col.id) })
|
|
138
|
-
for (const i of col.instances)
|
|
139
|
-
out.push({ id: `item:${i.id}`, label: i.text, section: col.title, run: () => this.cols.select(col.id, i.id) })
|
|
140
|
-
}
|
|
141
|
-
return out
|
|
142
|
-
}
|
|
143
|
-
/** ranked: prefix matches first, then substring; capped at 12 */
|
|
144
|
-
get palResults(): Jump[] {
|
|
145
|
-
const q = this.palQuery.trim().toLowerCase()
|
|
146
|
-
if (q === '') return this.jumps.slice(0, 12)
|
|
147
|
-
const pre: Jump[] = []
|
|
148
|
-
const sub: Jump[] = []
|
|
149
|
-
for (const j of this.jumps) {
|
|
150
|
-
const l = j.label.toLowerCase()
|
|
151
|
-
if (l.startsWith(q)) pre.push(j)
|
|
152
|
-
else if (l.includes(q)) sub.push(j)
|
|
153
|
-
}
|
|
154
|
-
return [...pre, ...sub].slice(0, 12)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── selection + hash deep-links ───────────────────────────────────────
|
|
158
|
-
selectAsset(id: string): void {
|
|
159
|
-
this.setSel({ t: 'asset', id })
|
|
160
|
-
void this.media.select(id)
|
|
161
|
-
}
|
|
162
|
-
showCat(cat: string): void {
|
|
163
|
-
this.setSel({ t: 'cat', cat })
|
|
164
|
-
this.media.openCats.add(cat)
|
|
165
|
-
void this.media.fillCat()
|
|
166
|
-
}
|
|
167
|
-
showStatus(): void {
|
|
168
|
-
this.setSel({ t: 'status' })
|
|
169
|
-
void this.health.load()
|
|
170
|
-
}
|
|
171
|
-
/** where the test checks' time goes: the `tests` group (null) or one test
|
|
172
|
-
* check's group. Fetched as the page opens, never on the poll */
|
|
173
|
-
showTests(check: string | null): void {
|
|
174
|
-
this.setSel({ t: 'tests', check })
|
|
175
|
-
void this.tests.load(check)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** the ONE place a selection is written, so the url always describes what
|
|
179
|
-
* is on screen. A new page is a history entry, so back returns to the
|
|
180
|
-
* page before it. The same address is never pushed twice: applyHash
|
|
181
|
-
* lands here too, with the hash already naming this selection */
|
|
182
|
-
setSel(sel: Sel): void {
|
|
183
|
-
this.sel = sel
|
|
184
|
-
const h = hashFor(sel)
|
|
185
|
-
if (h == null) return
|
|
186
|
-
const cur = parseHash(currentHash())
|
|
187
|
-
const next = parseHash(h)
|
|
188
|
-
// the tool set rides after the asset id (media-svc syncHash), so an
|
|
189
|
-
// address is the same page whatever tools it carries
|
|
190
|
-
if (cur.kind === next.kind && cur.col === next.col && cur.id === next.id) return
|
|
191
|
-
history.pushState(null, '', `#${h}`)
|
|
192
|
-
}
|
|
193
|
-
/** one tree row, opened: an asset goes to the atelier, everything else to
|
|
194
|
-
* its own item card. A gate check has a page of its own now, so nothing
|
|
195
|
-
* lands on the raw wall of text unless it is asked for by name */
|
|
196
|
-
openResource(r: Resource): void {
|
|
197
|
-
const assetId = r.id.replace(/^media:/, '')
|
|
198
|
-
if (r.collection === 'media' && this.media.assets.some((a) => a.id === assetId)) {
|
|
199
|
-
this.selectAsset(assetId)
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
this.cols.select(r.collection, r.id)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** collection view — one card per instance */
|
|
206
|
-
showCol(id: string): void {
|
|
207
|
-
this.setSel({ t: 'col', id })
|
|
208
|
-
}
|
|
209
|
-
/** open whatever a selection names, through the same calls a click makes */
|
|
210
|
-
go(sel: HashSel): void {
|
|
211
|
-
switch (sel.t) {
|
|
212
|
-
case 'asset': return this.selectAsset(sel.id)
|
|
213
|
-
case 'cat': return this.showCat(sel.cat)
|
|
214
|
-
case 'col': return this.showCol(sel.id)
|
|
215
|
-
case 'item': return this.cols.select(sel.col, sel.id)
|
|
216
|
-
case 'status': return this.showStatus()
|
|
217
|
-
case 'tests': return this.showTests(sel.check)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
/** the url decides the page: a reload, back, forward and a pasted link
|
|
221
|
-
* all land here, and only a hash naming something else moves the view */
|
|
222
|
-
applyHash(): void {
|
|
223
|
-
const p = parseHash(currentHash())
|
|
224
|
-
const named = selOf(p)
|
|
225
|
-
if (named == null) return
|
|
226
|
-
// an address naming what this build opens under another address is
|
|
227
|
-
// rewritten in place: a replace, so back never lands on the stale one
|
|
228
|
-
const sel = canonicalSel(named)
|
|
229
|
-
if (sel !== named) history.replaceState(null, '', `#${hashFor(sel)}`)
|
|
230
|
-
// media addresses resolve only once the first heartbeat listed the
|
|
231
|
-
// assets (afterFirstBeat calls back)
|
|
232
|
-
if (sel.t === 'asset' && !this.media.assets.some((a) => a.id === sel.id)) return
|
|
233
|
-
if (sel.t === 'cat' && !this.media.assets.some((a) => a.cat === sel.cat)) return
|
|
234
|
-
if (sel.t === 'asset' && this.media.curId === sel.id) {
|
|
235
|
-
// same asset, new tool set (a pasted share link): apply NOW, a
|
|
236
|
-
// parked pendingTools would hijack the next selection instead
|
|
237
|
-
if (p.tools != null) this.media.applyTab(p.tools)
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
if (hashFor(sel) === hashFor(this.sel)) return
|
|
241
|
-
if (sel.t === 'asset') this.media.pendingTools = p.tools
|
|
242
|
-
this.go(sel)
|
|
243
|
-
}
|
|
244
|
-
/** first heartbeat has the asset list — only then can a hash resolve */
|
|
245
|
-
afterFirstBeat(): void {
|
|
246
|
-
if (this.booted) return
|
|
247
|
-
this.booted = true
|
|
248
|
-
this.applyHash()
|
|
249
|
-
}
|
|
250
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/* the project gutter. It is FIXED to the left edge, so it takes no cell of
|
|
2
|
-
the .app grid, and the page makes room for it by padding, whatever the
|
|
3
|
-
grid holds. Colours are hub.css tokens only, spacing is its scale:
|
|
4
|
-
tests/hub-css-invariants.test.ts reads this sheet too. */
|
|
5
|
-
.app:has(> .pgut){--gut:44px;padding-left:var(--gut)}
|
|
6
|
-
.pgut{position:fixed;left:0;top:0;bottom:0;width:var(--gut,44px);z-index:5;display:flex;flex-direction:column;align-items:center;
|
|
7
|
-
gap:var(--s1);padding:var(--s2) 0;background:var(--bg);border-right:1px solid var(--line);overflow-y:auto;scrollbar-width:none}
|
|
8
|
-
.pgt{position:relative;flex:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;padding:0;
|
|
9
|
-
background:transparent;border:0;border-radius:6px;cursor:pointer}
|
|
10
|
-
.pgt:hover{background:var(--panel2)}
|
|
11
|
-
.pgt.cur{background:var(--sel)}
|
|
12
|
-
.pgt img{width:24px;height:24px;border-radius:5px;object-fit:cover;pointer-events:none}
|
|
13
|
-
/* the initial on a hue hashed from the name (projects-svc hueOf) */
|
|
14
|
-
.pgi{width:24px;height:24px;border-radius:5px;display:flex;align-items:center;justify-content:center;
|
|
15
|
-
font-size:12px;font-weight:600;user-select:none;color:var(--c);background:color-mix(in srgb,var(--c) 22%,var(--panel2))}
|
|
16
|
-
.pgi.h1{--c:var(--k1)}
|
|
17
|
-
.pgi.h2{--c:var(--k2)}
|
|
18
|
-
.pgi.h3{--c:var(--k3)}
|
|
19
|
-
.pgi.h4{--c:var(--k4)}
|
|
20
|
-
.pgi.h5{--c:var(--k5)}
|
|
21
|
-
.pgi.h6{--c:var(--k6)}
|
|
22
|
-
/* the face says three things and no more (projects-svc faceOf) */
|
|
23
|
-
/* closed in VS Code: the icon dims, the badge does not */
|
|
24
|
-
.pgt.dim img,.pgt.dim .pgi{opacity:.45}
|
|
25
|
-
/* the one badge: open asks, top right */
|
|
26
|
-
.pga{position:absolute;top:0;right:0;min-width:14px;height:14px;padding:0 var(--s0);border-radius:7px;
|
|
27
|
-
font-size:9px;font-weight:700;line-height:14px;text-align:center;color:var(--bg);background:var(--warn)}
|
|
28
|
-
/* Claude sessions running: a thin edge on the left that breathes */
|
|
29
|
-
.pgs{position:absolute;left:0;top:6px;bottom:6px;width:2px;border-radius:1px;background:var(--accent);pointer-events:none;animation:pgpulse 2.4s ease-in-out infinite}
|
|
30
|
-
@keyframes pgpulse{0%,100%{opacity:1}50%{opacity:.25}}
|
|
31
|
-
@media (prefers-reduced-motion:reduce){.pgs{animation:none}}
|
|
32
|
-
/* the hover card: right of the rail, one at a time, never interactive.
|
|
33
|
-
Header, then what needs a human, then the label and value rows of the
|
|
34
|
-
check page (.wfact .wk .wv from hub.css) */
|
|
35
|
-
.pgcard{position:fixed;left:calc(var(--gut,44px) + var(--s2));z-index:20;width:300px;padding:var(--s3);pointer-events:none;
|
|
36
|
-
background:var(--panel);border:1px solid var(--line);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px;line-height:1.45;color:var(--ink)}
|
|
37
|
-
.pgch{display:flex;align-items:center;gap:var(--s2);min-width:0}
|
|
38
|
-
.pgcface{flex:none;display:flex}
|
|
39
|
-
.pgcface img,.pgcface .pgi{width:20px;height:20px;border-radius:4px;font-size:11px}
|
|
40
|
-
.pgcname{flex:1;min-width:0;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
41
|
-
.pgcthis{font-weight:400;font-size:11px;color:var(--dim)}
|
|
42
|
-
.pgcver{flex:none;color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums}
|
|
43
|
-
.pgcline{margin-top:var(--s1);color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
44
|
-
.pgcdesc{margin-top:var(--s1);color:var(--dim);font-size:11px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
|
|
45
|
-
/* NEEDS YOU: a warn toned bullet, set apart from the facts */
|
|
46
|
-
.pgcneeds{margin-top:var(--s2);padding:var(--s1) var(--s2);border-left:2px solid var(--warn);background:var(--panel2);border-radius:0 3px 3px 0}
|
|
47
|
-
.pgcneed{display:flex;gap:var(--s1);min-width:0;color:var(--warn)}
|
|
48
|
-
.pgcneed.red{color:var(--red);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
49
|
-
.pgcask{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
50
|
-
.pgcmore{flex:none}
|
|
51
|
-
.pgcfacts{margin-top:var(--s2)}
|
|
52
|
-
.pgcfacts .wk{flex-basis:64px;white-space:nowrap}
|
|
53
|
-
.pgcfacts .wv{text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;word-break:normal}
|
|
54
|
-
.pgcfacts .wv.warn{color:var(--warn)}
|
|
55
|
-
.pgerr{font-size:12px;font-weight:700;color:var(--red);cursor:help}
|
|
56
|
-
/* the one time sort by last activity, above the icons */
|
|
57
|
-
.pgsort{flex:none;width:32px;height:20px;padding:0;background:transparent;border:1px solid var(--line);border-radius:3px;
|
|
58
|
-
color:var(--dim);font-size:12px;line-height:18px;cursor:pointer}
|
|
59
|
-
.pgsort:hover{color:var(--ink);border-color:var(--accent)}
|
|
60
|
-
/* a drag: the tile being moved fades, and the gap it would land in is a line
|
|
61
|
-
drawn on a tile edge, centred in the gap between two tiles */
|
|
62
|
-
.pgt.dragging{opacity:.4}
|
|
63
|
-
.pgt.dropb::before,.pgt.dropa::after{content:'';position:absolute;left:2px;right:2px;height:2px;border-radius:1px;background:var(--accent);pointer-events:none}
|
|
64
|
-
.pgt.dropb::before{top:-3px}
|
|
65
|
-
.pgt.dropa::after{bottom:-3px}
|