@shortlink-org/portolan 0.2.0 → 0.2.2

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 (109) hide show
  1. package/cli/init.mjs +14 -6
  2. package/cli/init.test.mjs +8 -1
  3. package/cli/portolan.mjs +56 -7
  4. package/cli/portolan.test.mjs +47 -0
  5. package/package.json +7 -6
  6. package/plugins/extract-django/README.md +1 -1
  7. package/plugins/extract-django/extract_test.py +1 -1
  8. package/plugins/extract-django/openapi.py +4 -14
  9. package/plugins/extract-django/openapi_test.py +4 -4
  10. package/plugins/extract-go/options.schema.json +5 -0
  11. package/plugins/extract-project/options.schema.json +21 -0
  12. package/plugins/extract-ts/README.md +1 -1
  13. package/plugins/extract-ts/clients.ts +1 -1
  14. package/plugins/extract-ts/extract.test.ts +2 -2
  15. package/plugins/extract-ts/graphql.test.ts +2 -2
  16. package/plugins/extract-ts/openapi.test.ts +6 -7
  17. package/plugins/extract-ts/openapi.ts +3 -10
  18. package/plugins/portolan-go.wasm +0 -0
  19. package/public/404-cat-v1.webp +0 -0
  20. package/public/cat-about-v1.webp +0 -0
  21. package/public/cat-clear-skies-v1.webp +0 -0
  22. package/public/cat-no-changes-v1.webp +0 -0
  23. package/public/cat-onboarding-v1.webp +0 -0
  24. package/public/cat-trial-success-v1.webp +0 -0
  25. package/schema/portolan.schema.json +61 -0
  26. package/scripts/catalog-sources.mjs +12 -1
  27. package/scripts/catalog-sources.test.mjs +35 -0
  28. package/scripts/gen-likec4.mjs +242 -85
  29. package/scripts/gen-likec4.test.mjs +168 -14
  30. package/scripts/gen.mjs +5 -3
  31. package/scripts/host-plugins/fetch-git.mjs +22 -12
  32. package/scripts/host-plugins/fetch-git.test.mjs +25 -5
  33. package/scripts/local-api.mjs +368 -34
  34. package/scripts/local-api.test.mjs +197 -2
  35. package/scripts/package-smoke.mjs +19 -2
  36. package/scripts/plugin-host.mjs +14 -3
  37. package/scripts/plugin-host.test.mjs +8 -0
  38. package/scripts/schema.mjs +7 -0
  39. package/src/app/CatalogApp.tsx +19 -4
  40. package/src/app/CommandPalette.tsx +7 -2
  41. package/src/app/toast.tsx +11 -7
  42. package/src/catalog.test.ts +33 -0
  43. package/src/catalog.ts +190 -8
  44. package/src/components/CatIllustration.tsx +30 -0
  45. package/src/components/CommitLink.tsx +154 -0
  46. package/src/components/EditorLink.tsx +15 -8
  47. package/src/components/GrpcMethodReference.tsx +93 -0
  48. package/src/components/Markdown.tsx +17 -0
  49. package/src/components/MethodRows.tsx +8 -1
  50. package/src/components/PageHeader.test.ts +24 -0
  51. package/src/components/PageHeader.tsx +34 -0
  52. package/src/components/ShapeRows.tsx +12 -0
  53. package/src/components/SourceDoc.tsx +27 -12
  54. package/src/components/SourcePreview.tsx +124 -36
  55. package/src/enrich.test.ts +209 -2
  56. package/src/enrich.ts +284 -15
  57. package/src/er/RedisSchema.tsx +86 -5
  58. package/src/flow/FlowTable.tsx +19 -4
  59. package/src/flow/StepDetail.tsx +461 -101
  60. package/src/flow/StepRail.tsx +36 -13
  61. package/src/flow/answers-response.test.ts +100 -0
  62. package/src/flow/answers.ts +11 -8
  63. package/src/flow/chapters.ts +2 -1
  64. package/src/flow/labels.ts +10 -0
  65. package/src/flow/mermaid.test.ts +87 -3
  66. package/src/flow/mermaid.ts +26 -14
  67. package/src/index.css +132 -0
  68. package/src/landing/LandingPage.tsx +12 -8
  69. package/src/lib/api.test.ts +4 -6
  70. package/src/lib/build-info.test.ts +29 -0
  71. package/src/lib/build-info.ts +21 -0
  72. package/src/lib/data-model.ts +14 -0
  73. package/src/lib/flow-tree.ts +1 -1
  74. package/src/lib/github-catalog.test.ts +51 -0
  75. package/src/lib/github-catalog.ts +69 -0
  76. package/src/lib/grpc-reference.test.ts +72 -0
  77. package/src/lib/grpc-reference.ts +117 -0
  78. package/src/lib/local-api.ts +17 -4
  79. package/src/lib/product.ts +7 -0
  80. package/src/lib/queries.ts +16 -1
  81. package/src/lib/readme-assets.test.ts +19 -0
  82. package/src/lib/readme-assets.ts +34 -0
  83. package/src/lib/setup-info.test.ts +7 -0
  84. package/src/lib/setup-info.ts +5 -0
  85. package/src/lib/source-link.test.ts +106 -18
  86. package/src/lib/source-link.ts +22 -6
  87. package/src/merge.test.ts +55 -0
  88. package/src/merge.ts +30 -0
  89. package/src/pages/AdrDetail.tsx +3 -2
  90. package/src/pages/AdrIndex.tsx +10 -7
  91. package/src/pages/AggregatePage.tsx +34 -4
  92. package/src/pages/BlockPage.tsx +39 -32
  93. package/src/pages/Changes.tsx +7 -9
  94. package/src/pages/ContextMap.tsx +11 -2
  95. package/src/pages/ContextPage.tsx +11 -4
  96. package/src/pages/FlowIndex.tsx +16 -7
  97. package/src/pages/GraphPage.tsx +21 -7
  98. package/src/pages/Language.tsx +9 -9
  99. package/src/pages/NotFound.tsx +58 -15
  100. package/src/pages/Overview.tsx +6 -0
  101. package/src/pages/Problems.tsx +10 -8
  102. package/src/pages/RegistryIndex.tsx +10 -8
  103. package/src/pages/ServicePage.tsx +1 -1
  104. package/src/pages/Settings.tsx +382 -44
  105. package/src/routes.test.ts +2 -0
  106. package/src/routes.ts +2 -1
  107. package/src/selection/DetailPanel.tsx +213 -113
  108. package/src/testing/setup.ts +14 -0
  109. package/vite.config.ts +5 -1
@@ -12,8 +12,8 @@
12
12
  // whose whole claim is that it does not hold opinions.
13
13
 
14
14
  import { useDocumentTitle } from "../app/title";
15
- import { useMemo } from "react";
16
- import { Link } from "react-router";
15
+ import { useMemo, useState } from "react";
16
+ import { Link, useSearchParams } from "react-router";
17
17
  import { ArrowRight, ArrowLeftRight, Boxes, Minus } from "lucide-react";
18
18
  import { catalog } from "../data";
19
19
  import { plural } from "../lib/format";
@@ -437,6 +437,8 @@ function SeparateWays({
437
437
 
438
438
  export function ContextMap() {
439
439
  useDocumentTitle("Context map");
440
+ const [search, setSearch] = useSearchParams();
441
+ const [tour, setTour] = useState(() => search.get("tour") === "1");
440
442
  const relations = useMemo(() => contextMap(catalog), []);
441
443
  const joined = relations.filter(
442
444
  (r) => r.dependencies.length > 0 || r.shared.length > 0,
@@ -444,6 +446,12 @@ export function ContextMap() {
444
446
  const apart = relations.filter((r) => !joined.includes(r));
445
447
  const wired = joined.length;
446
448
 
449
+ if (catalog.contexts.length === 0) {
450
+ return (
451
+ <div className="h-full overflow-y-auto p-gutter"><div className="mx-auto mt-[12vh] max-w-2xl rounded-card border border-accent bg-canvas p-6 shadow-xs"><div className="label mb-2">context map</div><h1 className="text-xl font-semibold text-ink">Your architecture will appear here</h1><p className="mt-2 text-muted">Connect one project first. Portolan will turn detected components, APIs and dependencies into the first map without asking you to draw it by hand.</p><div className="mt-5 flex flex-wrap gap-2"><Link to={paths.settingsProjects()} className="product-primary">Connect your first project</Link><Link to={paths.settings()} className="tbtn">See setup steps</Link></div></div></div>
452
+ );
453
+ }
454
+
447
455
  return (
448
456
  <div className="h-full overflow-y-auto">
449
457
  <div className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b px-gutter py-3 border-line">
@@ -482,6 +490,7 @@ export function ContextMap() {
482
490
 
483
491
  <div className="flex gap-section p-gutter">
484
492
  <div className="min-w-0 flex-1">
493
+ {tour ? <section className="mb-section overflow-hidden rounded-card border border-accent bg-canvas shadow-xs"><div className="flex items-start justify-between gap-3 border-b border-line bg-surface px-card py-3"><div><div className="font-semibold text-ink">Your first map</div><p className="mt-1 text-muted">Three things are enough to start exploring.</p></div><button type="button" className="tbtn" onClick={() => { setTour(false); search.delete("tour"); setSearch(search, { replace: true }); try { localStorage.setItem("portolan.onboarding.map-tour.v1", "seen"); } catch {} }}>Got it</button></div><ol className="grid md:grid-cols-3"><li className="border-b border-line p-card md:border-r md:border-b-0"><span className="label text-accent">1 · contexts</span><p className="mt-1 text-muted">Each large boundary owns a language and responsibility.</p></li><li className="border-b border-line p-card md:border-r md:border-b-0"><span className="label text-accent">2 · components</span><p className="mt-1 text-muted">Click a context to highlight it; double-click to open its components.</p></li><li className="p-card"><span className="label text-accent">3 · evidence</span><p className="mt-1 text-muted">Relations below explain which calls and events created each connection.</p></li></ol></section> : null}
485
494
  <section id={MAP_ANCHOR.model}>
486
495
  <SectionTitle
487
496
  anchor={MAP_ANCHOR.model}
@@ -144,22 +144,29 @@ export function ContextPage() {
144
144
  className="card card-tagged stagger-in"
145
145
  style={{ ...staggerStyle(i), ...ctxStyle(context.id) }}
146
146
  >
147
- <div className="flex items-baseline gap-2">
147
+ <div className="flex min-w-0 items-baseline gap-2">
148
148
  <Link
149
149
  to={paths.service(context.id, service.slug)}
150
150
  data-nav-item
151
- className="card-link rounded-control font-semibold"
151
+ className="card-link min-w-0 truncate rounded-control font-semibold"
152
152
  title={service.name}
153
153
  >
154
154
  {service.name}
155
155
  </Link>
156
- <Ident value={service.id} className="text-muted" />
157
156
  <RowActions
158
157
  copy={service.id}
159
158
  reveal={service.id}
160
159
  label={service.name}
161
160
  />
162
- <span className="chip">{componentKind(service)}</span>
161
+ </div>
162
+ {/* The id and kind get their own metadata row. Keeping the
163
+ kind after the hidden row actions made it overflow the
164
+ card before those actions were even visible. */}
165
+ <div className="mt-1.5 flex min-w-0 items-center justify-between gap-2">
166
+ <span className="min-w-0 overflow-hidden">
167
+ <Ident value={service.id} className="block truncate text-muted" />
168
+ </span>
169
+ <span className="chip shrink-0">{componentKind(service)}</span>
163
170
  </div>
164
171
  {/* Each number is a link into the part of the service page
165
172
  that lists what it counted. */}
@@ -2,7 +2,7 @@ import { useDocumentTitle } from "../app/title";
2
2
  import { useMemo, useState } from "react";
3
3
  import { Link, useSearchParams } from "react-router";
4
4
  import { ArrowUpDown, X } from "lucide-react";
5
- import { CATALOG_PATH, catalog } from "../data";
5
+ import { catalog } from "../data";
6
6
  import { allRepos, flowContexts, walkSteps } from "../catalog";
7
7
  import { FLOW_HEALTH_NOTE, flowHealth, flowOwner, statusCounts } from "../lib/flow-tree";
8
8
  import type { FlowHealth } from "../lib/flow-tree";
@@ -15,9 +15,10 @@ import { middleTruncate } from "../lib/format";
15
15
  import { staggerStyle } from "../lib/motion";
16
16
  import { Ident } from "../components/Ident";
17
17
  import { RowActions } from "../components/RowActions";
18
- import { Blank, Empty } from "../components/PageHeader";
18
+ import { CapabilityEmpty, Empty } from "../components/PageHeader";
19
19
  import { ContextPill } from "../components/primitives";
20
20
  import { FlowTrigger } from "../components/FlowTrigger";
21
+ import { paths } from "../routes";
21
22
 
22
23
  type Sort = "contexts" | "name" | "steps" | "health";
23
24
 
@@ -189,11 +190,19 @@ export function FlowIndex() {
189
190
 
190
191
  {bare ? (
191
192
  <div className="mt-section">
192
- <Blank where={CATALOG_PATH}>
193
- No flows yet a flow is one run across the estate, reconstructed
194
- from an integration test or written down by hand. Either way it
195
- arrives in <span className="text-ink">flows[]</span>.
196
- </Blank>
193
+ <CapabilityEmpty
194
+ title={catalog.contexts.length === 0 ? "Connect a project before tracing flows" : "No end-to-end flows yet"}
195
+ signal="Signals: integration tests, message handlers or authored flows[] catalog fragments."
196
+ actions={<>
197
+ <Link className="product-primary" to={catalog.contexts.length === 0 ? paths.settingsProjects() : paths.settingsPipeline()}>{catalog.contexts.length === 0 ? "Connect a project" : "Review extraction"}</Link>
198
+ {catalog.contexts.length > 0 ? <Link className="tbtn" to={paths.map()}>Open context map</Link> : null}
199
+ </>}
200
+ >
201
+ {catalog.contexts.length === 0
202
+ ? "Portolan needs a project before it can reconstruct a journey through the architecture."
203
+ : "A flow is one run across the estate. Portolan can reconstruct it from integration evidence or read an authored flow from the catalog."
204
+ }
205
+ </CapabilityEmpty>
197
206
  </div>
198
207
  ) : rows.length === 0 ? (
199
208
  <div className="mt-section">
@@ -1,8 +1,9 @@
1
1
  import { useDocumentTitle } from "../app/title";
2
2
  import { useMemo, useState } from "react";
3
+ import { Link } from "react-router";
3
4
  import { catalog } from "../data";
4
5
  import { plural } from "../lib/format";
5
- import { Empty } from "../components/PageHeader";
6
+ import { CapabilityEmpty, Empty } from "../components/PageHeader";
6
7
  import {
7
8
  bundles,
8
9
  edgeCount,
@@ -14,6 +15,7 @@ import { statusColor, statusDash } from "../graph/theme";
14
15
  import { DependencyGraphPane } from "../graph/DependencyGraph";
15
16
  import type { GraphMode } from "../graph/dependency-layout";
16
17
  import type { Status } from "../catalog";
18
+ import { paths } from "../routes";
17
19
 
18
20
  const STATUSES: { status: Status; note: string }[] = [
19
21
  { status: "verified", note: "consumption observed" },
@@ -183,8 +185,8 @@ export function GraphPage() {
183
185
  <div className="min-h-0 flex-1">
184
186
  {nothing ? (
185
187
  <div className="p-gutter">
186
- <Empty>
187
- {filtered ? (
188
+ {filtered ? (
189
+ <Empty>
188
190
  <>
189
191
  no event passes these filters.{" "}
190
192
  <button
@@ -198,10 +200,22 @@ export function GraphPage() {
198
200
  show everything
199
201
  </button>
200
202
  </>
201
- ) : (
202
- "no events in the catalog yet"
203
- )}
204
- </Empty>
203
+ </Empty>
204
+ ) : (
205
+ <CapabilityEmpty
206
+ title={catalog.contexts.length === 0 ? "Connect a project before mapping dependencies" : "No dependency graph yet"}
207
+ signal="Signals: domain events, message consumers, AsyncAPI channels and integration contracts."
208
+ actions={<>
209
+ <Link className="product-primary" to={paths.settingsProjects()}>{catalog.contexts.length === 0 ? "Connect a project" : "Review project extraction"}</Link>
210
+ {catalog.contexts.length > 0 ? <Link className="tbtn" to={paths.map()}>Open context map</Link> : null}
211
+ </>}
212
+ >
213
+ {catalog.contexts.length === 0
214
+ ? "Portolan needs at least one project before it can discover events and their consumers."
215
+ : "The catalog has components, but no events or consumers to connect yet. Review the selected extractors and the evidence they found."
216
+ }
217
+ </CapabilityEmpty>
218
+ )}
205
219
  </div>
206
220
  ) : (
207
221
  <DependencyGraphPane
@@ -13,9 +13,9 @@
13
13
  import { useDocumentTitle } from "../app/title";
14
14
  import { useMemo, useState } from "react";
15
15
  import { Link, useSearchParams } from "react-router";
16
- import { CATALOG_PATH, catalog, index } from "../data";
16
+ import { catalog, index } from "../data";
17
17
  import { allTerms } from "../catalog";
18
- import { Blank, Empty, SectionTitle } from "../components/PageHeader";
18
+ import { CapabilityEmpty, Empty, SectionTitle } from "../components/PageHeader";
19
19
  import { ContextPill } from "../components/primitives";
20
20
  import { contextVar } from "../lib/context-color";
21
21
  import { plural } from "../lib/format";
@@ -73,13 +73,13 @@ export function Language() {
73
73
  <div className="h-full overflow-y-auto p-gutter">
74
74
  <h1 className="text-lg font-semibold">Language</h1>
75
75
  <div className="mt-section">
76
- <Blank where={CATALOG_PATH}>
77
- No glossary yet — one file per bounded context, beside its README,
78
- saying what each word means inside the boundary and what it does
79
- not. They are read from every <span className="text-ink">GLOSSARY.md</span>{" "}
80
- the manifest points at and land in{" "}
81
- <span className="text-ink">terms[]</span>.
82
- </Blank>
76
+ <CapabilityEmpty
77
+ title="No bounded-context language yet"
78
+ signal="Signal: GLOSSARY.md files discovered beside project documentation terms[]."
79
+ actions={<Link className="product-primary" to={catalog.contexts.length === 0 ? paths.settingsProjects() : paths.settingsPipeline()}>{catalog.contexts.length === 0 ? "Connect a project" : "Review extraction"}</Link>}
80
+ >
81
+ A glossary records what a word means inside one boundary—and what it does not. Portolan combines those files to reveal shared terms and conflicting meanings.
82
+ </CapabilityEmpty>
83
83
  </div>
84
84
  </div>
85
85
  );
@@ -1,4 +1,5 @@
1
- import { Link } from "react-router";
1
+ import { House, Search } from "lucide-react";
2
+ import { Link, useLocation } from "react-router";
2
3
  import { useDocumentTitle } from "../app/title";
3
4
 
4
5
  /**
@@ -34,21 +35,63 @@ export function NotFound({ kind, id }: { kind: string; id?: string }) {
34
35
  );
35
36
  }
36
37
 
37
- export function NotFoundPage() {
38
- useDocumentTitle("Not found");
38
+ function LostExplorer() {
39
39
  return (
40
- <div className="glow p-gutter">
41
- <h1 className="text-lg font-semibold">Not found</h1>
42
- <p className="mt-3 text-muted">
43
- off the edge of the chart no route runs through here
44
- </p>
45
- <SearchHint />
46
- <Link
47
- to="/"
48
- className="mono mt-6 inline-block rounded-control text-accent hover:underline"
49
- >
50
- ← overview
51
- </Link>
40
+ <div className="not-found-art">
41
+ <img
42
+ src={`${import.meta.env.BASE_URL}404-cat-v1.webp`}
43
+ alt="An explorer cat studying a folded map with a missing route"
44
+ className="not-found-cat"
45
+ draggable={false}
46
+ />
47
+ </div>
48
+ );
49
+ }
50
+
51
+ export function NotFoundPage({ onOpenSearch }: { onOpenSearch: () => void }) {
52
+ useDocumentTitle("Route not found");
53
+ const { pathname } = useLocation();
54
+
55
+ return (
56
+ <div className="not-found-page pane">
57
+ <section className="not-found-state" aria-labelledby="not-found-title">
58
+ <LostExplorer />
59
+
60
+ <div className="not-found-copy">
61
+ <p className="label text-accent">uncharted route</p>
62
+ <h1 id="not-found-title" className="mt-2 text-xl font-semibold">
63
+ Route not found
64
+ </h1>
65
+ <p className="mt-3 text-muted">
66
+ This path runs beyond the current catalog. It may have moved or
67
+ been renamed since the chart was drawn.
68
+ </p>
69
+
70
+ <div className="not-found-request mt-5">
71
+ <span className="mono text-faint">requested</span>
72
+ <code className="mono trunc text-ink" title={pathname}>
73
+ {pathname}
74
+ </code>
75
+ </div>
76
+
77
+ <div className="mt-6 flex flex-wrap items-center gap-2">
78
+ <button
79
+ type="button"
80
+ className="product-primary"
81
+ onClick={onOpenSearch}
82
+ aria-keyshortcuts="Meta+K Control+K"
83
+ >
84
+ <Search size={15} aria-hidden />
85
+ <span>Search catalog</span>
86
+ <kbd className="not-found-key">⌘K</kbd>
87
+ </button>
88
+ <Link to="/" className="tbtn py-1.5 text-ink">
89
+ <House size={15} aria-hidden />
90
+ Back to overview
91
+ </Link>
92
+ </div>
93
+ </div>
94
+ </section>
52
95
  </div>
53
96
  );
54
97
  }
@@ -105,6 +105,12 @@ export function Overview() {
105
105
  const issueCount = issues.length;
106
106
  const errorCount = issues.filter((p) => p.severity === "error").length;
107
107
 
108
+ if (catalog.contexts.length === 0) {
109
+ return (
110
+ <div className="h-full overflow-y-auto p-gutter"><div className="mx-auto mt-[12vh] max-w-2xl rounded-card border border-accent bg-canvas p-6 shadow-xs"><div className="label mb-2">welcome to Portolan</div><h1 className="text-xl font-semibold text-ink">Turn your first project into an architecture catalog</h1><p className="mt-2 text-muted">Start with one repository. Portolan detects its components and contracts, lets you review the evidence, then generates a navigable map.</p><div className="mt-5 flex flex-wrap gap-2"><Link to={paths.settingsProjects()} className="product-primary">Connect your first project</Link><Link to={paths.settings()} className="tbtn">Open setup guide</Link></div><div className="mono mt-5 border-t border-line pt-4 text-muted">source → context → evidence → preview → map</div></div></div>
111
+ );
112
+ }
113
+
108
114
  return (
109
115
  <div className="h-full overflow-y-auto p-gutter">
110
116
  <div className="flex flex-col items-start gap-1 sm:flex-row sm:items-start sm:gap-x-4">
@@ -14,6 +14,7 @@ import { contextVar } from "../lib/context-color";
14
14
  import { absoluteTime, plural, relativeTime } from "../lib/format";
15
15
  import { SectionTitle } from "../components/PageHeader";
16
16
  import { ProblemRow } from "../components/ProblemRow";
17
+ import { CatEmptyState } from "../components/CatIllustration";
17
18
 
18
19
  export function Problems() {
19
20
  useDocumentTitle("Problems");
@@ -160,14 +161,15 @@ export function Problems() {
160
161
  /** The one line the reader wants to see. Nothing else earns the space. */
161
162
  function ClearSkies({ checked }: { checked: number }) {
162
163
  return (
163
- <div className="empty mt-section max-w-table">
164
+ <CatEmptyState
165
+ scene="clear"
166
+ title={checked === 0 ? "Nothing to resolve yet" : "Clear skies"}
167
+ className="mt-section max-w-prose"
168
+ meta={<span title={absoluteTime(catalog.generatedAt)}>last checked {relativeTime(catalog.generatedAt)}</span>}
169
+ >
164
170
  {checked === 0
165
- ? "Nothing to resolve yet — no service calls another, and no event has a consumer."
166
- : `Clear skies — all ${checked} ${plural(checked, "edge")} resolved.`}
167
- <span className="ml-2" title={absoluteTime(catalog.generatedAt)}>
168
- last checked {relativeTime(catalog.generatedAt)}
169
- </span>
170
- </div>
171
+ ? "No service calls another, and no event has a consumer."
172
+ : `All ${checked} ${plural(checked, "edge")} resolved.`}
173
+ </CatEmptyState>
171
174
  );
172
175
  }
173
-
@@ -1,8 +1,9 @@
1
1
  import { useDocumentTitle } from "../app/title";
2
2
  import { useMemo } from "react";
3
- import { CATALOG_PATH, catalog, index } from "../data";
3
+ import { Link } from "react-router";
4
+ import { catalog, index } from "../data";
4
5
  import type { ProtoModule } from "../catalog";
5
- import { Blank, Empty } from "../components/PageHeader";
6
+ import { CapabilityEmpty, Empty } from "../components/PageHeader";
6
7
  import { RowActions } from "../components/RowActions";
7
8
  import { DataTable } from "../table/DataTable";
8
9
  import type { ColumnSpec } from "../table/types";
@@ -88,12 +89,13 @@ export function RegistryIndex() {
88
89
 
89
90
  {bare ? (
90
91
  <div className="mt-section">
91
- <Blank where={CATALOG_PATH}>
92
- No schema modules yet — a module is the .proto files an interface
93
- was declared in, named and versioned by a registry. They are read
94
- from the protos in each repository and land in{" "}
95
- <span className="text-ink">modules[]</span>.
96
- </Blank>
92
+ <CapabilityEmpty
93
+ title="No schema modules discovered yet"
94
+ signal="Signals: .proto files and registry metadata from BSR or local modules → modules[]."
95
+ actions={<Link className="product-primary" to={catalog.contexts.length === 0 ? paths.settingsProjects() : paths.settingsPipeline()}>{catalog.contexts.length === 0 ? "Connect a project" : "Review extraction"}</Link>}
96
+ >
97
+ Registry modules show where shared contracts are published, pinned and consumed. Review the project’s protobuf and registry extractors to populate this index.
98
+ </CapabilityEmpty>
97
99
  </div>
98
100
  ) : (
99
101
  <div className="mt-section max-w-table">
@@ -284,7 +284,7 @@ export function ServicePage() {
284
284
  height={300}
285
285
  />
286
286
  <div className="mt-section" />
287
- <Markdown mermaid>{service.readme}</Markdown>
287
+ <Markdown mermaid sourceRoot={service.path}>{service.readme}</Markdown>
288
288
  {/* What to type to build, test and run it. Under the README
289
289
  rather than in a tab of its own: the list is short, it is the
290
290
  first thing a reader new to the checkout wants, and the README