@shortlink-org/portolan 0.1.0 → 0.2.1

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 +27 -10
  36. package/scripts/local-api.test.mjs +85 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +24 -3
  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 +17 -9
  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,6 +1,6 @@
1
1
  import { useDocumentTitle } from "../app/title";
2
2
  import { createContext, useCallback, useContext, useEffect, useState } from "react";
3
- import { Link } from "react-router";
3
+ import { Link, Navigate, NavLink, Route, Routes } from "react-router";
4
4
  import {
5
5
  ArrowLeft,
6
6
  Box,
@@ -10,24 +10,14 @@ import {
10
10
  FolderGit2,
11
11
  KeyRound,
12
12
  LoaderCircle,
13
- Moon,
14
13
  Play,
15
14
  Plus,
16
- Rows2,
17
- Rows4,
18
15
  ShieldCheck,
19
- Sun,
20
16
  Terminal,
21
17
  Trash2,
22
18
  X,
23
19
  } from "lucide-react";
24
20
  import { catalog, catalogSources } from "../data";
25
- import { useDensity } from "../app/density";
26
- import { useEditorStore } from "../components/EditorLink";
27
- import { Select } from "../components/Select";
28
- import { EDITORS } from "../lib/editor-link";
29
- import { parseEditor } from "../lib/editor-prefs";
30
- import { useTheme } from "../app/theme";
31
21
  import { useToastStore } from "../app/toast";
32
22
  import { absoluteTime, plural, relativeTime } from "../lib/format";
33
23
  import { setupInfo as staticSetupInfo } from "../lib/setup-info";
@@ -60,8 +50,8 @@ import { paths } from "../routes";
60
50
  import { Empty, SectionTitle } from "../components/PageHeader";
61
51
  import { Modal } from "../components/Overlay";
62
52
  import { MachineDocs } from "../components/MachineDocs";
63
- import { ChatSection } from "../chat/ChatSettings";
64
- import { BUILD as CHAT_BUILD } from "../chat/flags";
53
+ import { DeliverySettings } from "./settings/DeliverySettings";
54
+ import { PreferencesSettings } from "./settings/PreferencesSettings";
65
55
 
66
56
  type Health = "healthy" | "changed" | "failed" | "unchecked";
67
57
 
@@ -339,7 +329,7 @@ function ProjectCard({ project }: { project: SetupProject }) {
339
329
  </dl>
340
330
 
341
331
  <div className="mt-4 flex flex-wrap gap-1.5" aria-label="Active plugins">
342
- {pluginNames.map((name) => <a key={name} href={`#plugin-${name}`} className="chip border-line-strong hover:border-accent hover:text-accent">{name}</a>)}
332
+ {pluginNames.map((name) => <Link key={name} to={`${paths.settingsPipeline()}#plugin-${name}`} className="chip border-line-strong hover:border-accent hover:text-accent">{name}</Link>)}
343
333
  </div>
344
334
 
345
335
  <details className="group mt-4 border-t border-line pt-3">
@@ -378,7 +368,9 @@ function Runtime({ plugin, icon = false }: { plugin: SetupPlugin; icon?: boolean
378
368
  const [Icon, tone, name] =
379
369
  plugin.runtime === "wasm"
380
370
  ? ([ShieldCheck, "text-verified", "WASM sandbox"] as const)
381
- : ([Terminal, "text-declared", "host process"] as const);
371
+ : plugin.runtime === "host"
372
+ ? ([Terminal, "text-declared", "in the host"] as const)
373
+ : ([Terminal, "text-declared", "host process"] as const);
382
374
  return icon ? (
383
375
  <span className={tone} title={name}>
384
376
  <Icon size={14} aria-hidden />
@@ -469,7 +461,7 @@ function PluginsList() {
469
461
  <div><div className="label mb-2">last run</div><PipelineSteps steps={runSteps} /></div>
470
462
  <div className="space-y-4">
471
463
  <div><div className="label mb-2">runtime</div><p className="mono"><Runtime plugin={plugin} /></p></div>
472
- <div><div className="label mb-2">used by</div>{plugin.projectIds.length > 0 ? <div className="flex flex-wrap gap-1.5">{plugin.projectIds.map((id) => <a key={id} href={`#project-${id}`} className="chip border-line-strong hover:border-accent hover:text-accent">{projectNames.get(id) ?? id}</a>)}</div> : <p className="mono text-muted">{plugin.stepCount > 0 ? "Estate-wide catalog" : "No pipeline step uses this plugin."}</p>}</div>
464
+ <div><div className="label mb-2">used by</div>{plugin.projectIds.length > 0 ? <div className="flex flex-wrap gap-1.5">{plugin.projectIds.map((id) => <Link key={id} to={`${paths.settingsProjects()}#project-${id}`} className="chip border-line-strong hover:border-accent hover:text-accent">{projectNames.get(id) ?? id}</Link>)}</div> : <p className="mono text-muted">{plugin.stepCount > 0 ? "Estate-wide catalog" : "No pipeline step uses this plugin."}</p>}</div>
473
465
  {outputs.length > 0 ? <div><div className="label mb-2">outputs</div><ul className="mono space-y-1 text-muted">{outputs.map((output) => <li key={output} className="truncate"><FileLink path={output} /></li>)}</ul></div> : null}
474
466
  </div>
475
467
  </div>
@@ -483,54 +475,6 @@ function PluginsList() {
483
475
  );
484
476
  }
485
477
 
486
- /**
487
- * Which editor the "edit ↗" links hand a file to. The links exist only in
488
- * local mode; the choice is offered everywhere, because a reader sets it once
489
- * and the static build they open tomorrow is the same browser.
490
- */
491
- function EditorChoice() {
492
- const editor = useEditorStore((s) => s.editor);
493
- const setEditor = useEditorStore((s) => s.set);
494
- const options = EDITORS.map((e) => ({ value: e.id, label: e.name }));
495
- return (
496
- <div className="rounded-card border border-line p-card shadow-xs">
497
- <div className="label mb-3">open source in</div>
498
- <Select
499
- value={editor}
500
- options={options}
501
- onChange={(value) => setEditor(parseEditor(value))}
502
- label="Editor for source links"
503
- menuWidth={200}
504
- />
505
- <p className="mono mt-2 text-muted">Used by the edit links in local mode.</p>
506
- </div>
507
- );
508
- }
509
-
510
- function Appearance() {
511
- const { theme, toggle: toggleTheme } = useTheme();
512
- const { density, toggle: toggleDensity } = useDensity();
513
- return (
514
- <div className="grid gap-grid sm:grid-cols-2 xl:grid-cols-3">
515
- <div className="rounded-card border border-line p-card shadow-xs">
516
- <div className="label mb-3">theme</div>
517
- <div className="seg inline-flex" role="group" aria-label="Theme">
518
- <button type="button" aria-pressed={theme === "dark"} onClick={() => theme !== "dark" && toggleTheme()} className={`flex items-center gap-1.5 ${theme === "dark" ? "is-on" : ""}`}><Moon size={15} aria-hidden /> dark</button>
519
- <button type="button" aria-pressed={theme === "light"} onClick={() => theme !== "light" && toggleTheme()} className={`flex items-center gap-1.5 ${theme === "light" ? "is-on" : ""}`}><Sun size={15} aria-hidden /> light</button>
520
- </div>
521
- </div>
522
- <div className="rounded-card border border-line p-card shadow-xs">
523
- <div className="label mb-3">row density</div>
524
- <div className="seg inline-flex" role="group" aria-label="Row density">
525
- <button type="button" aria-pressed={density === "comfortable"} onClick={() => density !== "comfortable" && toggleDensity()} className={`flex items-center gap-1.5 ${density === "comfortable" ? "is-on" : ""}`}><Rows4 size={15} aria-hidden /> comfortable</button>
526
- <button type="button" aria-pressed={density === "compact"} onClick={() => density !== "compact" && toggleDensity()} className={`flex items-center gap-1.5 ${density === "compact" ? "is-on" : ""}`}><Rows2 size={15} aria-hidden /> compact</button>
527
- </div>
528
- </div>
529
- <EditorChoice />
530
- </div>
531
- );
532
- }
533
-
534
478
  const FIELD = "mono w-full rounded-control border border-line bg-canvas px-3 py-2 text-ink outline-none focus:border-accent";
535
479
 
536
480
  function Field({ label, value, onChange, placeholder, required = false }: { label: string; value: string; onChange: (value: string) => void; placeholder?: string; required?: boolean }) {
@@ -822,7 +766,7 @@ function Wizard({ open, onClose, onAdded, onRunStarted }: { open: boolean; onClo
822
766
  <div className="space-y-5">
823
767
  <div className={`rounded-control border px-3 py-3 ${trial ? "border-verified bg-surface" : finished?.status === "failed" ? "border-unresolved" : "border-line bg-surface"}`}>
824
768
  <div className="flex items-start gap-3">{trial ? <Check size={18} className="mt-0.5 shrink-0 text-verified" /> : finished?.status === "failed" ? <CircleAlert size={18} className="mt-0.5 shrink-0 text-unresolved" /> : <LoaderCircle size={18} className="mt-0.5 shrink-0 animate-spin text-accent" />}<div><div className="font-medium text-ink">{trial ? "Extraction succeeded — repository unchanged" : finished?.status === "failed" ? "Trial extraction failed" : activeStep ? `Running ${activeStep.plugin}` : "Creating an isolated workspace…"}</div><p className="mt-0.5 text-muted">{trial ? "These results came from real catalog fragments. Apply is now safe to continue." : "Portolan is running the selected extractors without writing to portolan.json or your project."}</p></div></div>
825
- {trial?.previewUrl ? <a className="btn-accent mt-3 inline-flex" href={trial.previewUrl} target="_blank" rel="noreferrer">Open catalog preview ↗</a> : null}
769
+ {trial?.previewUrl ? <a className="product-primary mt-3 inline-flex" href={trial.previewUrl} target="_blank" rel="noreferrer">Open catalog preview ↗</a> : null}
826
770
  {trial?.previewError ? <p className="mt-2 text-declared">The extraction is valid, but the temporary site could not start: {trial.previewError}</p> : null}
827
771
  {!finished ? <><div className="mt-3 h-1.5 overflow-hidden rounded-full bg-canvas"><div className="h-full bg-accent transition-[width]" style={{ width: `${percent}%` }} /></div><div className="mono mt-1 text-right text-muted">{completedSteps.length} / {totalSteps || "?"} steps</div></> : null}
828
772
  </div>
@@ -837,12 +781,12 @@ function Wizard({ open, onClose, onAdded, onRunStarted }: { open: boolean; onClo
837
781
  </div>
838
782
  <div className="flex flex-wrap justify-end gap-2 border-t border-line px-5 py-4">
839
783
  <button type="button" className="tbtn" onClick={onClose} disabled={busy || trialRunning}>Cancel</button>
840
- {stage === "source" ? <button key={repositoryError?.retryable ? "retry-inspection" : source} type="button" className="btn-accent" aria-label={repositoryError?.retryable ? credentialToken.trim() ? "Save token and retry" : "Retry inspection" : source === "external" ? "Inspect repository" : "Detect project"} onClick={() => void (repositoryError?.retryable ? credentialToken.trim() ? authenticateRepository() : retryInspection() : detect())} disabled={busy || (source === "local" ? !path.trim() : !repository.trim())}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : null} {repositoryError?.retryable ? credentialToken.trim() ? "Save token & retry" : "Retry inspection" : source === "external" ? "Inspect repository" : "Detect project"}</button> : null}
841
- {stage === "scope" ? <button key={repositoryError?.retryable ? "retry-component" : "review-components"} type="button" className="btn-accent" aria-label={repositoryError?.retryable ? credentialToken.trim() ? "Save token and retry" : "Retry inspection" : `Review ${selectedComponents.length || "selected"} ${selectedComponents.length === 1 ? "component" : "components"}`} onClick={() => void (repositoryError?.retryable ? credentialToken.trim() ? authenticateRepository() : retryInspection() : reviewSelectedComponents())} disabled={busy || (!repositoryError?.retryable && !selectedComponents.length)}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} {repositoryError?.retryable ? credentialToken.trim() ? "Save token & retry" : "Retry inspection" : <>Review {selectedComponents.length || "selected"} {selectedComponents.length === 1 ? "component" : "components"}</>}</button> : null}
842
- {stage === "configure" ? <button type="button" className="btn-accent" onClick={() => void runTrial()} disabled={busy || !draft?.plugins.length}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Run trial extraction</button> : null}
784
+ {stage === "source" ? <button key={repositoryError?.retryable ? "retry-inspection" : source} type="button" className="product-primary" aria-label={repositoryError?.retryable ? credentialToken.trim() ? "Save token and retry" : "Retry inspection" : source === "external" ? "Inspect repository" : "Detect project"} onClick={() => void (repositoryError?.retryable ? credentialToken.trim() ? authenticateRepository() : retryInspection() : detect())} disabled={busy || (source === "local" ? !path.trim() : !repository.trim())}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : null} {repositoryError?.retryable ? credentialToken.trim() ? "Save token & retry" : "Retry inspection" : source === "external" ? "Inspect repository" : "Detect project"}</button> : null}
785
+ {stage === "scope" ? <button key={repositoryError?.retryable ? "retry-component" : "review-components"} type="button" className="product-primary" aria-label={repositoryError?.retryable ? credentialToken.trim() ? "Save token and retry" : "Retry inspection" : `Review ${selectedComponents.length || "selected"} ${selectedComponents.length === 1 ? "component" : "components"}`} onClick={() => void (repositoryError?.retryable ? credentialToken.trim() ? authenticateRepository() : retryInspection() : reviewSelectedComponents())} disabled={busy || (!repositoryError?.retryable && !selectedComponents.length)}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} {repositoryError?.retryable ? credentialToken.trim() ? "Save token & retry" : "Retry inspection" : <>Review {selectedComponents.length || "selected"} {selectedComponents.length === 1 ? "component" : "components"}</>}</button> : null}
786
+ {stage === "configure" ? <button type="button" className="product-primary" onClick={() => void runTrial()} disabled={busy || !draft?.plugins.length}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Run trial extraction</button> : null}
843
787
  {stage === "trial" && trialRunning ? <button type="button" className="tbtn" onClick={() => trialRunId && void cancelGeneration(trialRunId)}>Cancel trial</button> : null}
844
- {stage === "trial" && finished && !trial ? <button type="button" className="btn-accent" onClick={back}>Back to configuration</button> : null}
845
- {stage === "trial" && trial ? pendingComponents.length > 0 ? <button type="button" className="btn-accent" onClick={() => void applyTrial(false)} disabled={busy}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Add & review next</button> : <><button type="button" className="tbtn" onClick={() => void applyTrial(false)} disabled={busy}>Add without generating</button><button type="button" className="btn-accent" onClick={() => void applyTrial(true)} disabled={busy}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Add & generate</button></> : null}
788
+ {stage === "trial" && finished && !trial ? <button type="button" className="product-primary" onClick={back}>Back to configuration</button> : null}
789
+ {stage === "trial" && trial ? pendingComponents.length > 0 ? <button type="button" className="product-primary" onClick={() => void applyTrial(false)} disabled={busy}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Add & review next</button> : <><button type="button" className="tbtn" onClick={() => void applyTrial(false)} disabled={busy}>Add without generating</button><button type="button" className="product-primary" onClick={() => void applyTrial(true)} disabled={busy}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Add & generate</button></> : null}
846
790
  </div>
847
791
  </Modal>
848
792
  );
@@ -878,55 +822,136 @@ function RunDialog({ runId, open, onClose, onFinished, onApply }: { runId: strin
878
822
  {preview ? <section className="mt-5"><div className="label mb-2">generated diff · {preview.totalFiles} {plural(preview.totalFiles, "file")}</div>{preview.files.length ? <div className="divide-y divide-line overflow-hidden rounded-control border border-line">{preview.files.map((file) => <details key={file.path} className="group"><summary className="flex cursor-pointer items-center gap-2 px-3 py-2 hover:bg-surface"><span className={`chip ${file.status === "removed" ? "status-unresolved" : file.status === "added" ? "status-verified" : "status-declared"}`}>{file.status}</span><span className="mono truncate text-ink">{file.path}</span><ChevronDown size={15} className="ml-auto shrink-0 text-muted transition-transform group-open:rotate-180" /></summary><pre className="mono max-h-80 overflow-auto whitespace-pre p-3 text-muted bg-surface">{file.diff || "Binary file changed"}</pre></details>)}</div> : <div className="rounded-control border border-line bg-surface px-3 py-3 text-muted">Generated documentation is already up to date.</div>}{preview.truncated ? <p className="mt-2 text-muted">Showing the first {preview.files.length} changed files.</p> : null}</section> : null}
879
823
  {logs.length ? <details className="mt-4"><summary className="cursor-pointer text-muted">Generator log · {logs.length} lines</summary><pre className="mono mt-2 max-h-48 overflow-auto whitespace-pre-wrap rounded-control bg-surface p-3 text-muted">{logs.map((event) => event.type === "log" ? event.message : "").join("\n")}</pre></details> : null}
880
824
  </div>
881
- <div className="flex justify-end gap-2 border-t border-line px-5 py-4">{finished ? <><button type="button" className="tbtn" onClick={onClose}>{preview?.files.length ? "Cancel" : "Done"}</button>{preview?.files.length && finished.status === "ok" && runId ? <button type="button" className="btn-accent" onClick={() => onApply(runId)}><Play size={15} /> Apply changes</button> : null}</> : <button type="button" className="tbtn" onClick={() => runId && void cancelGeneration(runId)}>Cancel generation</button>}</div>
825
+ <div className="flex justify-end gap-2 border-t border-line px-5 py-4">{finished ? <><button type="button" className="tbtn" onClick={onClose}>{preview?.files.length ? "Cancel" : "Done"}</button>{preview?.files.length && finished.status === "ok" && runId ? <button type="button" className="product-primary" onClick={() => onApply(runId)}><Play size={15} /> Apply changes</button> : null}</> : <button type="button" className="tbtn" onClick={() => runId && void cancelGeneration(runId)}>Cancel generation</button>}</div>
882
826
  </Modal>
883
827
  );
884
828
  }
885
829
 
886
- function SettingsContent({ local, onAdd, onGenerate }: { local: boolean; onAdd: () => void; onGenerate: () => void }) {
830
+ const SETTINGS_LINKS = [
831
+ ["Overview", paths.settings()],
832
+ ["Projects", paths.settingsProjects()],
833
+ ["Pipeline", paths.settingsPipeline()],
834
+ ["Delivery", paths.settingsDelivery()],
835
+ ["Preferences", paths.settingsPreferences()],
836
+ ] as const;
837
+
838
+ function SettingsNav() {
839
+ return (
840
+ <nav className="mt-5 border-b border-line" aria-label="Settings sections">
841
+ <div className="tab-scroll flex overflow-x-auto">
842
+ {SETTINGS_LINKS.map(([label, to], index) => (
843
+ <NavLink
844
+ key={to}
845
+ to={to}
846
+ end={index === 0}
847
+ className={({ isActive }) =>
848
+ `mono shrink-0 border-b-2 px-3 py-2 transition-colors ${
849
+ isActive
850
+ ? "border-accent text-ink"
851
+ : "border-transparent text-muted hover:text-ink"
852
+ }`
853
+ }
854
+ >
855
+ {label}
856
+ </NavLink>
857
+ ))}
858
+ </div>
859
+ </nav>
860
+ );
861
+ }
862
+
863
+ function OverviewSettings() {
887
864
  const setupInfo = useSetup();
888
865
  const active = setupInfo.plugins.filter((plugin) => plugin.stepCount > 0);
889
866
  return (
890
- <div className="h-full overflow-y-auto p-gutter">
891
- <div className="max-w-table">
892
- <div className="flex flex-wrap items-start justify-between gap-3"><div><div className="flex items-center gap-2"><h1 className="text-lg font-semibold">Settings</h1>{local ? <span className="chip status-verified">local mode</span> : null}</div><p className="mt-1 max-w-prose text-muted">The projects, plugins and local preferences used by this catalog. {local ? "This local session can update portolan.json and preview generator output." : "Build configuration is read-only here and comes from portolan.json."}</p></div>{local ? <button type="button" className="btn-accent" onClick={onGenerate}><Play size={15} /> Preview generated diff</button> : null}</div>
893
- <BuildHealth />
894
-
895
- <div className="mt-4 grid grid-cols-2 gap-grid lg:grid-cols-4">
896
- <Metric value={setupInfo.projects.length} label="project" />
897
- <Metric value={active.length} label="active plugin" />
898
- <Metric value={setupInfo.steps.length} label="pipeline step" />
899
- <Metric value={catalogSources.length} label="catalog source" />
900
- </div>
901
-
902
- <section className="mt-section">
903
- <SectionTitle right={local ? "editable in local mode" : "declared in portolan.json"}>Projects</SectionTitle>
904
- {setupInfo.projects.length === 0 && !local ? <Empty>portolan.json names no projects — every input here is the estate's own</Empty> : <div className="grid gap-grid xl:grid-cols-2">{setupInfo.projects.map((project) => <ProjectCard key={project.id} project={project} />)}{local ? <AddProjectCard onClick={onAdd} /> : null}</div>}
905
- </section>
906
-
907
- <section className="mt-section">
908
- <SectionTitle right={`${active.length} of ${setupInfo.plugins.length} active`}>Plugins</SectionTitle>
909
- <PluginsList />
910
- <p className="mono mt-2 text-muted">WASM runs without filesystem, network or environment access. A host process runs with the permissions of the build.</p>
911
- </section>
867
+ <>
868
+ <BuildHealth />
869
+ <div className="mt-4 grid grid-cols-2 gap-grid lg:grid-cols-4">
870
+ <Metric value={setupInfo.projects.length} label="project" />
871
+ <Metric value={active.length} label="active plugin" />
872
+ <Metric value={setupInfo.steps.length} label="pipeline step" />
873
+ <Metric value={catalogSources.length} label="catalog source" />
874
+ </div>
875
+ <div className="mt-section grid gap-grid sm:grid-cols-2">
876
+ <Link to={paths.settingsProjects()} className="card">
877
+ <div className="font-semibold text-ink">Projects</div>
878
+ <p className="mt-1 text-muted">Sources, scopes and extraction coverage for {setupInfo.projects.length} {plural(setupInfo.projects.length, "project")}.</p>
879
+ </Link>
880
+ <Link to={paths.settingsPipeline()} className="card">
881
+ <div className="font-semibold text-ink">Pipeline</div>
882
+ <p className="mt-1 text-muted">Inspect {active.length} active {plural(active.length, "plugin")} and their generated outputs.</p>
883
+ </Link>
884
+ <Link to={paths.settingsDelivery()} className="card">
885
+ <div className="font-semibold text-ink">Delivery</div>
886
+ <p className="mt-1 text-muted">Install review checks and static catalog publishing for GitHub or GitLab.</p>
887
+ </Link>
888
+ <Link to={paths.settingsPreferences()} className="card">
889
+ <div className="font-semibold text-ink">Preferences</div>
890
+ <p className="mt-1 text-muted">Theme, row density, source editor and Ask the catalog.</p>
891
+ </Link>
892
+ </div>
893
+ </>
894
+ );
895
+ }
912
896
 
913
- <section className="mt-section"><SectionTitle right="stored in this browser">Appearance</SectionTitle><Appearance /></section>
897
+ function ProjectsSettings({ local, onAdd }: { local: boolean; onAdd: () => void }) {
898
+ const setupInfo = useSetup();
899
+ return (
900
+ <section>
901
+ <SectionTitle right={local ? "editable in local mode" : "declared in portolan.json"}>Projects</SectionTitle>
902
+ {setupInfo.projects.length === 0 && !local ? <Empty>portolan.json names no projects — every input here is the estate's own</Empty> : <div className="grid gap-grid xl:grid-cols-2">{setupInfo.projects.map((project) => <ProjectCard key={project.id} project={project} />)}{local ? <AddProjectCard onClick={onAdd} /> : null}</div>}
903
+ </section>
904
+ );
905
+ }
914
906
 
915
- {CHAT_BUILD.built ? <section className="mt-section"><SectionTitle right="stored in this browser">Ask the catalog</SectionTitle><ChatSection /></section> : null}
907
+ function PipelineSettings() {
908
+ const setupInfo = useSetup();
909
+ const active = setupInfo.plugins.filter((plugin) => plugin.stepCount > 0);
910
+ return (
911
+ <div className="space-y-section">
912
+ <section>
913
+ <SectionTitle right={`${active.length} of ${setupInfo.plugins.length} active`}>Plugins</SectionTitle>
914
+ <PluginsList />
915
+ <p className="mono mt-2 text-muted">WASM runs without network or environment access, and a generator without a filesystem. A host process runs with the permissions of the build; a plugin in the host is Portolan's own code doing what needs a socket, such as fetching another repository.</p>
916
+ </section>
917
+ <details className="rounded-card border border-line shadow-xs">
918
+ <summary className="cursor-pointer select-none px-4 py-3 font-semibold text-ink">Advanced build inputs</summary>
919
+ <div className="border-t border-line p-4">
920
+ <div className="label mb-2">catalog source patterns</div>
921
+ {setupInfo.sources.length === 0 ? <Empty>no source patterns declared</Empty> : <ul className="mono space-y-1 text-muted">{setupInfo.sources.map((source) => <li key={source}>{source}</li>)}</ul>}
922
+ <div className="label mt-5 mb-2">pipeline</div>
923
+ {setupInfo.steps.length === 0 ? <Empty>no pipeline steps declared</Empty> : <div className="space-y-1">{setupInfo.steps.map((step, index) => <div key={`${step.phase}:${step.plugin}:${step.input ?? "catalog"}:${index}`} className="mono grid gap-x-3 text-muted sm:grid-cols-[5rem_9rem_1fr]"><span>{step.phase}</span><span className="text-ink">{step.plugin}</span><span className="truncate" title={step.input ?? "merged catalog"}>{step.input ?? "merged catalog"} → {step.output}</span></div>)}</div>}
924
+ <div className="label mt-5 mb-2">generated documentation</div>
925
+ <MachineDocs />
926
+ </div>
927
+ </details>
928
+ </div>
929
+ );
930
+ }
916
931
 
917
- <details className="mt-section rounded-card border border-line shadow-xs">
918
- <summary className="cursor-pointer select-none px-4 py-3 font-semibold text-ink">Advanced build inputs</summary>
919
- <div className="border-t border-line p-4">
920
- <div className="label mb-2">catalog source patterns</div>
921
- {setupInfo.sources.length === 0 ? <Empty>no source patterns declared</Empty> : <ul className="mono space-y-1 text-muted">{setupInfo.sources.map((source) => <li key={source}>{source}</li>)}</ul>}
922
- <div className="label mt-5 mb-2">pipeline</div>
923
- {setupInfo.steps.length === 0 ? <Empty>no pipeline steps declared</Empty> : <div className="space-y-1">{setupInfo.steps.map((step, index) => <div key={`${step.phase}:${step.plugin}:${step.input ?? "catalog"}:${index}`} className="mono grid gap-x-3 text-muted sm:grid-cols-[5rem_9rem_1fr]"><span>{step.phase}</span><span className="text-ink">{step.plugin}</span><span className="truncate" title={step.input ?? "merged catalog"}>{step.input ?? "merged catalog"} → {step.output}</span></div>)}</div>}
924
- <div className="label mt-5 mb-2">generated documentation</div>
925
- <MachineDocs />
932
+ function SettingsContent({ local, onAdd, onGenerate }: { local: boolean; onAdd: () => void; onGenerate: () => void }) {
933
+ return (
934
+ <div className="h-full overflow-y-auto p-gutter">
935
+ <div className="max-w-table">
936
+ <div className="flex flex-wrap items-start justify-between gap-3">
937
+ <div>
938
+ <div className="flex items-center gap-2"><h1 className="text-lg font-semibold">Settings</h1>{local ? <span className="chip status-verified">local mode</span> : null}</div>
939
+ <p className="mt-1 max-w-prose text-muted">Configure projects, extraction, delivery automation and local preferences. {local ? "This local session can write reviewed changes." : "Build configuration is read-only here."}</p>
926
940
  </div>
927
- </details>
928
-
929
- <div className="mono mt-section flex items-center gap-2 pb-section text-muted"><Box size={14} aria-hidden />{local ? "Changes are written to portolan.json; generated files remain reviewable in git." : "Configuration is embedded at build time; changing it requires a new catalog build."}</div>
941
+ {local ? <button type="button" className="product-primary" onClick={onGenerate}><Play size={15} /> Preview generated diff</button> : null}
942
+ </div>
943
+ <SettingsNav />
944
+ <div className="mt-section">
945
+ <Routes>
946
+ <Route index element={<OverviewSettings />} />
947
+ <Route path="projects" element={<ProjectsSettings local={local} onAdd={onAdd} />} />
948
+ <Route path="pipeline" element={<PipelineSettings />} />
949
+ <Route path="delivery" element={<section><SectionTitle right={local ? "preview before writing" : "local mode required"}>Delivery presets</SectionTitle><DeliverySettings local={local} /></section>} />
950
+ <Route path="preferences" element={<PreferencesSettings />} />
951
+ <Route path="*" element={<Navigate to={paths.settings()} replace />} />
952
+ </Routes>
953
+ </div>
954
+ <div className="mono mt-section flex items-center gap-2 pb-section text-muted"><Box size={14} aria-hidden />{local ? "Changes are written only after preview; generated files remain reviewable in git." : "Configuration is embedded at build time; changing it requires a new catalog build."}</div>
930
955
  </div>
931
956
  </div>
932
957
  );
@@ -0,0 +1,229 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import {
3
+ Check,
4
+ ChevronDown,
5
+ CircleAlert,
6
+ FolderGit2,
7
+ LoaderCircle,
8
+ Play,
9
+ ShieldCheck,
10
+ } from "lucide-react";
11
+ import { useToastStore } from "../../app/toast";
12
+ import {
13
+ installDeliveryPreset,
14
+ previewDeliveryPreset,
15
+ } from "../../lib/local-api";
16
+ import type {
17
+ DeliveryPreset,
18
+ DeliveryProvider,
19
+ } from "../../lib/local-api";
20
+
21
+ const STATUS_CLASS: Record<
22
+ DeliveryPreset["files"][number]["status"],
23
+ string
24
+ > = {
25
+ added: "status-verified",
26
+ changed: "status-declared",
27
+ unchanged: "status-verified",
28
+ conflict: "status-unresolved",
29
+ };
30
+
31
+ export function DeliverySettings({ local }: { local: boolean }) {
32
+ const [plan, setPlan] = useState<DeliveryPreset | null>(null);
33
+ const [loading, setLoading] = useState(local);
34
+ const [installing, setInstalling] = useState(false);
35
+ const [error, setError] = useState("");
36
+ const say = useToastStore((state) => state.say);
37
+
38
+ const load = useCallback(async (provider?: DeliveryProvider) => {
39
+ if (!local) return;
40
+ setLoading(true);
41
+ setError("");
42
+ try {
43
+ setPlan(await previewDeliveryPreset(provider));
44
+ } catch (cause) {
45
+ setError(cause instanceof Error ? cause.message : String(cause));
46
+ } finally {
47
+ setLoading(false);
48
+ }
49
+ }, [local]);
50
+
51
+ useEffect(() => {
52
+ void load();
53
+ }, [load]);
54
+
55
+ async function install() {
56
+ if (!plan) return;
57
+ setInstalling(true);
58
+ setError("");
59
+ try {
60
+ const installed = await installDeliveryPreset(
61
+ plan.provider,
62
+ plan.revision,
63
+ );
64
+ setPlan(installed);
65
+ say(
66
+ installed.written.length === 1
67
+ ? `${installed.written[0]} installed`
68
+ : `${installed.written.length} delivery files installed`,
69
+ );
70
+ } catch (cause) {
71
+ const message = cause instanceof Error ? cause.message : String(cause);
72
+ setError(message);
73
+ say(message);
74
+ } finally {
75
+ setInstalling(false);
76
+ }
77
+ }
78
+
79
+ if (!local) {
80
+ return (
81
+ <div className="empty">
82
+ Start <code>portolan dev</code> to preview and install delivery presets
83
+ in this repository.
84
+ </div>
85
+ );
86
+ }
87
+
88
+ const conflicts =
89
+ plan?.files.filter((file) => file.status === "conflict") ?? [];
90
+
91
+ return (
92
+ <div className="rounded-card border border-line p-card shadow-xs">
93
+ <div className="flex flex-wrap items-start justify-between gap-3">
94
+ <div>
95
+ <div className="flex items-center gap-2 font-semibold text-ink">
96
+ <FolderGit2 size={16} className="text-accent" /> Delivery automation
97
+ </div>
98
+ <p className="mt-1 max-w-prose text-muted">
99
+ Install review checks and static catalog publishing directly into
100
+ this repository.
101
+ </p>
102
+ </div>
103
+ <div
104
+ className="seg inline-flex"
105
+ role="group"
106
+ aria-label="Delivery provider"
107
+ >
108
+ {(["github", "gitlab"] as const).map((provider) => (
109
+ <button
110
+ key={provider}
111
+ type="button"
112
+ disabled={loading || installing}
113
+ aria-pressed={plan?.provider === provider}
114
+ onClick={() => void load(provider)}
115
+ className={`capitalize ${plan?.provider === provider ? "is-on" : ""}`}
116
+ >
117
+ {provider === "github" ? "GitHub" : "GitLab"}
118
+ </button>
119
+ ))}
120
+ </div>
121
+ </div>
122
+
123
+ {loading ? (
124
+ <div className="mt-4 flex items-center gap-2 text-muted">
125
+ <LoaderCircle size={15} className="animate-spin" /> Inspecting
126
+ repository…
127
+ </div>
128
+ ) : error && !plan ? (
129
+ <div className="mt-4 rounded-control border border-line bg-surface px-3 py-2 text-unresolved">
130
+ {error}
131
+ </div>
132
+ ) : plan ? (
133
+ <>
134
+ <div className="mt-4 flex flex-wrap items-center gap-2">
135
+ <span
136
+ className={`chip ${
137
+ plan.status === "conflict"
138
+ ? "status-unresolved"
139
+ : plan.status === "update"
140
+ ? "status-declared"
141
+ : "status-verified"
142
+ }`}
143
+ >
144
+ {plan.status}
145
+ </span>
146
+ <span className="mono text-muted">
147
+ {plan.repository} · {plan.defaultBranch}
148
+ </span>
149
+ {plan.detectedProvider === plan.provider ? (
150
+ <span className="text-faint">detected from origin</span>
151
+ ) : plan.detectedProvider ? (
152
+ <span className="text-faint">
153
+ origin uses {plan.detectedProvider === "github" ? "GitHub" : "GitLab"}
154
+ </span>
155
+ ) : (
156
+ <span className="text-faint">provider selected manually</span>
157
+ )}
158
+ </div>
159
+
160
+ <ul className="mt-4 grid gap-2 text-muted sm:grid-cols-2">
161
+ {plan.features.map((feature) => (
162
+ <li key={feature} className="flex items-center gap-2">
163
+ <Check size={14} className="shrink-0 text-verified" /> {feature}
164
+ </li>
165
+ ))}
166
+ </ul>
167
+
168
+ <div className="mt-4 divide-y divide-line overflow-hidden rounded-control border border-line">
169
+ {plan.files.map((file) => (
170
+ <details
171
+ key={file.path}
172
+ className="group"
173
+ open={file.status === "conflict"}
174
+ >
175
+ <summary className="flex cursor-pointer items-center gap-2 px-3 py-2 hover:bg-surface">
176
+ <span className={`chip ${STATUS_CLASS[file.status]}`}>
177
+ {file.status}
178
+ </span>
179
+ <span className="mono truncate text-ink">{file.path}</span>
180
+ <ChevronDown
181
+ size={15}
182
+ className="ml-auto shrink-0 text-muted transition-transform group-open:rotate-180"
183
+ />
184
+ </summary>
185
+ <div className="border-t border-line bg-surface">
186
+ {file.message ? (
187
+ <p className="flex items-center gap-2 px-3 pt-3 text-unresolved">
188
+ <CircleAlert size={14} /> {file.message}
189
+ </p>
190
+ ) : null}
191
+ <pre className="mono max-h-72 overflow-auto whitespace-pre p-3 text-muted">
192
+ {file.diff || "No changes — this file is up to date."}
193
+ </pre>
194
+ </div>
195
+ </details>
196
+ ))}
197
+ </div>
198
+
199
+ {error ? <p className="mt-3 text-unresolved">{error}</p> : null}
200
+ <div className="mt-4 flex flex-wrap items-center justify-between gap-3">
201
+ <p className="text-faint">
202
+ Only files shown above will be written. Existing unmanaged
203
+ workflows are never replaced.
204
+ </p>
205
+ {plan.status === "installed" ? (
206
+ <span className="flex items-center gap-1.5 text-verified">
207
+ <ShieldCheck size={15} /> Preset installed
208
+ </span>
209
+ ) : (
210
+ <button
211
+ type="button"
212
+ className="btn-accent"
213
+ disabled={installing || conflicts.length > 0}
214
+ onClick={() => void install()}
215
+ >
216
+ {installing ? (
217
+ <LoaderCircle size={15} className="animate-spin" />
218
+ ) : (
219
+ <Play size={15} />
220
+ )}
221
+ {plan.status === "update" ? "Update preset" : "Install preset"}
222
+ </button>
223
+ )}
224
+ </div>
225
+ </>
226
+ ) : null}
227
+ </div>
228
+ );
229
+ }