@shortlink-org/portolan 0.1.0 → 0.2.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 (381) hide show
  1. package/README.md +69 -205
  2. package/cli/init.mjs +285 -0
  3. package/cli/init.test.mjs +158 -0
  4. package/cli/portolan.mjs +37 -73
  5. package/index.html +19 -0
  6. package/package.json +5 -10
  7. package/plugins/README.md +66 -27
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/portolan.json +52 -73
  10. package/public/landing/cats/bug-hunter.webp +0 -0
  11. package/public/landing/cats/compass-nap.webp +0 -0
  12. package/public/landing/cats/diagrammer.webp +0 -0
  13. package/public/landing/cats/docs-reader.webp +0 -0
  14. package/public/landing/cats/laptop.webp +0 -0
  15. package/public/landing/cats/map-inspector.webp +0 -0
  16. package/public/landing/cats/server-break.webp +0 -0
  17. package/public/landing/cats/star-mapper.webp +0 -0
  18. package/public/landing/cats/system-builder.webp +0 -0
  19. package/public/landing/cats/thread-tangle.webp +0 -0
  20. package/public/og.png +0 -0
  21. package/public/readme-header.webp +0 -0
  22. package/schema/portolan.schema.json +180 -0
  23. package/scripts/builtin-plugins.mjs +14 -0
  24. package/scripts/delivery-presets.mjs +353 -0
  25. package/scripts/gen.mjs +37 -4
  26. package/scripts/history.mjs +126 -0
  27. package/scripts/history.test.mjs +110 -0
  28. package/scripts/host-plugins/fetch-bsr.mjs +351 -0
  29. package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
  30. package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
  31. package/scripts/host-plugins/fetch-csr.mjs +386 -0
  32. package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
  33. package/scripts/host-plugins/fetch-git.mjs +379 -0
  34. package/scripts/host-plugins/fetch-git.test.mjs +210 -0
  35. package/scripts/local-api.mjs +22 -4
  36. package/scripts/local-api.test.mjs +84 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +6 -2
  39. package/scripts/plugin-host.mjs +77 -18
  40. package/scripts/plugin-host.test.mjs +50 -1
  41. package/scripts/plugin-wasm-worker.mjs +6 -3
  42. package/scripts/run-builtin.mjs +6 -26
  43. package/scripts/schema.mjs +6 -1
  44. package/src/app/App.tsx +24 -429
  45. package/src/app/Breadcrumbs.test.ts +7 -0
  46. package/src/app/Breadcrumbs.tsx +15 -2
  47. package/src/app/CatalogApp.tsx +436 -0
  48. package/src/app/Sidebar.tsx +0 -1
  49. package/src/chat/ChatPanel.tsx +98 -41
  50. package/src/chat/Composer.tsx +4 -2
  51. package/src/chat/Conversation.tsx +79 -15
  52. package/src/chat/Header.tsx +43 -9
  53. package/src/chat/Starter.tsx +64 -3
  54. package/src/chat/Steps.tsx +1 -1
  55. package/src/chat/flags.test.ts +51 -14
  56. package/src/chat/flags.ts +8 -2
  57. package/src/chat/page-context.test.ts +51 -0
  58. package/src/chat/page-context.ts +128 -0
  59. package/src/chat/prompt.test.ts +49 -1
  60. package/src/chat/prompt.ts +42 -1
  61. package/src/chat/tools.ts +7 -2
  62. package/src/chat/transport.ts +16 -4
  63. package/src/components/ProblemRow.tsx +182 -0
  64. package/src/data.ts +20 -5
  65. package/src/er/ErCanvas.tsx +1 -1
  66. package/src/graph/DependencyGraph.tsx +10 -1
  67. package/src/graph/FocusedEventGraph.tsx +1 -1
  68. package/src/graph/GraphToolbar.tsx +2 -4
  69. package/src/index.css +220 -2
  70. package/src/landing/DraggableReveal.tsx +168 -0
  71. package/src/landing/EstateGraph.tsx +28 -0
  72. package/src/landing/FlowPlayback.tsx +282 -0
  73. package/src/landing/HeroMap.tsx +128 -0
  74. package/src/landing/LandingPage.tsx +757 -0
  75. package/src/landing/ProductFrame.tsx +68 -0
  76. package/src/landing/ProductTour.tsx +371 -0
  77. package/src/landing/catalog.ts +25 -0
  78. package/src/landing/motion.tsx +52 -0
  79. package/src/lib/all-problems.ts +28 -0
  80. package/src/lib/local-api.ts +35 -1
  81. package/src/lib/setup-info.test.ts +8 -0
  82. package/src/lib/setup-info.ts +3 -2
  83. package/src/lib/source-code.ts +2 -1
  84. package/src/likec4/C4View.tsx +5 -0
  85. package/src/likec4/InteractiveView.tsx +5 -1
  86. package/src/map/ContextMapGraph.tsx +47 -20
  87. package/src/pages/ContextMap.tsx +5 -1
  88. package/src/pages/FlowDetail.tsx +24 -7
  89. package/src/pages/Overview.tsx +68 -12
  90. package/src/pages/Problems.tsx +6 -193
  91. package/src/pages/Settings.tsx +133 -108
  92. package/src/pages/settings/DeliverySettings.tsx +229 -0
  93. package/src/pages/settings/PreferencesSettings.tsx +101 -0
  94. package/src/routes.test.ts +20 -0
  95. package/src/routes.ts +13 -1
  96. package/vite.config.ts +12 -8
  97. package/catalog/enum_test.go +0 -46
  98. package/catalog/model.go +0 -932
  99. package/catalog/roundtrip_test.go +0 -185
  100. package/catalog/via_test.go +0 -38
  101. package/go.mod +0 -14
  102. package/go.sum +0 -14
  103. package/internal/commands/cargo.go +0 -154
  104. package/internal/commands/commands.go +0 -143
  105. package/internal/commands/commands_test.go +0 -323
  106. package/internal/commands/gradle.go +0 -77
  107. package/internal/commands/justfile.go +0 -82
  108. package/internal/commands/makefile.go +0 -106
  109. package/internal/commands/maven.go +0 -93
  110. package/internal/commands/packagejson.go +0 -159
  111. package/internal/commands/pyproject.go +0 -169
  112. package/internal/commands/taskfile.go +0 -101
  113. package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
  114. package/internal/commands/testdata/estate/Makefile +0 -33
  115. package/internal/commands/testdata/estate/Taskfile.yml +0 -21
  116. package/internal/commands/testdata/estate/build.gradle.kts +0 -21
  117. package/internal/commands/testdata/estate/justfile +0 -20
  118. package/internal/commands/testdata/estate/package.json +0 -13
  119. package/internal/commands/testdata/estate/pom.xml +0 -29
  120. package/internal/commands/testdata/estate/pyproject.toml +0 -27
  121. package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
  122. package/internal/commands/testdata/golden/commands.json +0 -305
  123. package/internal/gohttp/analyze.go +0 -2500
  124. package/internal/gohttp/endpoints.go +0 -1067
  125. package/internal/gohttp/roots.go +0 -320
  126. package/internal/gohttp/typed.go +0 -143
  127. package/internal/goscan/constants.go +0 -85
  128. package/internal/goscan/goscan_test.go +0 -227
  129. package/internal/goscan/names.go +0 -52
  130. package/internal/goscan/parse_test.go +0 -11
  131. package/internal/goscan/source.go +0 -36
  132. package/internal/goscan/tree.go +0 -155
  133. package/internal/goscan/types.go +0 -99
  134. package/internal/wsdl/ids.go +0 -127
  135. package/internal/wsdl/ids_test.go +0 -21
  136. package/internal/wsdl/model.go +0 -62
  137. package/internal/wsdl/parse.go +0 -920
  138. package/internal/wsdl/parse_test.go +0 -133
  139. package/plugin/describe.go +0 -107
  140. package/plugin/describe_test.go +0 -114
  141. package/plugin/protocol.go +0 -117
  142. package/plugin/schematest/schematest.go +0 -126
  143. package/plugins/extract-adr/describe.go +0 -19
  144. package/plugins/extract-adr/describe_test.go +0 -11
  145. package/plugins/extract-adr/extract.go +0 -153
  146. package/plugins/extract-adr/extract_test.go +0 -350
  147. package/plugins/extract-adr/history.go +0 -99
  148. package/plugins/extract-adr/main.go +0 -65
  149. package/plugins/extract-adr/parse.go +0 -642
  150. package/plugins/extract-adr/parse_test.go +0 -405
  151. package/plugins/extract-asyncapi/describe.go +0 -19
  152. package/plugins/extract-asyncapi/describe_test.go +0 -11
  153. package/plugins/extract-asyncapi/extract.go +0 -315
  154. package/plugins/extract-asyncapi/extract_test.go +0 -197
  155. package/plugins/extract-asyncapi/main.go +0 -48
  156. package/plugins/extract-asyncapi/spec.go +0 -151
  157. package/plugins/extract-commands/describe.go +0 -19
  158. package/plugins/extract-commands/describe_test.go +0 -11
  159. package/plugins/extract-commands/extract.go +0 -79
  160. package/plugins/extract-commands/extract_test.go +0 -92
  161. package/plugins/extract-commands/main.go +0 -55
  162. package/plugins/extract-csr/avro.go +0 -258
  163. package/plugins/extract-csr/describe.go +0 -19
  164. package/plugins/extract-csr/describe_test.go +0 -11
  165. package/plugins/extract-csr/extract.go +0 -338
  166. package/plugins/extract-csr/extract_test.go +0 -374
  167. package/plugins/extract-csr/jsonschema.go +0 -343
  168. package/plugins/extract-csr/lock.go +0 -25
  169. package/plugins/extract-csr/main.go +0 -84
  170. package/plugins/extract-csr/subject.go +0 -102
  171. package/plugins/extract-flows/describe.go +0 -19
  172. package/plugins/extract-flows/describe_test.go +0 -11
  173. package/plugins/extract-flows/extract.go +0 -91
  174. package/plugins/extract-flows/main.go +0 -45
  175. package/plugins/extract-flows/parse.go +0 -593
  176. package/plugins/extract-flows/parse_test.go +0 -204
  177. package/plugins/extract-glossary/describe.go +0 -19
  178. package/plugins/extract-glossary/describe_test.go +0 -11
  179. package/plugins/extract-glossary/extract.go +0 -115
  180. package/plugins/extract-glossary/extract_test.go +0 -220
  181. package/plugins/extract-glossary/main.go +0 -59
  182. package/plugins/extract-glossary/parse.go +0 -214
  183. package/plugins/extract-glossary/parse_test.go +0 -203
  184. package/plugins/extract-go/aggregate.go +0 -214
  185. package/plugins/extract-go/client.go +0 -409
  186. package/plugins/extract-go/client_test.go +0 -266
  187. package/plugins/extract-go/describe.go +0 -19
  188. package/plugins/extract-go/describe_test.go +0 -11
  189. package/plugins/extract-go/enum.go +0 -195
  190. package/plugins/extract-go/enum_test.go +0 -82
  191. package/plugins/extract-go/event.go +0 -99
  192. package/plugins/extract-go/extract.go +0 -191
  193. package/plugins/extract-go/extract_test.go +0 -261
  194. package/plugins/extract-go/flow.go +0 -1441
  195. package/plugins/extract-go/flow_test.go +0 -609
  196. package/plugins/extract-go/httpclient.go +0 -174
  197. package/plugins/extract-go/httpclient_test.go +0 -296
  198. package/plugins/extract-go/ids.go +0 -92
  199. package/plugins/extract-go/layout.go +0 -225
  200. package/plugins/extract-go/layout_test.go +0 -125
  201. package/plugins/extract-go/lifecycle.go +0 -324
  202. package/plugins/extract-go/lifecycle_test.go +0 -108
  203. package/plugins/extract-go/main.go +0 -79
  204. package/plugins/extract-go/operation.go +0 -157
  205. package/plugins/extract-go/source.go +0 -316
  206. package/plugins/extract-go/transport.go +0 -321
  207. package/plugins/extract-go/transport_test.go +0 -145
  208. package/plugins/extract-go/wiring.go +0 -434
  209. package/plugins/extract-go-nats/describe.go +0 -19
  210. package/plugins/extract-go-nats/describe_test.go +0 -11
  211. package/plugins/extract-go-nats/extract.go +0 -177
  212. package/plugins/extract-go-nats/extract_test.go +0 -389
  213. package/plugins/extract-go-nats/index.go +0 -379
  214. package/plugins/extract-go-nats/main.go +0 -42
  215. package/plugins/extract-go-nats/resolve.go +0 -161
  216. package/plugins/extract-go-nats/sites.go +0 -224
  217. package/plugins/extract-graphql/describe.go +0 -19
  218. package/plugins/extract-graphql/describe_test.go +0 -11
  219. package/plugins/extract-graphql/extract.go +0 -433
  220. package/plugins/extract-graphql/extract_test.go +0 -256
  221. package/plugins/extract-graphql/ids.go +0 -49
  222. package/plugins/extract-graphql/lex.go +0 -237
  223. package/plugins/extract-graphql/main.go +0 -51
  224. package/plugins/extract-graphql/parse.go +0 -621
  225. package/plugins/extract-graphql/parse_test.go +0 -122
  226. package/plugins/extract-http-clients/describe.go +0 -19
  227. package/plugins/extract-http-clients/describe_test.go +0 -11
  228. package/plugins/extract-http-clients/extract.go +0 -705
  229. package/plugins/extract-http-clients/extract_test.go +0 -1263
  230. package/plugins/extract-http-clients/main.go +0 -42
  231. package/plugins/extract-openapi/describe.go +0 -19
  232. package/plugins/extract-openapi/describe_test.go +0 -11
  233. package/plugins/extract-openapi/discover.go +0 -243
  234. package/plugins/extract-openapi/extract.go +0 -526
  235. package/plugins/extract-openapi/extract_test.go +0 -545
  236. package/plugins/extract-openapi/main.go +0 -75
  237. package/plugins/extract-openapi/spec.go +0 -350
  238. package/plugins/extract-project/describe.go +0 -19
  239. package/plugins/extract-project/describe_test.go +0 -11
  240. package/plugins/extract-project/extract.go +0 -221
  241. package/plugins/extract-project/extract_test.go +0 -109
  242. package/plugins/extract-project/main.go +0 -41
  243. package/plugins/extract-proto/ast.go +0 -125
  244. package/plugins/extract-proto/consumes.go +0 -77
  245. package/plugins/extract-proto/describe.go +0 -19
  246. package/plugins/extract-proto/extract.go +0 -293
  247. package/plugins/extract-proto/extract_test.go +0 -459
  248. package/plugins/extract-proto/ids.go +0 -89
  249. package/plugins/extract-proto/ids_test.go +0 -57
  250. package/plugins/extract-proto/lex.go +0 -285
  251. package/plugins/extract-proto/main.go +0 -100
  252. package/plugins/extract-proto/module.go +0 -120
  253. package/plugins/extract-proto/parse.go +0 -720
  254. package/plugins/extract-proto/parse_test.go +0 -307
  255. package/plugins/extract-proto/provides.go +0 -236
  256. package/plugins/extract-proto/resolve.go +0 -222
  257. package/plugins/extract-proto/resolve_test.go +0 -119
  258. package/plugins/extract-redis/describe.go +0 -19
  259. package/plugins/extract-redis/describe_test.go +0 -11
  260. package/plugins/extract-redis/extract.go +0 -183
  261. package/plugins/extract-redis/extract_test.go +0 -168
  262. package/plugins/extract-redis/keyspaces.go +0 -469
  263. package/plugins/extract-redis/main.go +0 -44
  264. package/plugins/extract-river/describe.go +0 -19
  265. package/plugins/extract-river/describe_test.go +0 -11
  266. package/plugins/extract-river/extract.go +0 -507
  267. package/plugins/extract-river/extract_test.go +0 -132
  268. package/plugins/extract-river/main.go +0 -42
  269. package/plugins/extract-sql/ddl.go +0 -893
  270. package/plugins/extract-sql/ddl_test.go +0 -401
  271. package/plugins/extract-sql/describe.go +0 -19
  272. package/plugins/extract-sql/describe_test.go +0 -11
  273. package/plugins/extract-sql/layout.go +0 -221
  274. package/plugins/extract-sql/layout_test.go +0 -55
  275. package/plugins/extract-sql/lineage.go +0 -107
  276. package/plugins/extract-sql/main.go +0 -141
  277. package/plugins/extract-sql/maps.go +0 -564
  278. package/plugins/extract-sql/maps_java.go +0 -117
  279. package/plugins/extract-sql/maps_rust.go +0 -333
  280. package/plugins/extract-sql/maps_rust_test.go +0 -70
  281. package/plugins/extract-sql/maps_test.go +0 -204
  282. package/plugins/extract-sql/maps_ts.go +0 -398
  283. package/plugins/extract-sql/maps_ts_test.go +0 -136
  284. package/plugins/extract-sql/projection.go +0 -70
  285. package/plugins/extract-sql/projection_test.go +0 -50
  286. package/plugins/extract-sql/store.go +0 -420
  287. package/plugins/extract-sql/store_test.go +0 -233
  288. package/plugins/extract-sql/view.go +0 -295
  289. package/plugins/extract-watermill/describe.go +0 -19
  290. package/plugins/extract-watermill/describe_test.go +0 -11
  291. package/plugins/extract-watermill/extract.go +0 -1228
  292. package/plugins/extract-watermill/extract_test.go +0 -234
  293. package/plugins/extract-watermill/main.go +0 -41
  294. package/plugins/extract-wsdl/describe.go +0 -19
  295. package/plugins/extract-wsdl/describe_test.go +0 -11
  296. package/plugins/extract-wsdl/extract.go +0 -148
  297. package/plugins/extract-wsdl/extract_test.go +0 -45
  298. package/plugins/extract-wsdl/main.go +0 -53
  299. package/plugins/fetch-bsr/auth.go +0 -114
  300. package/plugins/fetch-bsr/bsr.go +0 -240
  301. package/plugins/fetch-bsr/cache.go +0 -66
  302. package/plugins/fetch-bsr/describe.go +0 -19
  303. package/plugins/fetch-bsr/fetch.go +0 -192
  304. package/plugins/fetch-bsr/fetch_test.go +0 -465
  305. package/plugins/fetch-bsr/lock.go +0 -71
  306. package/plugins/fetch-bsr/main.go +0 -117
  307. package/plugins/fetch-csr/auth.go +0 -95
  308. package/plugins/fetch-csr/cache.go +0 -69
  309. package/plugins/fetch-csr/describe.go +0 -19
  310. package/plugins/fetch-csr/describe_test.go +0 -11
  311. package/plugins/fetch-csr/fetch.go +0 -254
  312. package/plugins/fetch-csr/fetch_test.go +0 -494
  313. package/plugins/fetch-csr/lock.go +0 -95
  314. package/plugins/fetch-csr/main.go +0 -122
  315. package/plugins/fetch-csr/registry.go +0 -209
  316. package/plugins/fetch-git/cache.go +0 -65
  317. package/plugins/fetch-git/describe.go +0 -19
  318. package/plugins/fetch-git/describe_test.go +0 -11
  319. package/plugins/fetch-git/fetch.go +0 -181
  320. package/plugins/fetch-git/fetch_test.go +0 -332
  321. package/plugins/fetch-git/git.go +0 -169
  322. package/plugins/fetch-git/lock.go +0 -72
  323. package/plugins/fetch-git/main.go +0 -127
  324. package/plugins/fetch-git/offline.go +0 -39
  325. package/plugins/fetch-git/pin.go +0 -90
  326. package/plugins/fetch-git/pin_test.go +0 -74
  327. package/plugins/gen-backstage/describe.go +0 -17
  328. package/plugins/gen-backstage/main.go +0 -22
  329. package/plugins/gen-backstage/plugin.go +0 -473
  330. package/plugins/gen-backstage/plugin_test.go +0 -145
  331. package/plugins/gen-backstage.wasm +0 -0
  332. package/plugins/gen-markdown/adr.go +0 -162
  333. package/plugins/gen-markdown/aggregate.go +0 -362
  334. package/plugins/gen-markdown/canonical.go +0 -93
  335. package/plugins/gen-markdown/context.go +0 -90
  336. package/plugins/gen-markdown/coverage_test.go +0 -89
  337. package/plugins/gen-markdown/describe.go +0 -19
  338. package/plugins/gen-markdown/describe_test.go +0 -11
  339. package/plugins/gen-markdown/external.go +0 -71
  340. package/plugins/gen-markdown/flow.go +0 -277
  341. package/plugins/gen-markdown/glossary.go +0 -76
  342. package/plugins/gen-markdown/glossary_test.go +0 -148
  343. package/plugins/gen-markdown/llms.go +0 -302
  344. package/plugins/gen-markdown/main.go +0 -22
  345. package/plugins/gen-markdown/markdown.go +0 -254
  346. package/plugins/gen-markdown/markdown_test.go +0 -100
  347. package/plugins/gen-markdown/module.go +0 -107
  348. package/plugins/gen-markdown/plugin.go +0 -39
  349. package/plugins/gen-markdown/quality_test.go +0 -176
  350. package/plugins/gen-markdown/redis_test.go +0 -38
  351. package/plugins/gen-markdown/render.go +0 -344
  352. package/plugins/gen-markdown/render_test.go +0 -186
  353. package/plugins/gen-markdown/service.go +0 -456
  354. package/plugins/gen-markdown/source.go +0 -177
  355. package/plugins/gen-markdown/store.go +0 -201
  356. package/plugins/gen-markdown.wasm +0 -0
  357. package/plugins/gen-mermaid/describe.go +0 -17
  358. package/plugins/gen-mermaid/main.go +0 -22
  359. package/plugins/gen-mermaid/plugin.go +0 -104
  360. package/plugins/gen-mermaid/plugin_test.go +0 -33
  361. package/plugins/gen-mermaid.wasm +0 -0
  362. package/plugins/openapi/ids.go +0 -261
  363. package/plugins/openapi/ids_test.go +0 -98
  364. package/plugins/verify-codeowners/describe.go +0 -19
  365. package/plugins/verify-codeowners/describe_test.go +0 -11
  366. package/plugins/verify-codeowners/main.go +0 -75
  367. package/plugins/verify-codeowners/match.go +0 -85
  368. package/plugins/verify-codeowners/match_test.go +0 -47
  369. package/plugins/verify-codeowners/owners.go +0 -164
  370. package/plugins/verify-codeowners/owners_test.go +0 -225
  371. package/plugins/verify-codeowners/parse.go +0 -90
  372. package/plugins/verify-codeowners/parse_test.go +0 -62
  373. package/plugins/verify-otel/describe.go +0 -19
  374. package/plugins/verify-otel/describe_test.go +0 -11
  375. package/plugins/verify-otel/main.go +0 -53
  376. package/plugins/verify-otel/match.go +0 -336
  377. package/plugins/verify-otel/otlp.go +0 -200
  378. package/plugins/verify-otel/verify.go +0 -734
  379. package/plugins/verify-otel/verify_test.go +0 -460
  380. /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
  381. /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { useCallback } from "react";
2
+ import type { ViewPadding } from "likec4/react";
2
3
  import { InteractiveView } from "./InteractiveView";
3
4
  import { catalogIdOf } from "./mapping";
4
5
  import { fqn } from "./ids";
@@ -14,11 +15,14 @@ export function C4View({
14
15
  viewId,
15
16
  height = 320,
16
17
  controls = false,
18
+ fitViewPadding,
17
19
  }: {
18
20
  viewId: string;
19
21
  height?: number | string;
20
22
  /** Zoom controls, useful when a whole estate has to fit on a phone. */
21
23
  controls?: boolean;
24
+ /** Space around the fitted model; overview uses a tighter product frame. */
25
+ fitViewPadding?: ViewPadding;
22
26
  }) {
23
27
  const selection = useSelectionStore((s) => s.selection);
24
28
  const source = useSelectionStore((s) => s.source);
@@ -46,6 +50,7 @@ export function C4View({
46
50
  <InteractiveView
47
51
  viewId={viewId}
48
52
  controls={controls}
53
+ fitViewPadding={fitViewPadding}
49
54
  highlightNodes={marked}
50
55
  onNode={onNode}
51
56
  onCanvas={onCanvas}
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo, useRef } from "react";
2
2
  import type { ReactNode } from "react";
3
- import type { DiagramApi } from "likec4/react";
3
+ import type { DiagramApi, ViewPadding } from "likec4/react";
4
4
  import { ReactLikeC4, isLikeC4ViewId } from "./generated";
5
5
  import { useTheme } from "../app/theme";
6
6
  import { Ident } from "../components/Ident";
@@ -12,6 +12,8 @@ export interface InteractiveViewProps {
12
12
  /** Sequence for flows, diagram for element views. */
13
13
  variant?: "diagram" | "sequence";
14
14
  controls?: boolean;
15
+ /** Space around an initially fitted diagram. */
16
+ fitViewPadding?: ViewPadding;
15
17
  walkthrough?: boolean;
16
18
  /** LikeC4 element ids to mark; everything else on the canvas is dimmed. */
17
19
  highlightNodes?: readonly string[];
@@ -71,6 +73,7 @@ export function InteractiveView({
71
73
  viewId,
72
74
  variant = "diagram",
73
75
  controls = false,
76
+ fitViewPadding,
74
77
  walkthrough = false,
75
78
  highlightNodes = [],
76
79
  highlightEdges = [],
@@ -122,6 +125,7 @@ export function InteractiveView({
122
125
  background="dots"
123
126
  controls={controls}
124
127
  fitView
128
+ fitViewPadding={fitViewPadding}
125
129
  pannable
126
130
  zoomable
127
131
  enableDynamicViewWalkthrough={walkthrough}
@@ -61,13 +61,33 @@ interface Drawn {
61
61
  ready: boolean;
62
62
  }
63
63
 
64
+ export interface ContextMapGraphProps {
65
+ catalog: Catalog;
66
+ relations: readonly ContextRelation[];
67
+ /**
68
+ * Whether the wheel zooms the map. On the map's own page it does; on a page
69
+ * the map is embedded in, the wheel belongs to the page and the map is
70
+ * zoomed by pinch or by the fit control instead.
71
+ */
72
+ zoomOnScroll?: boolean;
73
+ /** Tighter geometry for small embedded canvases such as the landing hero. */
74
+ compactLayout?: boolean;
75
+ /** The full map exports files; embedded previews keep the canvas focused. */
76
+ showExport?: boolean;
77
+ /** Embedded previews keep the surrounding estate readable while touring it. */
78
+ inactiveEdgeOpacity?: number;
79
+ inactiveNodeOpacity?: number;
80
+ }
81
+
64
82
  export function ContextMapGraph({
65
83
  catalog,
66
84
  relations,
67
- }: {
68
- catalog: Catalog;
69
- relations: readonly ContextRelation[];
70
- }) {
85
+ zoomOnScroll = true,
86
+ compactLayout = false,
87
+ showExport = true,
88
+ inactiveEdgeOpacity = DIM,
89
+ inactiveNodeOpacity = 0.55,
90
+ }: ContextMapGraphProps) {
71
91
  const navigate = useNavigate();
72
92
  const selectionId = useSelectionStore((s) => s.selection?.id ?? null);
73
93
  const select = useSelectionStore((s) => s.select);
@@ -126,8 +146,8 @@ export function ContextMapGraph({
126
146
  // Far enough apart that a pattern name fits in the gap: "customer /
127
147
  // supplier" is 110px of mono, and a label wider than the space between
128
148
  // two boxes is a label lying across one of them.
129
- edgeLength: 300,
130
- nodeSpacing: 130,
149
+ edgeLength: compactLayout ? 210 : 300,
150
+ nodeSpacing: compactLayout ? 80 : 130,
131
151
  });
132
152
  if (cancelled) return;
133
153
 
@@ -185,7 +205,7 @@ export function ContextMapGraph({
185
205
  return () => {
186
206
  cancelled = true;
187
207
  };
188
- }, [catalog, lines]);
208
+ }, [catalog, compactLayout, lines]);
189
209
 
190
210
  const fitKey = drawn.ready
191
211
  ? `fit-${drawn.nodes.length}-${drawn.edges.length}`
@@ -201,11 +221,14 @@ export function ContextMapGraph({
201
221
  // line and its dimmed label can never disagree.
202
222
  return {
203
223
  ...edge,
204
- style: { ...edge.style, opacity: lit ? 1 : DIM },
224
+ style: {
225
+ ...edge.style,
226
+ opacity: lit ? 1 : inactiveEdgeOpacity,
227
+ },
205
228
  zIndex: lit ? 10 : 0,
206
229
  };
207
230
  });
208
- }, [drawn.edges, selectionId]);
231
+ }, [drawn.edges, inactiveEdgeOpacity, selectionId]);
209
232
 
210
233
  const shownNodes = useMemo(
211
234
  () =>
@@ -214,10 +237,13 @@ export function ContextMapGraph({
214
237
  selected: node.id === selectionId,
215
238
  style: {
216
239
  ...node.style,
217
- opacity: selectionId && selectionId !== node.id ? 0.55 : 1,
240
+ opacity:
241
+ selectionId && selectionId !== node.id
242
+ ? inactiveNodeOpacity
243
+ : 1,
218
244
  },
219
245
  })),
220
- [drawn.nodes, selectionId],
246
+ [drawn.nodes, inactiveNodeOpacity, selectionId],
221
247
  );
222
248
 
223
249
  const onNodeClick = useCallback(
@@ -231,7 +257,7 @@ export function ContextMapGraph({
231
257
  );
232
258
 
233
259
  return (
234
- <div className="relative h-full w-full">
260
+ <div className="canvas-motion relative h-full w-full">
235
261
  {drawn.ready ? null : <DiagramSkeleton />}
236
262
  <ReactFlow
237
263
  nodes={shownNodes}
@@ -246,7 +272,9 @@ export function ContextMapGraph({
246
272
  elementsSelectable
247
273
  proOptions={{ hideAttribution: true }}
248
274
  fitView
249
- fitViewOptions={{ padding: 0.16 }}
275
+ fitViewOptions={{ padding: compactLayout ? 0.06 : 0.16 }}
276
+ zoomOnScroll={zoomOnScroll}
277
+ preventScrolling={zoomOnScroll}
250
278
  minZoom={0.2}
251
279
  maxZoom={1.6}
252
280
  key={fitKey}
@@ -254,18 +282,17 @@ export function ContextMapGraph({
254
282
  <Background gap={20} size={2} />
255
283
  {/* The map has no modes to switch, so its only control is the way
256
284
  out: the drawing as a file, for the page that explains it. */}
257
- <Panel position="top-right">
258
- <ExportSeg name="context-map" />
259
- </Panel>
285
+ {showExport ? (
286
+ <Panel position="top-right">
287
+ <ExportSeg name="context-map" />
288
+ </Panel>
289
+ ) : null}
260
290
  </ReactFlow>
261
291
  </div>
262
292
  );
263
293
  }
264
294
 
265
- export function ContextMapPane(props: {
266
- catalog: Catalog;
267
- relations: readonly ContextRelation[];
268
- }) {
295
+ export function ContextMapPane(props: ContextMapGraphProps) {
269
296
  return (
270
297
  <ReactFlowProvider>
271
298
  <ContextMapGraph {...props} />
@@ -495,7 +495,11 @@ export function ContextMap() {
495
495
  Map
496
496
  </SectionTitle>
497
497
  <div className="h-[340px] overflow-hidden rounded-card border border-line">
498
- <ContextMapPane catalog={catalog} relations={relations} />
498
+ <ContextMapPane
499
+ catalog={catalog}
500
+ relations={relations}
501
+ zoomOnScroll={false}
502
+ />
499
503
  </div>
500
504
  </section>
501
505
 
@@ -8,7 +8,7 @@ import {
8
8
  useState,
9
9
  } from "react";
10
10
  import { useParams } from "react-router";
11
- import { ChevronLeft, ChevronRight } from "lucide-react";
11
+ import { ChevronLeft, ChevronRight, MessageSquare } from "lucide-react";
12
12
  import { saveCanvasImage } from "../lib/export-canvas";
13
13
  import { allRepos, flowContexts, walkSteps } from "../catalog";
14
14
  import type { Flow, Status, Step } from "../catalog";
@@ -55,6 +55,8 @@ import { ContextPill } from "../components/primitives";
55
55
  import { WhatLinksHere } from "../components/WhatLinksHere";
56
56
  import { FlowTrigger } from "../components/FlowTrigger";
57
57
  import { NotFound } from "./NotFound";
58
+ import { useChatAvailable } from "../chat/prefs";
59
+ import { useChatUi } from "../chat/store";
58
60
 
59
61
  /**
60
62
  * The summary, clamped to two lines.
@@ -117,6 +119,7 @@ export function FlowDetail() {
117
119
  const [statusFilter, setStatusFilter] = useState<Status | null>(null);
118
120
  const [exporting, setExporting] = useState(false);
119
121
  const say = useToastStore((s) => s.say);
122
+ const chatAvailable = useChatAvailable();
120
123
  /** Empty means every branch at once — the union, not a run. */
121
124
  const [pathId, setPathId] = useState("");
122
125
  /** Chapters the reader has folded away. Empty means the whole flow is open. */
@@ -441,12 +444,23 @@ export function FlowDetail() {
441
444
  {/* A flow belongs to no single context, so the wash takes the first one
442
445
  it crosses - the context it starts in. */}
443
446
  <div aria-hidden className="hero-wash" style={ctxStyle(contexts[0])} />
444
- <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
445
- <h1 className="text-md font-semibold" title={flow.name}>
447
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
448
+ <h1 className="text-lg font-semibold" title={flow.name}>
446
449
  {flow.name}
447
450
  </h1>
448
451
  <Ident value={flow.id} className="text-muted" />
449
452
  <div className="ml-auto flex items-center gap-2">
453
+ {chatAvailable ? (
454
+ <button
455
+ type="button"
456
+ onClick={() => useChatUi.getState().setOpen(true)}
457
+ className="product-primary"
458
+ title={`Ask the catalog about ${flow.name}`}
459
+ >
460
+ <MessageSquare size={14} aria-hidden />
461
+ ask this flow
462
+ </button>
463
+ ) : null}
450
464
  <PinButton kind="flow" id={flow.id} label={flow.name} />
451
465
  </div>
452
466
  </div>
@@ -465,7 +479,8 @@ export function FlowDetail() {
465
479
  What the row does NOT carry is anything about the picture: the view
466
480
  id and the filter's own count are facts about what is on the canvas
467
481
  right now, and they live with the controls that changed them. */}
468
- <div className="mt-1.5 flex flex-wrap items-center gap-x-2.5 gap-y-1.5">
482
+ <div className="mt-2 flex flex-wrap items-center gap-x-2.5 gap-y-1.5 rounded-control border border-line bg-surface px-2.5 py-1.5 shadow-xs">
483
+ <span className="label shrink-0 text-faint">evidence</span>
469
484
  {flow.trigger ? <FlowTrigger trigger={flow.trigger} /> : null}
470
485
  {flow.includes && flow.includes.length > 0 ? (
471
486
  <span
@@ -501,9 +516,11 @@ export function FlowDetail() {
501
516
  variant="line"
502
517
  className="mt-0"
503
518
  />
504
- {contexts.map((c) => (
505
- <ContextPill key={c} id={c} name={contextName(c)} />
506
- ))}
519
+ <div className="flex flex-wrap items-center gap-1.5 sm:ml-auto">
520
+ {contexts.map((c) => (
521
+ <ContextPill key={c} id={c} name={contextName(c)} />
522
+ ))}
523
+ </div>
507
524
  </div>
508
525
 
509
526
  <FlowToolbar
@@ -3,11 +3,9 @@ import { useState } from "react";
3
3
  import { Link } from "react-router";
4
4
  import { AlertTriangle } from "lucide-react";
5
5
  import { activeCatalogProfile, CATALOG_PATH, catalog, index } from "../data";
6
- import { contextOwners, contextStats, problems, widestFlows } from "../lib/derive";
6
+ import { contextOwners, contextStats, widestFlows } from "../lib/derive";
7
7
  import type { ContextOwner } from "../lib/derive";
8
- import { dataProblems } from "../lib/data-problems";
9
- import { protoProblems } from "../lib/proto-problems";
10
- import { wireProblems } from "../lib/wire-problems";
8
+ import { allProblems } from "../lib/all-problems";
11
9
  import { ctxStyle } from "../lib/context-color";
12
10
  import { middleTruncate, plural } from "../lib/format";
13
11
  import { useCountUp, staggerStyle } from "../lib/motion";
@@ -19,6 +17,7 @@ import { profileContainersViewId, profileLandscapeViewId } from "../likec4/ids";
19
17
  import { LevelSwitch } from "../likec4/levels";
20
18
  import type { C4Level } from "../likec4/levels";
21
19
  import { CatalogStamp } from "../components/CatalogStamp";
20
+ import { ProblemRow } from "../components/ProblemRow";
22
21
  import { RowActions } from "../components/RowActions";
23
22
  import { MachineDocs } from "../components/MachineDocs";
24
23
  import {
@@ -26,6 +25,9 @@ import {
26
25
  ContextPill,
27
26
  } from "../components/primitives";
28
27
 
28
+ /** How many problems the overview lists before pointing at the page that lists them all. */
29
+ const OVERVIEW_PROBLEMS = 4;
30
+
29
31
  /**
30
32
  * A measurement that arrives rather than appears - 200ms, linear, once - and
31
33
  * then takes you to what it measured. A number that counts something the app
@@ -99,12 +101,9 @@ export function Overview() {
99
101
  (count, context) => count + context.services.length,
100
102
  0,
101
103
  );
102
- const issueCount = [
103
- ...problems(catalog),
104
- ...protoProblems(catalog, index),
105
- ...dataProblems(catalog, index),
106
- ...wireProblems(catalog, index),
107
- ].length;
104
+ const issues = allProblems(catalog, index);
105
+ const issueCount = issues.length;
106
+ const errorCount = issues.filter((p) => p.severity === "error").length;
108
107
 
109
108
  return (
110
109
  <div className="h-full overflow-y-auto p-gutter">
@@ -144,7 +143,7 @@ export function Overview() {
144
143
  <HealthMetric
145
144
  value={issueCount}
146
145
  unit="problem"
147
- to={paths.problems()}
146
+ to={issueCount > 0 ? `#${OVERVIEW_ANCHOR.problems}` : paths.problems()}
148
147
  problem
149
148
  />
150
149
  </div>
@@ -178,8 +177,9 @@ export function Overview() {
178
177
  ? profileContainersViewId(activeCatalogProfile.id)
179
178
  : profileLandscapeViewId(activeCatalogProfile.id)
180
179
  }
181
- height={phone ? 300 : level === 2 ? 560 : 400}
180
+ height={phone ? 320 : level === 2 ? 660 : 580}
182
181
  controls={phone}
182
+ fitViewPadding={{ x: 8, y: 8 }}
183
183
  />
184
184
  </section>
185
185
 
@@ -338,6 +338,62 @@ export function Overview() {
338
338
  </div>
339
339
  </section>
340
340
 
341
+ {/* The first few problems, errors first, on the page a reader lands on.
342
+ The count in the health row says how many; this says which, so
343
+ "13 problems" is a list to start on rather than a number to worry
344
+ about. Nothing is shown for a clean estate - the metric already
345
+ says zero, and a heading over an empty list is a dead end. */}
346
+ {issueCount > 0 ? (
347
+ <section id={OVERVIEW_ANCHOR.problems} className="mt-section">
348
+ <SectionTitle
349
+ anchor={OVERVIEW_ANCHOR.problems}
350
+ right={
351
+ <span className="flex items-center gap-2">
352
+ <span className="section-aside">
353
+ <span className="text-unresolved">
354
+ {errorCount} {plural(errorCount, "error")}
355
+ </span>
356
+ {issueCount - errorCount > 0 ? (
357
+ <>
358
+ {" · "}
359
+ <span className="text-declared">
360
+ {issueCount - errorCount}{" "}
361
+ {plural(issueCount - errorCount, "warning")}
362
+ </span>
363
+ </>
364
+ ) : null}
365
+ </span>
366
+ <Link
367
+ to={paths.problems()}
368
+ className="rounded-control px-1 text-accent hover:underline"
369
+ >
370
+ all problems →
371
+ </Link>
372
+ </span>
373
+ }
374
+ >
375
+ Problems
376
+ </SectionTitle>
377
+ <div className="flex max-w-table flex-col gap-2" data-nav-list>
378
+ {issues.slice(0, OVERVIEW_PROBLEMS).map((problem, i) => (
379
+ <ProblemRow
380
+ key={`${problem.kind}:${problem.id}:${problem.peer}`}
381
+ problem={problem}
382
+ index={i}
383
+ />
384
+ ))}
385
+ {issueCount > OVERVIEW_PROBLEMS ? (
386
+ <Link
387
+ to={paths.problems()}
388
+ className="mono self-start rounded-control px-1 text-muted hover:text-ink"
389
+ >
390
+ {issueCount - OVERVIEW_PROBLEMS} more →
391
+ </Link>
392
+ ) : null}
393
+ </div>
394
+ </section>
395
+ ) : null}
396
+
341
397
  <MachineDocs className="mt-section pb-section" />
342
398
  </div>
343
399
  );
@@ -6,62 +6,14 @@
6
6
 
7
7
  import { useDocumentTitle } from "../app/title";
8
8
  import { useMemo, useState } from "react";
9
- import { Link, useSearchParams } from "react-router";
9
+ import { useSearchParams } from "react-router";
10
10
  import { catalog, index } from "../data";
11
- import { edgeCount, problems } from "../lib/derive";
12
- import type { Problem } from "../lib/derive";
13
- import { dataProblems } from "../lib/data-problems";
14
- import { protoProblems } from "../lib/proto-problems";
15
- import { wireProblems } from "../lib/wire-problems";
16
- import { contextVar, ctxStyle } from "../lib/context-color";
11
+ import { edgeCount } from "../lib/derive";
12
+ import { allProblems } from "../lib/all-problems";
13
+ import { contextVar } from "../lib/context-color";
17
14
  import { absoluteTime, plural, relativeTime } from "../lib/format";
18
- import { staggerStyle } from "../lib/motion";
19
- import { KindIcon } from "../components/kind";
20
- import { Ident } from "../components/Ident";
21
15
  import { SectionTitle } from "../components/PageHeader";
22
- import {
23
- eventPath,
24
- servicePath,
25
- storePath,
26
- tablePath,
27
- viewPath,
28
- } from "../routes";
29
-
30
- /** The icon a problem row carries: what the near end of the edge IS. */
31
- const KIND_OF: Record<Problem["kind"], "service" | "event" | "table"> = {
32
- rpc: "service",
33
- consumer: "event",
34
- "cross-service-fk": "table",
35
- "cross-service-lineage": "table",
36
- "shared-store": "table",
37
- "persistence-drift": "table",
38
- "column-type": "table",
39
- "outbox-payload": "table",
40
- "proto-missing": "service",
41
- "shared-channel": "event",
42
- "channel-undeclared": "event",
43
- "channel-unpublished": "service",
44
- "subscription-unresolved": "service",
45
- };
46
-
47
- const KIND_NOTE: Record<Problem["kind"], string> = {
48
- rpc: "the provider of this call is not in the catalog",
49
- consumer: "this consumer of the event is not in the catalog",
50
- "cross-service-fk": "foreign key across a service boundary",
51
- "cross-service-lineage": "a value copied from another service's schema",
52
- "shared-store": "a second service writes this database",
53
- "persistence-drift": "this table no longer carries the aggregate it claims",
54
- "column-type": "column type and domain type disagree",
55
- "outbox-payload": "an outbox with no payload column",
56
- "proto-missing":
57
- "the provider is in the catalog but answers on no such method",
58
- "shared-channel": "a second service publishes on this channel",
59
- "channel-undeclared":
60
- "this event goes out on a channel the service does not declare",
61
- "channel-unpublished": "a declared channel no event of this service names",
62
- "subscription-unresolved":
63
- "nothing in the catalog publishes what this service listens for",
64
- };
16
+ import { ProblemRow } from "../components/ProblemRow";
65
17
 
66
18
  export function Problems() {
67
19
  useDocumentTitle("Problems");
@@ -81,18 +33,7 @@ export function Problems() {
81
33
  // Unresolved edges first, then everything the schema disagrees with. Within
82
34
  // each, errors before warnings: a boundary leak is not the same kind of news
83
35
  // as a column whose type has drifted, and mixing them buries the first.
84
- const all = useMemo(() => {
85
- const found = [
86
- ...problems(catalog),
87
- ...protoProblems(catalog, index),
88
- ...dataProblems(catalog, index),
89
- ...wireProblems(catalog, index),
90
- ];
91
- return [
92
- ...found.filter((p) => p.severity === "error"),
93
- ...found.filter((p) => p.severity === "warning"),
94
- ];
95
- }, []);
36
+ const all = useMemo(() => allProblems(catalog, index), []);
96
37
  // How many edges there were to resolve at all. Zero problems out of zero
97
38
  // edges is not a clean bill of health - nothing crossed a boundary, so
98
39
  // nothing was checked, and saying "every edge resolved" there is a green
@@ -230,131 +171,3 @@ function ClearSkies({ checked }: { checked: number }) {
230
171
  );
231
172
  }
232
173
 
233
- /** Where the near end of a problem lives, by what kind of edge it is. */
234
- function nearPath(problem: Problem): string | null {
235
- switch (problem.kind) {
236
- case "rpc":
237
- // The near end is the CALLING service either way. `rpc` is a call whose
238
- // peer is nobody; this is a call whose peer is known and whose method is
239
- // not - and in both the thing to go and look at is the caller.
240
- case "proto-missing":
241
- return servicePath(problem.service);
242
- case "consumer":
243
- case "channel-undeclared":
244
- return eventPath(problem.id);
245
- // The near end is an event when the service has one on the channel, and
246
- // the service itself when the claim comes from its document alone.
247
- case "shared-channel":
248
- return eventPath(problem.id) ?? servicePath(problem.service);
249
- case "channel-unpublished":
250
- case "subscription-unresolved":
251
- return servicePath(problem.service);
252
- case "shared-store":
253
- case "persistence-drift":
254
- case "outbox-payload":
255
- return tablePath(problem.id);
256
- // The id is "<relation>.<column>", so the relation is its own id minus the
257
- // last segment — and the relation is what the canvas can actually show.
258
- case "cross-service-fk":
259
- case "column-type":
260
- return relationPath(problem.id.split(".").slice(0, -1).join("."));
261
- // Lineage is stated on a column, except when a view states it whole, so
262
- // the id is one or the other and both are looked up the same way.
263
- case "cross-service-lineage":
264
- return (
265
- relationPath(problem.id) ??
266
- relationPath(problem.id.split(".").slice(0, -1).join("."))
267
- );
268
- }
269
- }
270
-
271
- /** A table or a view, whichever the id turns out to name. */
272
- function relationPath(id: string): string | null {
273
- return tablePath(id) ?? viewPath(id);
274
- }
275
-
276
- /** Where the far end lives, when the catalog knows it. */
277
- function peerPath(problem: Problem): string | null {
278
- switch (problem.kind) {
279
- case "cross-service-fk":
280
- return tablePath(problem.peer);
281
- case "cross-service-lineage":
282
- return (
283
- relationPath(problem.peer) ??
284
- relationPath(problem.peer.split(".").slice(0, -1).join("."))
285
- );
286
- case "shared-store":
287
- case "shared-channel":
288
- return servicePath(problem.peer);
289
- case "outbox-payload":
290
- return storePath(problem.peer);
291
- default:
292
- return null;
293
- }
294
- }
295
-
296
- function ProblemRow({ problem, index }: { problem: Problem; index: number }) {
297
- const near = nearPath(problem);
298
- const peerTo = peerPath(problem);
299
- const tone =
300
- problem.severity === "error"
301
- ? "var(--status-unresolved)"
302
- : "var(--status-declared)";
303
-
304
- return (
305
- <div
306
- className="stagger-in flex flex-wrap items-baseline gap-x-3 gap-y-1 rounded-control border px-3 py-2"
307
- style={{ ...staggerStyle(index), borderColor: tone }}
308
- >
309
- <KindIcon kind={KIND_OF[problem.kind]} />
310
- {near ? (
311
- <Link
312
- to={near}
313
- data-nav-item
314
- className="mono rounded-control text-accent hover:underline"
315
- title={problem.id}
316
- >
317
- {problem.id}
318
- </Link>
319
- ) : (
320
- <Ident value={problem.id} />
321
- )}
322
- <span aria-hidden className="text-muted">
323
-
324
- </span>
325
- {peerTo ? (
326
- <Link
327
- to={peerTo}
328
- className="mono rounded-control hover:underline"
329
- style={{ color: tone }}
330
- title={problem.peer}
331
- >
332
- {problem.peer}
333
- </Link>
334
- ) : (
335
- <Ident
336
- value={problem.peer}
337
- className={
338
- problem.severity === "error" ? "text-unresolved" : "text-declared"
339
- }
340
- title={`${problem.peer} — ${KIND_NOTE[problem.kind]}. Click to copy.`}
341
- />
342
- )}
343
- <span className="chip ctx" style={ctxStyle(problem.context)}>
344
- <span aria-hidden className="dot" />
345
- {problem.context}
346
- </span>
347
- <span className="mono ml-auto text-muted">{KIND_NOTE[problem.kind]}</span>
348
- {problem.note ? (
349
- <p className="w-full border-l-2 pl-2 border-line-strong text-muted">
350
- {problem.note}
351
- </p>
352
- ) : null}
353
- {problem.source ? (
354
- <div className="mono w-full text-muted">
355
- <Ident value={problem.source} />
356
- </div>
357
- ) : null}
358
- </div>
359
- );
360
- }