rvlib-shipkit 0.11.0 → 0.13.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +17 -3
  3. package/package.json +5 -2
  4. package/src/asks/asks-queue.ts +8 -0
  5. package/src/assets/kit-config.ts +2 -3
  6. package/src/assets/roll-workspace.ts +2 -1
  7. package/src/assets/run-assets.ts +0 -11
  8. package/src/assets/server/studio-server.ts +7 -24
  9. package/src/cli/cmd_Assets.ts +3 -14
  10. package/src/cli/cmd_Check.ts +22 -4
  11. package/src/cli/cmd_Reflect.ts +116 -0
  12. package/src/cli/cmd_Rules.ts +32 -0
  13. package/src/cli/cmd_Serve.ts +1 -1
  14. package/src/cli.ts +5 -2
  15. package/src/collections/actions.ts +19 -0
  16. package/src/collections/collection-types.ts +13 -0
  17. package/src/collections/graphs.ts +34 -0
  18. package/src/collections/pings.ts +5 -1
  19. package/src/collections/posts.ts +20 -1
  20. package/src/collections/providers.ts +163 -50
  21. package/src/contracts/apply-contracts.ts +47 -0
  22. package/src/contracts/run-contracts.ts +8 -3
  23. package/src/core/canonical-scripts.ts +46 -0
  24. package/src/core/check-suite.ts +6 -3
  25. package/src/core/locked-write.ts +39 -0
  26. package/src/core/motherhome.ts +87 -0
  27. package/src/core/run-check.ts +28 -15
  28. package/src/core/run-ci.ts +4 -0
  29. package/src/core/write-status.ts +7 -26
  30. package/src/init-repo.ts +24 -11
  31. package/src/reflect/prompt-corpus.ts +305 -0
  32. package/src/repo-config.ts +20 -38
  33. package/src/rules/SK001-package-json-shape.ts +156 -0
  34. package/src/rules/SK002-contracts-fulfillness.ts +19 -0
  35. package/src/rules/SK003-claude-md-shape.ts +64 -0
  36. package/src/rules/SK004-gitignore-policy.ts +105 -0
  37. package/src/rules/SK005-rvlib-versions.ts +99 -0
  38. package/src/rules/SK006-npm-publish-safety.ts +42 -0
  39. package/src/rules/SK007-github-visibility.ts +63 -0
  40. package/src/rules/rule-registry.ts +32 -0
  41. package/src/rules/rule.ts +62 -0
  42. package/src/rules/run-fix.ts +57 -0
  43. package/src/serve/hub-app/col-card.tsx +192 -0
  44. package/src/serve/hub-app/cols-svc.ts +136 -0
  45. package/src/serve/hub-app/geometry-view.tsx +155 -0
  46. package/src/serve/hub-app/health-svc.ts +49 -0
  47. package/src/serve/hub-app/hub-app.tsx +236 -215
  48. package/src/serve/hub-app/hub-st.ts +169 -0
  49. package/src/serve/hub-app/hub-types.ts +72 -0
  50. package/src/serve/hub-app/hub.css +286 -59
  51. package/src/serve/hub-app/index.html +1 -1
  52. package/src/serve/hub-app/md.ts +24 -0
  53. package/src/serve/hub-app/media-svc.ts +429 -0
  54. package/src/serve/hub-app/media-view.tsx +299 -0
  55. package/src/serve/run-serve.ts +23 -9
  56. package/src/serve/serve-hub.ts +62 -47
  57. package/src/shipkit.ts +9 -5
  58. package/src/assets/server/studio-page.ts +0 -687
  59. package/src/core/package-json-check.ts +0 -88
  60. package/src/serve/hub-pages.ts +0 -38
@@ -0,0 +1,169 @@
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; legacy /?needs=1 reads as needs),
5
+ // hash deep-links (#<asset-id> · #cat-<Category>).
6
+ import { makeAutoObservable } from 'mobx'
7
+ import { ColsSvc } from './cols-svc.ts'
8
+ import { HealthSvc } from './health-svc.ts'
9
+ import { MediaSvc } from './media-svc.ts'
10
+
11
+ export type Sel =
12
+ | { t: 'asset'; id: string }
13
+ | { t: 'cat'; cat: string }
14
+ | { t: 'col'; id: string }
15
+ | { t: 'item'; col: string; id: string }
16
+ | { t: 'status' }
17
+ | null
18
+
19
+ /** one cmd+j target — run() fires the SAME action clicking would */
20
+ export type Jump = { id: string; label: string; section: string; run: () => void }
21
+
22
+ /** all · needs (yellow+red) · red — one severity ladder for every section */
23
+ export type Filter = 'all' | 'needs' | 'red'
24
+ const parseFilter = (search: string): Filter => {
25
+ const sp = new URLSearchParams(search)
26
+ const f = sp.get('f')
27
+ if (f === 'needs' || f === 'red') return f
28
+ return sp.get('needs') === '1' ? 'needs' : 'all' // legacy address keeps working
29
+ }
30
+
31
+ export class HubSt {
32
+ name = ''
33
+ media = new MediaSvc(this)
34
+ cols = new ColsSvc(this)
35
+ health = new HealthSvc(this)
36
+ sel: Sel = null
37
+ filter: Filter = parseFilter(location.search)
38
+ /** sidebar search — live substring filter over asset + collection rows */
39
+ query = ''
40
+ palOpen = false
41
+ palQuery = ''
42
+ palIdx = 0
43
+ private booted = false
44
+
45
+ constructor() {
46
+ makeAutoObservable<this, 'booted'>(this, { media: false, cols: false, health: false, booted: false })
47
+ }
48
+
49
+ boot(): void {
50
+ void fetch('/hub.json')
51
+ .then((r) => r.json() as Promise<{ name: string }>)
52
+ .then((h) => { this.setName(h.name) })
53
+ .catch(() => {})
54
+ this.media.start()
55
+ this.cols.start()
56
+ void this.health.load()
57
+ window.addEventListener('hashchange', () => this.applyHash())
58
+ }
59
+ private setName(n: string): void {
60
+ this.name = n
61
+ document.title = `${n} · hub`
62
+ }
63
+
64
+ setFilter(v: Filter): void {
65
+ this.filter = v
66
+ const u = new URL(location.href)
67
+ u.searchParams.delete('needs') // legacy param never survives a click
68
+ v === 'all' ? u.searchParams.delete('f') : u.searchParams.set('f', v)
69
+ history.replaceState(null, '', u)
70
+ }
71
+ setQuery(v: string): void {
72
+ this.query = v
73
+ }
74
+
75
+ // ── cmd+j palette ─────────────────────────────────────────────────────
76
+ palToggle(): void {
77
+ this.palOpen = !this.palOpen
78
+ this.palQuery = ''
79
+ this.palIdx = 0
80
+ }
81
+ palClose(): void {
82
+ this.palOpen = false
83
+ }
84
+ setPalQuery(v: string): void {
85
+ this.palQuery = v
86
+ this.palIdx = 0
87
+ }
88
+ setPalIdx(n: number): void {
89
+ this.palIdx = n
90
+ }
91
+ palMove(d: number): void {
92
+ const n = this.palResults.length
93
+ if (n > 0) this.palIdx = (this.palIdx + d + n) % n
94
+ }
95
+ palRun(): void {
96
+ const j = this.palResults[this.palIdx]
97
+ if (j == null) return
98
+ this.palOpen = false
99
+ j.run()
100
+ }
101
+ /** every jump target: section titles, media assets, collection instances */
102
+ get jumps(): Jump[] {
103
+ const out: Jump[] = []
104
+ for (const cat of new Set(this.media.assets.map((a) => a.cat)))
105
+ out.push({ id: `cat:${cat}`, label: cat, section: 'media', run: () => this.showCat(cat) })
106
+ for (const a of this.media.assets)
107
+ out.push({ id: `asset:${a.id}`, label: a.label, section: a.cat, run: () => this.selectAsset(a.id) })
108
+ for (const col of this.cols.sidebarCols) {
109
+ const health = col.id === 'health'
110
+ out.push({ id: `col:${col.id}`, label: col.title, section: 'section', run: () => (health ? this.showStatus() : this.showCol(col.id)) })
111
+ for (const i of col.instances)
112
+ out.push({ id: `item:${i.id}`, label: i.text, section: col.title, run: () => (health ? this.showStatus() : this.cols.select(col.id, i.id)) })
113
+ }
114
+ return out
115
+ }
116
+ /** ranked: prefix matches first, then substring; capped at 12 */
117
+ get palResults(): Jump[] {
118
+ const q = this.palQuery.trim().toLowerCase()
119
+ if (q === '') return this.jumps.slice(0, 12)
120
+ const pre: Jump[] = []
121
+ const sub: Jump[] = []
122
+ for (const j of this.jumps) {
123
+ const l = j.label.toLowerCase()
124
+ if (l.startsWith(q)) pre.push(j)
125
+ else if (l.includes(q)) sub.push(j)
126
+ }
127
+ return [...pre, ...sub].slice(0, 12)
128
+ }
129
+
130
+ // ── selection + hash deep-links ───────────────────────────────────────
131
+ selectAsset(id: string, writeHash = true): void {
132
+ this.sel = { t: 'asset', id }
133
+ if (writeHash) this.setHash(id)
134
+ void this.media.select(id)
135
+ }
136
+ showCat(cat: string, writeHash = true): void {
137
+ this.sel = { t: 'cat', cat }
138
+ this.media.openCats.add(cat)
139
+ if (writeHash) this.setHash(`cat-${cat}`)
140
+ void this.media.fillCat()
141
+ }
142
+ showStatus(): void {
143
+ this.sel = { t: 'status' }
144
+ void this.health.load()
145
+ }
146
+ /** collection view — one card per instance (health has StatusCard instead) */
147
+ showCol(id: string): void {
148
+ this.sel = { t: 'col', id }
149
+ }
150
+ private setHash(h: string): void {
151
+ if (location.hash.slice(1) !== h) location.hash = h
152
+ }
153
+ applyHash(): void {
154
+ const h = decodeURIComponent(location.hash.slice(1))
155
+ if (h === '') return
156
+ if (h.startsWith('cat-')) {
157
+ const c = h.slice(4)
158
+ if (this.media.assets.some((a) => a.cat === c) && this.media.catMode !== c) this.showCat(c, false)
159
+ return
160
+ }
161
+ if (this.media.assets.some((a) => a.id === h) && this.media.curId !== h) this.selectAsset(h, false)
162
+ }
163
+ /** first heartbeat has the asset list — only then can a hash resolve */
164
+ afterFirstBeat(): void {
165
+ if (this.booted) return
166
+ this.booted = true
167
+ this.applyHash()
168
+ }
169
+ }
@@ -0,0 +1,72 @@
1
+ // Wire types the hub app reads — mirrors of the server shapes
2
+ // (studio-server.ts, collection-types.ts). Kept structural on purpose: the
3
+ // page renders what the API sends, the file is the truth.
4
+
5
+ export type SideAsset = {
6
+ id: string
7
+ cat: string
8
+ label: string
9
+ prompt: string
10
+ rolls: number
11
+ picked: boolean
12
+ icon: boolean
13
+ media: 'image' | 'audio'
14
+ manual: boolean
15
+ derived: boolean
16
+ stale: boolean
17
+ }
18
+
19
+ export type RollRow = { name: string; picked: boolean; alt: boolean; hasMeta: boolean }
20
+
21
+ export type AssetInfo = {
22
+ backend: string
23
+ deterministic: boolean
24
+ workflow: { id: string | null; path: string | null; note?: string } | null
25
+ ship: { mode: string; path?: string; note?: string }
26
+ metaFile: string | null
27
+ kindId: string
28
+ kitSource: string
29
+ }
30
+
31
+ export type CustomPanelRef = { id: string; kind: string; url: string }
32
+
33
+ export type AssetDetail = {
34
+ asset: { id: string; label?: string; prompt: string; cat: string; geometry: boolean; custom: CustomPanelRef[] } & Record<string, unknown>
35
+ info: AssetInfo
36
+ rolls: RollRow[]
37
+ }
38
+
39
+ export type Leg = { x: number; y: number; w: number; h: number }
40
+ export type Rect = { x: number; y: number; w: number; h: number; size: number; flip?: boolean; legs?: Leg[]; legColor?: string; art?: unknown }
41
+
42
+ export type CatRow = { id: string; label: string; name: string; rect: Rect | null; prop: boolean; icon: boolean }
43
+
44
+ export type QueueState = {
45
+ generating: boolean
46
+ current: string | null
47
+ done: number
48
+ total: number
49
+ queued: number
50
+ progress: { value: number; max: number } | null
51
+ pending: string[]
52
+ errors: string[]
53
+ assets: SideAsset[]
54
+ }
55
+
56
+ export type Candidate = { id: string; label: string; hint?: string }
57
+ export type Artifact = { copy?: string; url?: string; steps?: string[] }
58
+ export type ColInstance = {
59
+ id: string
60
+ text: string
61
+ severity?: 'red' | 'yellow'
62
+ href?: string
63
+ artifact?: Artifact
64
+ candidates?: Candidate[]
65
+ file?: string
66
+ img?: string
67
+ remarks?: string[]
68
+ /** the truth file's markdown BODY (frontmatter stripped) — clamped card
69
+ * in the grid, full in the item view */
70
+ md?: string
71
+ }
72
+ export type Col = { id: string; title: string; media: string; instances: ColInstance[] }
@@ -1,65 +1,292 @@
1
- /* house palette — matches the studio/graph family. Plain CSS on purpose:
2
- a published lib must not impose bunfig plugins (tailwind) on consumers. */
1
+ /* house palette — matches the graph family. Plain CSS on purpose: a
2
+ published lib must not impose bunfig plugins (tailwind) on consumers. */
3
3
  :root{--bg:#141210;--panel:#1e1a15;--panel2:#262019;--line:#332b22;--ink:#f3ebe0;--dim:#a89a88;
4
- --accent:#e88c28;--gold:#ffcf5c;--teal:#58c0b0;--red:#e05252}
5
- *{box-sizing:border-box}
6
- body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.6 system-ui,sans-serif;min-height:100vh}
7
- .app{display:flex;min-height:100vh}
8
- .wrap{max-width:900px;margin:0 auto;padding:34px 26px;flex:1}
9
- h1{font-size:20px;margin:0}
10
- h1 small{color:var(--dim);font-weight:400;font-size:13px;margin-left:10px}
11
- .crumb{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);margin-bottom:6px}
12
-
13
- .skrail{position:sticky;top:0;align-self:flex-start;height:100vh;width:46px;display:flex;flex-direction:column;
14
- align-items:center;padding-top:10px;gap:4px;background:#17140f;border-right:1px solid var(--line);flex:none}
15
- .skrail a{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:9px;
16
- font-size:17px;text-decoration:none;border-left:2px solid transparent;filter:grayscale(35%);opacity:.75;position:relative}
17
- .skrail a:hover{background:var(--panel2);filter:none;opacity:1}
18
- .skrail a.on{background:var(--panel2);border-left-color:var(--accent);filter:none;opacity:1}
19
- .skrail a i{position:absolute;top:4px;right:4px;width:8px;height:8px;border-radius:50%;background:var(--red)}
20
-
21
- .clear{margin-top:30px;font-size:17px;color:var(--teal)}
22
- section{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--gold);border-radius:12px;
23
- padding:14px 18px;margin-top:16px}
24
- section.red{border-left-color:var(--red)}
25
- h2{font-size:14px;margin:0 0 8px;display:flex;align-items:center;gap:8px}
26
- h2 a{color:var(--ink);text-decoration:none}h2 a:hover{color:var(--gold)}
27
- h2 em{font-style:normal;font-size:12px;color:var(--dim);background:var(--panel2);border-radius:8px;padding:1px 8px}
28
- .line{font:12.5px/1.7 ui-monospace,Menlo,monospace;color:var(--dim);white-space:pre-wrap}
29
- .line a{color:var(--dim);text-decoration:underline dotted}.line a:hover{color:var(--gold)}
30
- section.red .line{color:#e8a0a0}
31
- .sub{margin-top:10px;font-size:12px;color:var(--dim)}
32
- .sub a{color:var(--dim)}
33
-
34
- .bar{display:flex;align-items:center;gap:12px;margin:18px 0 12px}
35
- button{background:var(--accent);color:#1a1206;border:0;border-radius:9px;padding:8px 14px;font-weight:650;cursor:pointer}
4
+ --accent:#e88c28;--gold:#ffcf5c;--teal:#58c0b0;--red:#e05252;--blue:#6ca0e8;--chk1:#221d17;--chk2:#1a1611}
5
+ *{box-sizing:border-box}html,body,#root{height:100%}
6
+ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
7
+ .app{display:grid;grid-template-columns:46px 290px 1fr;height:100vh}
8
+
9
+ /* ── icon rail: jump-to-section, never a navigation ── */
10
+ .skrail{width:46px;height:100vh;display:flex;flex-direction:column;align-items:center;padding-top:10px;gap:4px;
11
+ background:#17140f;border-right:1px solid var(--line)}
12
+ .skrail .ric{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:9px;
13
+ font-size:17px;cursor:pointer;filter:grayscale(35%);opacity:.75;position:relative;user-select:none}
14
+ .skrail .ric:hover{background:var(--panel2);filter:none;opacity:1}
15
+ .skrail .ric i{position:absolute;top:4px;right:4px;width:8px;height:8px;border-radius:50%;background:var(--red)}
16
+
17
+ /* ── sidebar ── */
18
+ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto;height:100vh}
19
+ .shead{position:sticky;top:0;background:var(--panel);padding:12px 14px;border-bottom:1px solid var(--line);
20
+ font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);z-index:2;display:flex;justify-content:space-between;align-items:center}
21
+ /* segmented severity filter — all · needs · red */
22
+ .seg{display:inline-flex;border:1px solid var(--line);border-radius:7px;overflow:hidden;flex:none}
23
+ .seg button{background:var(--bg);color:var(--dim);border:0;border-radius:0;padding:2px 8px;font-size:10px;font-weight:600;
24
+ letter-spacing:.06em;text-transform:uppercase;cursor:pointer}
25
+ .seg button+button{border-left:1px solid var(--line)}
26
+ .seg button:hover{color:var(--ink)}
27
+ .seg button.on{background:var(--accent);color:#1a1206}
28
+ .ssearch{padding:8px 12px}
29
+ .ssearch input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:8px;color:var(--ink);
30
+ padding:6px 10px;font-size:12.5px;outline:none}
31
+ .ssearch input:focus{border-color:var(--accent)}
32
+ /* section header — hairline top border, tight letterspacing; media cats
33
+ and collection sections share this treatment EXACTLY */
34
+ .gname{padding:9px 14px 4px;font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:var(--accent);
35
+ border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;scroll-margin-top:46px}
36
+ .gname .cn{cursor:pointer}.gname .cn:hover{color:var(--gold)}
37
+ .gname .cnt{font-size:11px;color:var(--dim);letter-spacing:0;text-align:right;min-width:18px}
38
+ .warnline{color:var(--red);text-transform:none;letter-spacing:0}
39
+ .fold{cursor:pointer;color:var(--dim);font-size:15px;display:inline-block;width:22px;text-align:center;
40
+ padding:2px 3px;margin:-2px 0;border-radius:6px;user-select:none}
41
+ .fold:hover{color:var(--ink);background:var(--panel2)}
42
+ .catgen button{background:var(--panel2);color:var(--dim);border:1px solid var(--line);border-radius:6px;
43
+ padding:1px 6px;font-size:11px;font-weight:700;margin-left:3px;cursor:pointer}
44
+ .catgen button:hover{color:var(--ink);border-color:var(--accent)}
45
+ /* items sit one step in from their header, on a subtle left guide line;
46
+ the full row hovers, the selected row keeps the accent bar */
47
+ .arow{position:relative;display:flex;align-items:center;gap:8px;padding:3px 12px 3px 24px;cursor:pointer;
48
+ border-left:3px solid transparent}
49
+ .arow::before{content:'';position:absolute;left:15px;top:0;bottom:0;width:1px;background:var(--line)}
50
+ .arow:hover{background:var(--panel2)}
51
+ .arow.sel{background:rgba(232,140,40,.18);border-left-color:var(--accent)}
52
+ .arow.sel::before{background:transparent}
53
+ .arow .lb{flex:1;min-width:0}.arow .lb b{display:block;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
54
+ .arow .lb span{font-size:11px;color:var(--dim);display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
55
+ .dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--line)}
56
+ .dot.done{background:var(--gold)}.dot.some{background:var(--accent)}
57
+ .dot.miss{background:#e5484d}.dot.stale{background:#f5a524}
58
+ .cdot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--line)}
59
+ .cdot.red{background:var(--red)}.cdot.yellow{background:var(--gold)}
60
+
61
+ /* ── main column ── */
62
+ main{display:flex;flex-direction:column;height:100vh;overflow:hidden}
63
+ /* topbar: the prompt is its OWN full-width row (small mono, auto-grows to
64
+ its content, scrolls only past ~14 lines), then ONE slim toolbar row */
65
+ .bar{display:flex;flex-direction:column;gap:8px;padding:10px 16px;border-bottom:1px solid var(--line);background:var(--panel)}
66
+ textarea{width:100%;min-height:64px;background:var(--bg);color:var(--ink);border:1px solid var(--line);
67
+ border-radius:9px;padding:9px 11px;font:12px/1.5 ui-monospace,Menlo,monospace;resize:vertical}
68
+ .bar .prompt{min-height:36px;max-height:252px;overflow-y:auto;resize:none;font:11px/1.5 ui-monospace,Menlo,monospace;padding:7px 10px}
69
+ .toolbar{display:flex;gap:8px;align-items:center;min-height:30px}
70
+ .toolbar button{padding:5px 11px;font-size:12px}
71
+ .toolbar .sp{flex:1}
72
+ .toolbar .xn{display:flex;gap:3px;align-items:center;font-size:12px;color:var(--dim)}
73
+ .toolbar .xn select{padding:4px 5px;font-size:12px}
74
+ button{background:var(--accent);color:#1a1206;border:0;border-radius:9px;padding:8px 13px;font-weight:650;cursor:pointer}
75
+ button.ghost{background:var(--panel2);color:var(--ink);border:1px solid var(--line)}
36
76
  button:disabled{opacity:.45;cursor:default}
77
+ select,input[type=number]{background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px}
78
+ .status{font-size:12px;color:var(--dim);text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:46vw;flex:none}
79
+ .spin{width:12px;height:12px;border:2px solid var(--line);border-top-color:var(--gold);border-radius:50%;animation:rot 1s linear infinite;flex:none}
80
+ @keyframes rot{to{transform:rotate(360deg)}}
81
+ .progbar{display:block;width:120px;height:4px;background:var(--panel2);border-radius:2px;overflow:hidden;flex:none}
82
+ .progbar i{display:block;height:100%;width:0;background:var(--gold);transition:width .3s}
83
+ .detnote{font-size:11px;color:var(--teal);line-height:1.25}
84
+ .jrow{display:flex;gap:8px;margin-top:6px;align-items:center}
85
+ .jmsg{font-size:12px;color:var(--dim)}
86
+ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:0 5px;font-size:11px}
87
+ .help{font-size:11px;color:var(--dim);padding:4px 16px;border-top:1px solid var(--line)}
88
+ .errs{background:#3a1717;border-bottom:1px solid #5a2222;color:#f0c0c0;font:12px/1.5 ui-monospace,Menlo,monospace;
89
+ padding:6px 16px;white-space:pre-wrap}
90
+
91
+ /* ── per-asset info chips ── */
92
+ .info{flex-wrap:wrap;gap:6px;align-items:center;padding:6px 16px;border-bottom:1px solid var(--line);background:var(--panel2)}
93
+ .info .chip{display:inline-flex;align-items:center;gap:5px;background:var(--bg);border:1px solid var(--line);border-radius:6px;
94
+ padding:2px 8px;font-size:11px;color:var(--ink);max-width:360px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
95
+ .info .chip b{color:var(--accent);font-weight:650;letter-spacing:.05em;text-transform:uppercase;font-size:10px}
96
+ .info .chip.warn{border-color:var(--red);color:#f0c0c0}.info .chip.warn b{color:var(--red)}
97
+ .info .chip code{font:11px ui-monospace,Menlo,monospace;color:var(--dim)}
98
+
99
+ /* ── stage ── */
100
+ .stage{position:relative;flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:18px;overflow:hidden;
101
+ background:conic-gradient(var(--chk1) 90deg,var(--chk2) 0 180deg,var(--chk1) 0 270deg,var(--chk2) 0) 0/34px 34px}
102
+ .stage img.roll{max-width:100%;max-height:100%;object-fit:contain;filter:drop-shadow(0 6px 20px #0008);z-index:5;position:relative}
103
+ .stage .ph,.stagewrap .ph{color:var(--dim)}
104
+ .stagewrap{flex:1;min-height:0;overflow-y:auto;display:flex;align-items:flex-start;justify-content:center;padding:18px}
105
+ .stageaud{display:flex;flex-direction:column;align-items:center;gap:14px;font-size:48px;padding:40px}
106
+ .stageaud audio{width:min(480px,80vw)}
107
+
108
+ /* ── geometry overlay ── */
109
+ #ov{position:absolute;border:2px solid var(--gold);border-radius:50%;background:rgba(255,207,92,.14);cursor:move;z-index:10}
110
+ #ov .hdl{position:absolute;right:-7px;top:-7px;width:14px;height:14px;background:var(--gold);border-radius:3px;cursor:nesw-resize}
111
+ #ov .arrow{position:absolute;left:0;right:0;top:50%;border-top:1px dashed var(--gold);opacity:.8}
112
+ #ov .sz{position:absolute;left:50%;top:50%;transform:translate(-50%,-120%);font-size:11px;color:var(--gold);
113
+ background:#000a;padding:0 5px;border-radius:4px;white-space:nowrap}
114
+ .plumb{position:absolute;width:0;border-left:1px dashed rgba(255,207,92,.5);top:-2000px;bottom:-2000px;z-index:9}
115
+ .leg{position:absolute;z-index:10;border:1px solid;border-radius:3px}
116
+ .leg.far{border-color:var(--blue);z-index:3}
117
+ .leg.near{border-color:var(--red);z-index:7}
118
+ .leg .hip{position:absolute;top:-3px;left:50%;transform:translateX(-50%);width:6px;height:6px;border-radius:50%;background:currentColor}
119
+ .leg .foot{position:absolute;bottom:-4px;left:50%;transform:translateX(-50%);width:12px;height:7px;border-radius:50%;background:var(--gold)}
120
+ .leg .lh{position:absolute;right:-6px;top:-6px;width:11px;height:11px;background:currentColor;border-radius:3px;cursor:nesw-resize}
121
+ .leg .tag{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:9px;opacity:.85;white-space:nowrap}
122
+ .pill{position:absolute;border-radius:999px;z-index:3;opacity:.9}
123
+ .pill.near{z-index:7}
124
+ #rectbar{position:absolute;left:14px;bottom:14px;z-index:11;gap:8px;align-items:center;
125
+ background:#000b;border:1px solid var(--line);border-radius:10px;padding:7px 10px;font-size:12px}
126
+ #rectbar input[type=number]{width:64px;padding:4px}
127
+ #rectbar .ro{color:var(--dim);font:11px ui-monospace,Menlo,monospace}
128
+ #rectbar label{display:flex;gap:6px;align-items:center}
129
+
130
+ /* ── category grid + lineup ── */
131
+ #catgrid{flex:1;min-height:0;overflow-y:auto;padding:14px}
132
+ .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}
133
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px;cursor:pointer}
134
+ .card:hover{border-color:var(--accent)}
135
+ .card .th{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;
136
+ background:conic-gradient(var(--chk1) 90deg,var(--chk2) 0 180deg,var(--chk1) 0 270deg,var(--chk2) 0) 0/14px 14px}
137
+ .card .th img{width:100%;height:100%;object-fit:contain}
138
+ .card .th .ovmini{position:absolute;border:1.5px solid var(--gold);border-radius:50%;background:rgba(255,207,92,.12)}
139
+ .card .th .spin{position:absolute;top:8px;right:8px}
140
+ .card .nm{font-size:12px;margin:6px 0 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
141
+ .card .nm:hover{color:var(--gold)}
142
+ .card .row{display:flex;gap:6px;align-items:center;font-size:11px;color:var(--dim)}
143
+ .card .row input{width:58px;padding:3px}
144
+ .card .aud{aspect-ratio:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;font-size:28px}
145
+ .card .aud audio{width:92%;height:24px}
146
+ #allbar{display:flex;gap:8px;align-items:center;margin:0 0 12px;font-size:12px;color:var(--dim)}
147
+ #allbar label{display:flex;gap:6px;align-items:center}
148
+ #mixsec{margin-top:16px}
149
+ #mixbox{position:relative;height:420px;overflow-x:auto;overflow-y:hidden;background:var(--panel);
150
+ border:1px solid var(--line);border-radius:10px}
151
+ #mixinner{position:relative;height:100%}
152
+ #mixground{position:absolute;left:0;right:0;bottom:0;height:44px;background:#1d2b1a;border-top:1px solid #2c4527}
153
+ .mixsprite{position:absolute;image-rendering:auto;pointer-events:none}
154
+ .mixlabel{position:absolute;font-size:10px;color:var(--dim);text-align:center;transform:translateX(-50%)}
155
+ .ruler{position:absolute;left:0;right:0;bottom:44px;top:0;pointer-events:none;
156
+ background:
157
+ repeating-linear-gradient(to top,rgba(255,255,255,.28) 0 1px,transparent 1px 240px),
158
+ repeating-linear-gradient(to top,rgba(255,255,255,.16) 0 1px,transparent 1px 120px),
159
+ repeating-linear-gradient(to top,rgba(255,255,255,.07) 0 1px,transparent 1px 24px)}
160
+ .rlabel{position:absolute;left:4px;font-size:9px;color:var(--dim)}
161
+ .rlabel.b{font-weight:700;color:var(--ink)}
162
+
163
+ /* ── roll strip ── */
164
+ .strip{height:150px;flex:none;border-top:1px solid var(--line);background:var(--panel);display:flex;gap:10px;padding:12px;overflow-x:auto}
165
+ .vig{position:relative;height:100%;aspect-ratio:1;flex:none;border:2px solid var(--line);border-radius:10px;overflow:hidden;cursor:pointer;
166
+ background:conic-gradient(var(--chk1) 90deg,var(--chk2) 0 180deg,var(--chk1) 0 270deg,var(--chk2) 0) 0/12px 12px}
167
+ .vig img{width:100%;height:100%;object-fit:contain}
168
+ .vig.active{border-color:var(--ink)}
169
+ .vig.picked{border-color:var(--gold);box-shadow:0 0 0 2px var(--gold) inset}
170
+ .vig.alt{border-color:var(--teal)}
171
+ .vig .st{position:absolute;top:4px;right:6px;font-size:16px;filter:drop-shadow(0 1px 2px #000)}
172
+ .vig .btns{position:absolute;bottom:4px;left:4px;right:4px;display:flex;gap:4px;justify-content:center;opacity:0;transition:opacity .15s}
173
+ .vig:hover .btns{opacity:1}
174
+ .vig .btns button{padding:2px 7px;font-size:11px;border-radius:6px}
175
+ .vig .btns .b-alt{background:var(--teal);color:#06342c}
176
+ .vig .btns .b-del{background:var(--red);color:#fff}
177
+ .vig.aud{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;font-size:20px}
178
+ .vig.aud audio{width:100%;height:24px}
179
+ .vig.gen{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--dim);font-size:11px}
180
+ .strip .ph{color:var(--dim);align-self:center;padding:0 12px}
181
+
182
+ /* ── drawer ── */
183
+ .drawer{flex:none;border-top:1px solid var(--line);background:var(--panel)}
184
+ .tabs{display:flex;gap:2px;padding:6px 12px 0}
185
+ .tabs button{background:var(--panel2);color:var(--dim);border:1px solid var(--line);border-bottom:0;border-radius:8px 8px 0 0;
186
+ padding:5px 12px;font-size:12px;font-weight:400}
187
+ .tabs button.on{color:var(--ink);background:var(--bg)}
188
+ .tabs .sp{flex:1}
189
+ .tabs .ghost{border:1px solid var(--line);border-radius:8px;font-size:12px;padding:5px 12px}
190
+ #drawerbody{padding:10px 12px}
191
+ #jsonta{min-height:140px}
192
+
193
+ /* ── asset remark row ── */
194
+ .assetrem{flex:none;border-top:1px solid var(--line);background:var(--panel);padding:8px 12px}
195
+ .assetrem .row{display:flex;gap:10px;align-items:center}
196
+ .assetrem input{flex:1;background:var(--bg);border:1px solid var(--line);border-radius:8px;color:var(--ink);padding:7px 10px;font-size:12.5px}
197
+ .assetrem .rem{color:var(--teal);border-left:2px solid var(--teal);padding-left:10px;font-size:12.5px;margin:0 0 8px;white-space:pre-wrap}
198
+
199
+ /* ── collection item card + status card ── */
200
+ .colcard{max-width:760px;width:100%;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 22px;text-align:left}
201
+ .colcard h3{margin:0 0 10px;font-size:15px;display:flex;gap:10px;align-items:baseline}
202
+ .colcard .truth{margin-left:auto;font:11px ui-monospace,Menlo,monospace;color:var(--dim)}
203
+ .colcard .rem{color:var(--teal);border-left:2px solid var(--teal);padding-left:10px;font-size:12.5px;margin:8px 0;white-space:pre-wrap}
204
+ .colcard .row{display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap}
205
+ .colcard input{flex:1;min-width:220px;background:var(--bg);border:1px solid var(--line);border-radius:8px;color:var(--ink);padding:7px 10px;font-size:12.5px}
206
+ .colcard code{display:block;background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:12px ui-monospace,Menlo,monospace;white-space:pre-wrap;max-height:200px;overflow-y:auto;margin-top:8px}
207
+ .colcard .cand{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:9px 12px;margin-top:8px}
208
+ .colcard .candhint{color:var(--dim);font-size:12.5px;white-space:pre-wrap}
209
+ .colcard .err,.assetrem .err{color:var(--red);margin-top:8px;font-size:12.5px}
210
+ .colcard a{color:var(--accent)}
211
+
212
+ .back{cursor:pointer;color:var(--dim);font-size:12px}
213
+ .back:hover{color:var(--ink)}
214
+ .colcard .back{margin-bottom:8px}
215
+
216
+ /* ── collection view (one card per instance) ── */
217
+ .colgrid{flex:1;min-height:0;overflow-y:auto;padding:14px 16px}
218
+ .cghead{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
219
+ display:flex;gap:10px;align-items:baseline;margin:0 0 12px}
220
+ .cghead .cnt{font-size:11px;color:var(--dim);letter-spacing:0;text-transform:none}
221
+ .cgcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}
222
+ .cgcard{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:10px 12px;cursor:pointer;min-width:0}
223
+ .cgcard:hover{border-color:var(--accent)}
224
+ .cgcard .cgtop{display:flex;gap:8px;align-items:center;min-width:0}
225
+ .cgcard .cgtop b{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
226
+ .cgcard .truth{display:block;font:11px ui-monospace,Menlo,monospace;color:var(--dim);margin-top:4px;
227
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
228
+ .cgcard .cgrem{color:var(--teal);border-left:2px solid var(--teal);padding-left:8px;font-size:12px;margin-top:6px;
229
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
230
+
231
+ /* ── graph item view (the graph itself fills the stage) ── */
232
+ .graphview{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px;padding:12px 16px 16px}
233
+ .gvhead{display:flex;gap:10px;align-items:center;font-size:13px;min-width:0}
234
+ .gvhead .gvtext{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
235
+ .graphview iframe{flex:1;min-height:0;width:100%;border:1px solid var(--line);border-radius:12px;background:var(--bg)}
236
+
237
+ /* ── md output (truth-file card bodies — marked + sanitize allowlist) ── */
238
+ .md{font-size:12px;line-height:1.5;color:var(--ink)}
239
+ .md table{border-collapse:collapse;margin:6px 0;font-size:11.5px;max-width:100%}
240
+ .md th,.md td{border:1px solid var(--line);padding:3px 8px;text-align:left;vertical-align:top}
241
+ .md th{color:var(--accent);font-weight:650;font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;background:var(--panel2)}
242
+ .md blockquote{margin:6px 0;padding:2px 10px;border-left:2px solid var(--line);color:var(--dim)}
243
+ .md hr{border:0;border-top:1px solid var(--line);margin:8px 0}
244
+ .md h1,.md h2,.md h3{font-size:12.5px;margin:8px 0 4px;color:var(--accent);letter-spacing:.02em}
245
+ .md h1{font-size:13.5px}
246
+ .md p{margin:4px 0}
247
+ .md ul{margin:4px 0;padding-left:18px}
248
+ .md li{margin:2px 0}
249
+ .md code{display:inline;background:var(--bg);border:1px solid var(--line);border-radius:4px;padding:0 4px;
250
+ font:11px ui-monospace,Menlo,monospace;color:var(--ink);white-space:pre-wrap;max-height:none;margin:0}
251
+ .md pre{background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;margin:6px 0;
252
+ overflow-x:auto;font:11px/1.5 ui-monospace,Menlo,monospace;white-space:pre}
253
+ .md pre code{background:none;border:0;padding:0}
254
+ .md a{color:var(--accent)}
255
+ .md.clamp{max-height:180px;overflow:hidden;margin-top:6px;
256
+ -webkit-mask-image:linear-gradient(#000 65%,transparent);mask-image:linear-gradient(#000 65%,transparent)}
257
+
258
+ /* ── secret (cred) cards ── */
259
+ .credname{font:600 13px ui-monospace,Menlo,monospace}
260
+ .creddesc{font-size:12px;color:var(--dim);margin-top:4px}
261
+ .credrow{display:flex;gap:8px;align-items:center;margin-top:8px}
262
+ .stchip{font-size:10px;border-radius:5px;padding:1px 7px;letter-spacing:.08em;text-transform:uppercase;
263
+ border:1px solid;flex:none;font-weight:600}
264
+ .stchip.ok{color:var(--teal);border-color:var(--teal)}
265
+ .stchip.miss{color:var(--red);border-color:var(--red)}
266
+ a.mint{color:var(--accent);font-size:12px;text-decoration:none}
267
+ a.mint:hover{text-decoration:underline}
268
+ .colcard h3 .stchip,.colcard h3 a.mint{margin-left:8px}
269
+
270
+ /* ── cmd+j palette ── */
271
+ .palov{position:fixed;inset:0;background:#0009;z-index:100;display:flex;justify-content:center;align-items:flex-start;
272
+ padding-top:14vh}
273
+ .pal{width:min(560px,90vw);background:var(--panel);border:1px solid var(--line);border-radius:12px;
274
+ box-shadow:0 12px 40px #000a;overflow:hidden}
275
+ .pal input{width:100%;background:var(--bg);border:0;border-bottom:1px solid var(--line);color:var(--ink);
276
+ padding:12px 14px;font-size:14px;outline:none}
277
+ .prows{max-height:50vh;overflow-y:auto;padding:6px}
278
+ .prow{display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:8px;cursor:pointer;font-size:13px}
279
+ .prow.on{background:var(--panel2)}
280
+ .prow.none{color:var(--dim);cursor:default}
281
+ .prow .plabel{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
282
+ .prow .pchip{font-size:10px;color:var(--dim);background:var(--bg);border:1px solid var(--line);border-radius:5px;
283
+ padding:1px 6px;text-transform:uppercase;letter-spacing:.08em;flex:none}
284
+
285
+ .wrap{max-width:900px;width:100%}
286
+ .wrap h1{font-size:20px;margin:0}
287
+ .wrap h1 small{color:var(--dim);font-weight:400;font-size:13px;margin-left:10px}
288
+ .hbar{display:flex;align-items:center;gap:12px;margin:18px 0 12px}
37
289
  .note{font-size:12px;color:var(--dim)}
38
290
  pre{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;overflow-x:auto;
39
291
  font:12.5px/1.55 ui-monospace,Menlo,monospace;white-space:pre-wrap}
40
292
  .ok{color:var(--teal)}.warn{color:var(--gold)}.fail{color:var(--red)}.dim{color:var(--dim)}
41
- .err{margin-top:26px;color:var(--red)}
42
-
43
- /* needs feed tiles — red blocks, yellow = taste pending */
44
- .tile{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--dim);border-radius:12px;
45
- padding:12px 16px;margin-top:10px}
46
- .tile.red{border-left-color:var(--red)}
47
- .tile.yellow{border-left-color:var(--gold)}
48
- .tline{display:flex;align-items:baseline;gap:10px;font-size:14px}
49
- .tline a{color:var(--ink);text-decoration:underline dotted}.tline a:hover{color:var(--gold)}
50
- .chip{flex:none;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);
51
- background:var(--panel2);border-radius:8px;padding:1px 8px}
52
- .tile.red .chip{color:#e8a0a0}
53
- .tact{display:flex;align-items:center;gap:10px;margin-top:8px;font-size:12.5px;flex-wrap:wrap}
54
- .tact code{color:var(--dim);font:12px ui-monospace,Menlo,monospace;overflow:hidden;text-overflow:ellipsis;max-width:560px;white-space:nowrap}
55
- .tact button{padding:4px 10px;font-size:12px}
56
- .tact input{background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--ink);padding:6px 10px;font-size:12.5px;min-width:240px}
57
- .tact a{color:var(--accent)}
58
- .tact details{width:100%}
59
- .tact summary{cursor:pointer;color:var(--dim)}
60
- .hint{color:var(--dim);font-size:12.5px;white-space:pre-wrap;border-left:2px solid var(--line);padding-left:10px;margin:6px 0 4px}
61
- .picks{flex-direction:column;align-items:stretch}
62
- .cand{background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:8px 12px}
63
- .candhead{display:flex;align-items:center;gap:10px;margin-bottom:4px}
64
- .tileerr{margin-top:8px;font-size:12.5px;color:var(--red)}
65
- label{font-size:12.5px;color:var(--dim);display:flex;gap:6px;align-items:center;cursor:pointer}
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>shipkit hub</title>
6
+ <title>hub</title>
7
7
  <link rel="stylesheet" href="./hub.css" />
8
8
  </head>
9
9
  <body>
@@ -0,0 +1,24 @@
1
+ // md — the SAFE markdown renderer for truth-file bodies on cards.
2
+ // marked (GFM: tables, strikethrough, autolinks) → sanitize-html allowlist.
3
+ // dangerouslySetInnerHTML consumes ONLY this output: every tag outside the
4
+ // allowlist is stripped, hrefs are http/https only, and every surviving
5
+ // anchor opens a new tab with rel=noreferrer.
6
+ import { marked } from 'marked'
7
+ import sanitizeHtml from 'sanitize-html'
8
+
9
+ marked.use({ gfm: true, breaks: false, async: false })
10
+
11
+ export const renderMd = (src: string): string =>
12
+ sanitizeHtml(marked.parse(src) as string, {
13
+ allowedTags: [
14
+ 'h1', 'h2', 'h3', 'h4', 'p', 'ul', 'ol', 'li',
15
+ 'table', 'thead', 'tbody', 'tr', 'th', 'td',
16
+ 'code', 'pre', 'strong', 'em', 'b', 'i', 'a', 'br', 'blockquote', 'hr',
17
+ ],
18
+ allowedAttributes: { a: ['href', 'target', 'rel'] },
19
+ allowedSchemes: ['http', 'https'],
20
+ allowProtocolRelative: false,
21
+ transformTags: {
22
+ a: (tagName, attribs) => ({ tagName, attribs: { ...attribs, target: '_blank', rel: 'noreferrer' } }),
23
+ },
24
+ })