@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
@@ -0,0 +1,128 @@
1
+ // What the reader is looking at when they open Ask the catalog.
2
+ //
3
+ // The route is navigation context, not evidence. It gives the model a useful
4
+ // referent for “this flow” and lets the empty conversation offer questions
5
+ // about the thing already on screen. The answer must still read the generated
6
+ // markdown page before it claims anything about that thing.
7
+
8
+ import type { Kind } from "../lib/kinds";
9
+ import type { PaletteItem } from "../lib/palette";
10
+ import { catalog } from "../data";
11
+ import { paletteItems } from "../lib/palette";
12
+
13
+ export interface ChatPageContext {
14
+ kind: Kind;
15
+ id: string;
16
+ title: string;
17
+ /** Path relative to the profile's docs directory, as llms.txt spells it. */
18
+ docPath: string | null;
19
+ }
20
+
21
+ const items = paletteItems(catalog);
22
+
23
+ function docsPath(item: PaletteItem): string | null {
24
+ if (!item.path) return null;
25
+ const parts = item.path.split("?")[0]?.split("#")[0]?.split("/").filter(Boolean) ?? [];
26
+
27
+ switch (item.kind) {
28
+ case "context":
29
+ return `${item.id}/README.md`;
30
+ case "service":
31
+ return parts.length >= 3 ? `${parts[1]}/${parts[2]}/README.md` : null;
32
+ case "aggregate":
33
+ case "event":
34
+ case "vo":
35
+ case "entity":
36
+ case "enum":
37
+ case "command":
38
+ case "query":
39
+ return parts.length >= 4
40
+ ? `${parts[1]}/${parts[2]}/aggregates/${parts[3]}.md`
41
+ : null;
42
+ case "flow":
43
+ return `flows/${item.name}.md`;
44
+ case "adr":
45
+ return `adr/${item.id}.md`;
46
+ case "external":
47
+ return parts[1] ? `externals/${parts[1]}.md` : null;
48
+ case "module":
49
+ return parts[1] ? `modules/${parts[1]}.md` : null;
50
+ default:
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function titleOf(item: PaletteItem): string {
56
+ if (item.kind === "flow" || item.kind === "adr" || item.kind === "context") {
57
+ return item.detail;
58
+ }
59
+ return item.name;
60
+ }
61
+
62
+ /** The exact catalog entity page at this route, if there is one. */
63
+ export function pageContextFrom(
64
+ candidates: readonly PaletteItem[],
65
+ pathname: string,
66
+ ): ChatPageContext | null {
67
+ const item = candidates.find((candidate) => candidate.path === pathname);
68
+ if (!item) return null;
69
+ return {
70
+ kind: item.kind,
71
+ id: item.id,
72
+ title: titleOf(item),
73
+ docPath: docsPath(item),
74
+ };
75
+ }
76
+
77
+ export function pageContext(pathname: string): ChatPageContext | null {
78
+ return pageContextFrom(items, pathname);
79
+ }
80
+
81
+ /** Questions that use the current page instead of making the reader restate it. */
82
+ export function contextQuestions(context: ChatPageContext): string[] {
83
+ const id = context.id;
84
+ switch (context.kind) {
85
+ case "flow":
86
+ return [
87
+ `Summarize ${id} and its trust status.`,
88
+ `Which contexts and services does ${id} cross?`,
89
+ `Which steps in ${id} are declared rather than verified?`,
90
+ `Which decisions explain ${id}?`,
91
+ ];
92
+ case "context":
93
+ return [
94
+ `What does ${id} own?`,
95
+ `Which services and aggregates are inside ${id}?`,
96
+ `Which flows cross the boundary of ${id}?`,
97
+ `Which problems and decisions touch ${id}?`,
98
+ ];
99
+ case "service":
100
+ return [
101
+ `Summarize ${id} and what it owns.`,
102
+ `What does ${id} provide and consume?`,
103
+ `Which flows pass through ${id}?`,
104
+ `Which decisions and problems touch ${id}?`,
105
+ ];
106
+ case "aggregate":
107
+ return [
108
+ `Summarize the invariants of ${id}.`,
109
+ `Show the lifecycle of ${id}.`,
110
+ `Which commands, queries and events belong to ${id}?`,
111
+ `Which flows change ${id}?`,
112
+ ];
113
+ case "adr":
114
+ return [
115
+ `Summarize ${id} and its consequences.`,
116
+ `Which parts of the catalog does ${id} affect?`,
117
+ `Which flows are explained by ${id}?`,
118
+ `What evidence supports ${id}?`,
119
+ ];
120
+ default:
121
+ return [
122
+ `Summarize ${id}.`,
123
+ `What is connected to ${id}?`,
124
+ `Which flows touch ${id}?`,
125
+ `Which decisions and problems touch ${id}?`,
126
+ ];
127
+ }
128
+ }
@@ -1,5 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { clipPage, instructions, pagePath, PAGE_LIMIT } from "./prompt";
2
+ import {
3
+ clipPage,
4
+ instructions,
5
+ pagePath,
6
+ PAGE_LIMIT,
7
+ promptPageContext,
8
+ } from "./prompt";
3
9
 
4
10
  describe("pagePath", () => {
5
11
  it("accepts a path as the index spells it", () => {
@@ -40,4 +46,46 @@ describe("instructions", () => {
40
46
  expect(text).toContain("read_page");
41
47
  expect(text.endsWith("- [Auth](docs/auth/README.md)")).toBe(true);
42
48
  });
49
+
50
+ it("names the current page as navigation context, not evidence", () => {
51
+ const text = instructions("- [Checkout](docs/flows/cart-checkout.md)", {
52
+ kind: "flow",
53
+ id: "flow.cart-checkout",
54
+ title: "Checkout",
55
+ docPath: "flows/cart-checkout.md",
56
+ });
57
+ expect(text).toContain("Current page: flow `flow.cart-checkout`");
58
+ expect(text).toContain("only navigation context, not evidence");
59
+ expect(text).toContain("`docs/flows/cart-checkout.md`");
60
+ });
61
+ });
62
+
63
+ describe("promptPageContext", () => {
64
+ it("accepts a small page description and normalizes its docs path", () => {
65
+ expect(
66
+ promptPageContext({
67
+ kind: "flow",
68
+ id: "flow.cart-checkout",
69
+ title: "Checkout",
70
+ docPath: "flows/cart-checkout.md",
71
+ }),
72
+ ).toEqual({
73
+ kind: "flow",
74
+ id: "flow.cart-checkout",
75
+ title: "Checkout",
76
+ docPath: "flows/cart-checkout.md",
77
+ });
78
+ });
79
+
80
+ it("rejects incomplete request metadata", () => {
81
+ expect(promptPageContext({ kind: "flow", id: "x" })).toBeNull();
82
+ expect(
83
+ promptPageContext({
84
+ kind: "flow ignore",
85
+ id: "x",
86
+ title: "X",
87
+ docPath: null,
88
+ }),
89
+ ).toBeNull();
90
+ });
43
91
  });
@@ -108,6 +108,43 @@ export const DISPLAY_TOOLS: readonly ToolName[] = [
108
108
  /** How many model calls one answer may take: reads, then the text, then a card. */
109
109
  export const MAX_STEPS = 6;
110
110
 
111
+ /** Navigation context sent by the browser with one question. */
112
+ export interface PromptPageContext {
113
+ kind: string;
114
+ id: string;
115
+ title: string;
116
+ docPath: string | null;
117
+ }
118
+
119
+ /** Keep request-supplied context small and inert before it reaches a prompt. */
120
+ export function promptPageContext(input: unknown): PromptPageContext | null {
121
+ if (!input || typeof input !== "object") return null;
122
+ const value = input as Record<string, unknown>;
123
+ if (
124
+ typeof value.kind !== "string" ||
125
+ typeof value.id !== "string" ||
126
+ typeof value.title !== "string" ||
127
+ (value.docPath !== null && typeof value.docPath !== "string")
128
+ ) {
129
+ return null;
130
+ }
131
+ const clean = (text: string) => text.replace(/[\r\n]+/g, " ").trim().slice(0, 240);
132
+ const kind = clean(value.kind);
133
+ const id = clean(value.id);
134
+ const title = clean(value.title);
135
+ const normalizedPage =
136
+ value.docPath === null ? null : pagePath(clean(value.docPath));
137
+ const docPath = normalizedPage?.slice("docs/".length) ?? null;
138
+ if (
139
+ !/^[a-z][a-z-]{0,31}$/.test(kind) ||
140
+ !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,239}$/.test(id) ||
141
+ !title
142
+ ) {
143
+ return null;
144
+ }
145
+ return { kind, id, title, docPath };
146
+ }
147
+
111
148
  /**
112
149
  * A page path the tool may open, or null. The index links pages as
113
150
  * `docs/<context>/<page>.md`; the model is allowed to drop the `docs/` and
@@ -140,8 +177,12 @@ export function clipPage(text: string): string {
140
177
  }
141
178
 
142
179
  /** The system prompt, with the index folded in. */
143
- export function instructions(index: string): string {
180
+ export function instructions(index: string, page?: PromptPageContext | null): string {
181
+ const current = page
182
+ ? `\nCurrent page: ${page.kind} \`${page.id}\`. Treat “this”, “it” and similar references as this catalog entity. This route is only navigation context, not evidence: read ${page.docPath ? `\`docs/${page.docPath}\`` : "the relevant page from the index"} before making claims about it.\n`
183
+ : "";
144
184
  return `You are the guide to an architecture catalog: bounded contexts, services, aggregates, events, flows and decisions (ADRs). The index of its pages is below.
185
+ ${current}
145
186
 
146
187
  How to answer:
147
188
  - Answer in the language the question was asked in.
package/src/chat/tools.ts CHANGED
@@ -6,14 +6,19 @@
6
6
 
7
7
  import { jsonSchema, tool } from "ai";
8
8
  import { clipPage, pagePath, TOOL_SPECS } from "./prompt";
9
+ import { activeCatalogProfile } from "../data";
9
10
 
10
11
  const base = import.meta.env.BASE_URL;
12
+ const profileSuffix =
13
+ activeCatalogProfile.id === "portolan" ? "" : `${activeCatalogProfile.id}/`;
14
+ const docsBase = `${base}docs/${profileSuffix}`;
15
+ const indexUrl = profileSuffix ? `${docsBase}llms.txt` : `${base}llms.txt`;
11
16
 
12
17
  /** One page of the generated docs, as the model gets it. */
13
18
  export async function readPage(raw: unknown): Promise<string> {
14
19
  const path = pagePath(raw);
15
20
  if (!path) return "That is not a page of the catalog. Use a path from the index.";
16
- const response = await fetch(`${base}${path}`);
21
+ const response = await fetch(`${docsBase}${path.slice("docs/".length)}`);
17
22
  if (!response.ok) return `No page at ${path}. Use a path from the index.`;
18
23
  return clipPage(await response.text());
19
24
  }
@@ -21,7 +26,7 @@ export async function readPage(raw: unknown): Promise<string> {
21
26
  /** llms.txt, once per session. */
22
27
  let indexPromise: Promise<string> | null = null;
23
28
  export function loadIndex(): Promise<string> {
24
- indexPromise ??= fetch(`${base}llms.txt`).then((response) => {
29
+ indexPromise ??= fetch(indexUrl).then((response) => {
25
30
  if (!response.ok) {
26
31
  indexPromise = null;
27
32
  throw new Error("the catalog index (llms.txt) could not be read");
@@ -14,7 +14,11 @@ import {
14
14
  import type { ChatTransport, UIMessage, UIMessageChunk } from "ai";
15
15
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
16
16
  import type { ChatRoute, OwnModel } from "./flags";
17
- import { instructions, MAX_STEPS } from "./prompt";
17
+ import {
18
+ instructions,
19
+ MAX_STEPS,
20
+ promptPageContext,
21
+ } from "./prompt";
18
22
  import { useChatUi } from "./store";
19
23
  import { browserTools, loadIndex } from "./tools";
20
24
 
@@ -22,10 +26,11 @@ type Answering = Extract<ChatRoute, { kind: "proxy" | "own" }>;
22
26
 
23
27
  class OwnTransport implements ChatTransport<UIMessage> {
24
28
  private inner: Promise<ChatTransport<UIMessage>> | null = null;
29
+ private contextKey = "";
25
30
 
26
31
  constructor(private readonly own: OwnModel) {}
27
32
 
28
- private async build(): Promise<ChatTransport<UIMessage>> {
33
+ private async build(pageContext: unknown): Promise<ChatTransport<UIMessage>> {
29
34
  const ui = useChatUi.getState();
30
35
  ui.setPhase("index");
31
36
  let index: string;
@@ -41,7 +46,7 @@ class OwnTransport implements ChatTransport<UIMessage> {
41
46
  });
42
47
  const agent = new ToolLoopAgent({
43
48
  model: provider.chatModel(this.own.model.trim()),
44
- instructions: instructions(index),
49
+ instructions: instructions(index, promptPageContext(pageContext)),
45
50
  tools: browserTools(),
46
51
  stopWhen: stepCountIs(MAX_STEPS),
47
52
  });
@@ -51,7 +56,14 @@ class OwnTransport implements ChatTransport<UIMessage> {
51
56
  async sendMessages(
52
57
  options: Parameters<ChatTransport<UIMessage>["sendMessages"]>[0],
53
58
  ): Promise<ReadableStream<UIMessageChunk>> {
54
- this.inner ??= this.build().catch((error: unknown) => {
59
+ const pageContext = (options.body as { pageContext?: unknown } | undefined)
60
+ ?.pageContext;
61
+ const nextContextKey = JSON.stringify(promptPageContext(pageContext));
62
+ if (this.contextKey !== nextContextKey) {
63
+ this.inner = null;
64
+ this.contextKey = nextContextKey;
65
+ }
66
+ this.inner ??= this.build(pageContext).catch((error: unknown) => {
55
67
  this.inner = null;
56
68
  throw error;
57
69
  });
@@ -0,0 +1,182 @@
1
+ // One problem, as a row: what the near end is, where both ends live, and
2
+ // the words for what is wrong with the edge between them. The problems page
3
+ // lists every one of these; the overview shows the first few.
4
+
5
+ import { Link } from "react-router";
6
+ import type { Problem } from "../lib/derive";
7
+ import { ctxStyle } from "../lib/context-color";
8
+ import { staggerStyle } from "../lib/motion";
9
+ import {
10
+ eventPath,
11
+ servicePath,
12
+ storePath,
13
+ tablePath,
14
+ viewPath,
15
+ } from "../routes";
16
+ import { KindIcon } from "./kind";
17
+ import { Ident } from "./Ident";
18
+
19
+ /** The icon a problem row carries: what the near end of the edge IS. */
20
+ const KIND_OF: Record<Problem["kind"], "service" | "event" | "table"> = {
21
+ rpc: "service",
22
+ consumer: "event",
23
+ "cross-service-fk": "table",
24
+ "cross-service-lineage": "table",
25
+ "shared-store": "table",
26
+ "persistence-drift": "table",
27
+ "column-type": "table",
28
+ "outbox-payload": "table",
29
+ "proto-missing": "service",
30
+ "shared-channel": "event",
31
+ "channel-undeclared": "event",
32
+ "channel-unpublished": "service",
33
+ "subscription-unresolved": "service",
34
+ };
35
+
36
+ const KIND_NOTE: Record<Problem["kind"], string> = {
37
+ rpc: "the provider of this call is not in the catalog",
38
+ consumer: "this consumer of the event is not in the catalog",
39
+ "cross-service-fk": "foreign key across a service boundary",
40
+ "cross-service-lineage": "a value copied from another service's schema",
41
+ "shared-store": "a second service writes this database",
42
+ "persistence-drift": "this table no longer carries the aggregate it claims",
43
+ "column-type": "column type and domain type disagree",
44
+ "outbox-payload": "an outbox with no payload column",
45
+ "proto-missing":
46
+ "the provider is in the catalog but answers on no such method",
47
+ "shared-channel": "a second service publishes on this channel",
48
+ "channel-undeclared":
49
+ "this event goes out on a channel the service does not declare",
50
+ "channel-unpublished": "a declared channel no event of this service names",
51
+ "subscription-unresolved":
52
+ "nothing in the catalog publishes what this service listens for",
53
+ };
54
+
55
+ /** Where the near end of a problem lives, by what kind of edge it is. */
56
+ function nearPath(problem: Problem): string | null {
57
+ switch (problem.kind) {
58
+ case "rpc":
59
+ // The near end is the CALLING service either way. `rpc` is a call whose
60
+ // peer is nobody; this is a call whose peer is known and whose method is
61
+ // not - and in both the thing to go and look at is the caller.
62
+ case "proto-missing":
63
+ return servicePath(problem.service);
64
+ case "consumer":
65
+ case "channel-undeclared":
66
+ return eventPath(problem.id);
67
+ // The near end is an event when the service has one on the channel, and
68
+ // the service itself when the claim comes from its document alone.
69
+ case "shared-channel":
70
+ return eventPath(problem.id) ?? servicePath(problem.service);
71
+ case "channel-unpublished":
72
+ case "subscription-unresolved":
73
+ return servicePath(problem.service);
74
+ case "shared-store":
75
+ case "persistence-drift":
76
+ case "outbox-payload":
77
+ return tablePath(problem.id);
78
+ // The id is "<relation>.<column>", so the relation is its own id minus the
79
+ // last segment — and the relation is what the canvas can actually show.
80
+ case "cross-service-fk":
81
+ case "column-type":
82
+ return relationPath(problem.id.split(".").slice(0, -1).join("."));
83
+ // Lineage is stated on a column, except when a view states it whole, so
84
+ // the id is one or the other and both are looked up the same way.
85
+ case "cross-service-lineage":
86
+ return (
87
+ relationPath(problem.id) ??
88
+ relationPath(problem.id.split(".").slice(0, -1).join("."))
89
+ );
90
+ }
91
+ }
92
+
93
+ /** A table or a view, whichever the id turns out to name. */
94
+ function relationPath(id: string): string | null {
95
+ return tablePath(id) ?? viewPath(id);
96
+ }
97
+
98
+ /** Where the far end lives, when the catalog knows it. */
99
+ function peerPath(problem: Problem): string | null {
100
+ switch (problem.kind) {
101
+ case "cross-service-fk":
102
+ return tablePath(problem.peer);
103
+ case "cross-service-lineage":
104
+ return (
105
+ relationPath(problem.peer) ??
106
+ relationPath(problem.peer.split(".").slice(0, -1).join("."))
107
+ );
108
+ case "shared-store":
109
+ case "shared-channel":
110
+ return servicePath(problem.peer);
111
+ case "outbox-payload":
112
+ return storePath(problem.peer);
113
+ default:
114
+ return null;
115
+ }
116
+ }
117
+
118
+ export function ProblemRow({ problem, index }: { problem: Problem; index: number }) {
119
+ const near = nearPath(problem);
120
+ const peerTo = peerPath(problem);
121
+ const tone =
122
+ problem.severity === "error"
123
+ ? "var(--status-unresolved)"
124
+ : "var(--status-declared)";
125
+
126
+ return (
127
+ <div
128
+ className="stagger-in flex flex-wrap items-baseline gap-x-3 gap-y-1 rounded-control border px-3 py-2"
129
+ style={{ ...staggerStyle(index), borderColor: tone }}
130
+ >
131
+ <KindIcon kind={KIND_OF[problem.kind]} />
132
+ {near ? (
133
+ <Link
134
+ to={near}
135
+ data-nav-item
136
+ className="mono rounded-control text-accent hover:underline"
137
+ title={problem.id}
138
+ >
139
+ {problem.id}
140
+ </Link>
141
+ ) : (
142
+ <Ident value={problem.id} />
143
+ )}
144
+ <span aria-hidden className="text-muted">
145
+
146
+ </span>
147
+ {peerTo ? (
148
+ <Link
149
+ to={peerTo}
150
+ className="mono rounded-control hover:underline"
151
+ style={{ color: tone }}
152
+ title={problem.peer}
153
+ >
154
+ {problem.peer}
155
+ </Link>
156
+ ) : (
157
+ <Ident
158
+ value={problem.peer}
159
+ className={
160
+ problem.severity === "error" ? "text-unresolved" : "text-declared"
161
+ }
162
+ title={`${problem.peer} — ${KIND_NOTE[problem.kind]}. Click to copy.`}
163
+ />
164
+ )}
165
+ <span className="chip ctx" style={ctxStyle(problem.context)}>
166
+ <span aria-hidden className="dot" />
167
+ {problem.context}
168
+ </span>
169
+ <span className="mono ml-auto text-muted">{KIND_NOTE[problem.kind]}</span>
170
+ {problem.note ? (
171
+ <p className="w-full border-l-2 pl-2 border-line-strong text-muted">
172
+ {problem.note}
173
+ </p>
174
+ ) : null}
175
+ {problem.source ? (
176
+ <div className="mono w-full text-muted">
177
+ <Ident value={problem.source} />
178
+ </div>
179
+ ) : null}
180
+ </div>
181
+ );
182
+ }
package/src/data.ts CHANGED
@@ -35,11 +35,26 @@ import manifestJson from "../portolan.json";
35
35
 
36
36
  const manifest = manifestJson as CatalogProfileManifest & { sources: string[] };
37
37
  export const catalogProfiles: CatalogProfile[] = profilesFromManifest(manifest);
38
- const requestedProfile =
39
- typeof window === "undefined"
40
- ? null
41
- : new URLSearchParams(window.location.search).get("catalog");
42
- export const activeCatalogProfile = catalogProfileNamed(manifest, requestedProfile);
38
+ /**
39
+ * The catalog the public landing page demonstrates. The landing has no
40
+ * `?catalog=` of its own - it is a page about the product, not about one
41
+ * estate - and the product is best shown on the example estate rather than on
42
+ * the three-service catalog Portolan keeps about itself. A manifest without
43
+ * an `example` profile falls back to its default, as any unknown name does.
44
+ */
45
+ const LANDING_PROFILE = "example";
46
+
47
+ function requestedProfileFromUrl(): string | null {
48
+ if (typeof window === "undefined") return null;
49
+ const named = new URLSearchParams(window.location.search).get("catalog");
50
+ if (named) return named;
51
+ return /\/landing\/?$/.test(window.location.pathname) ? LANDING_PROFILE : null;
52
+ }
53
+
54
+ export const activeCatalogProfile = catalogProfileNamed(
55
+ manifest,
56
+ requestedProfileFromUrl(),
57
+ );
43
58
 
44
59
  /**
45
60
  * Where sources are looked for. The patterns are written out because
@@ -342,7 +342,7 @@ function Canvas({
342
342
  wrapper={wrapper}
343
343
  name={store.id}
344
344
  />
345
- <div className="relative min-h-0 flex-1">
345
+ <div className="canvas-motion relative min-h-0 flex-1">
346
346
  {layout.ready ? null : <DiagramSkeleton />}
347
347
  <ReactFlow
348
348
  nodes={nodes}
@@ -51,6 +51,12 @@ export interface DependencyGraphProps {
51
51
  * one it replaced.
52
52
  */
53
53
  fitKey: string;
54
+ /**
55
+ * Whether the wheel zooms the canvas. On the graph's own page it does; on a
56
+ * page the graph is embedded in, the wheel belongs to the page and the
57
+ * canvas is zoomed by pinch or by the fit control instead.
58
+ */
59
+ zoomOnScroll?: boolean;
54
60
  }
55
61
 
56
62
  export function DependencyGraph({
@@ -58,6 +64,7 @@ export function DependencyGraph({
58
64
  mode,
59
65
  onMode,
60
66
  fitKey,
67
+ zoomOnScroll = true,
61
68
  }: DependencyGraphProps) {
62
69
  const navigate = useNavigate();
63
70
  const [layout, setLayout] = useState<Layout | null>(null);
@@ -217,7 +224,7 @@ export function DependencyGraph({
217
224
  return (
218
225
  /* The box is the box whether elk has answered or not: the skeleton is
219
226
  laid over it, never in place of it, so nothing reflows on arrival. */
220
- <div className="relative h-full w-full">
227
+ <div className="canvas-motion relative h-full w-full">
221
228
  {ready ? null : <DiagramSkeleton />}
222
229
  <TinyZoom.Provider value={tiny}>
223
230
  <ReactFlow
@@ -238,6 +245,8 @@ export function DependencyGraph({
238
245
  proOptions={{ hideAttribution: true }}
239
246
  fitView
240
247
  fitViewOptions={FIT_OPTIONS}
248
+ zoomOnScroll={zoomOnScroll}
249
+ preventScrolling={zoomOnScroll}
241
250
  minZoom={0.08}
242
251
  maxZoom={2}
243
252
  key={ready ? `fit-${fitKey}-${nodes.length}-${edges.length}` : "pending"}
@@ -161,7 +161,7 @@ export function FocusedEventGraph({
161
161
  return (
162
162
  <div
163
163
  style={{ height: canvasHeight }}
164
- className="relative w-full overflow-hidden rounded-card border border-line shadow-xs"
164
+ className="canvas-motion relative w-full overflow-hidden rounded-card border border-line shadow-xs"
165
165
  >
166
166
  {ready ? null : <DiagramSkeleton />}
167
167
  <ReactFlow
@@ -66,13 +66,11 @@ export function GraphToolbar({
66
66
  type="button"
67
67
  onClick={() => onFocusing(!focusing)}
68
68
  aria-pressed={focusing}
69
- className={focusing ? "is-on" : undefined}
69
+ className={`flex items-center gap-1.5 ${focusing ? "is-on" : ""}`}
70
70
  title="Click a service to dim everything more than one hop away (Esc clears)"
71
71
  >
72
72
  <Crosshair size={13} aria-hidden />
73
- <span className="ml-1.5">
74
- {focused && focusing ? focusedLabel(focused) : "focus"}
75
- </span>
73
+ <span>{focused && focusing ? focusedLabel(focused) : "focus"}</span>
76
74
  </button>
77
75
  </div>
78
76