@shortlink-org/portolan 0.1.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 (826) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +387 -0
  3. package/catalog/enum_test.go +46 -0
  4. package/catalog/model.go +932 -0
  5. package/catalog/roundtrip_test.go +185 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/portolan.mjs +368 -0
  8. package/go.mod +14 -0
  9. package/go.sum +14 -0
  10. package/index.html +13 -0
  11. package/internal/commands/cargo.go +154 -0
  12. package/internal/commands/commands.go +143 -0
  13. package/internal/commands/commands_test.go +323 -0
  14. package/internal/commands/gradle.go +77 -0
  15. package/internal/commands/justfile.go +82 -0
  16. package/internal/commands/makefile.go +106 -0
  17. package/internal/commands/maven.go +93 -0
  18. package/internal/commands/packagejson.go +159 -0
  19. package/internal/commands/pyproject.go +169 -0
  20. package/internal/commands/taskfile.go +101 -0
  21. package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
  22. package/internal/commands/testdata/estate/Makefile +33 -0
  23. package/internal/commands/testdata/estate/Taskfile.yml +21 -0
  24. package/internal/commands/testdata/estate/build.gradle.kts +21 -0
  25. package/internal/commands/testdata/estate/justfile +20 -0
  26. package/internal/commands/testdata/estate/package.json +13 -0
  27. package/internal/commands/testdata/estate/pom.xml +29 -0
  28. package/internal/commands/testdata/estate/pyproject.toml +27 -0
  29. package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
  30. package/internal/commands/testdata/golden/commands.json +305 -0
  31. package/internal/gohttp/analyze.go +2500 -0
  32. package/internal/gohttp/endpoints.go +1067 -0
  33. package/internal/gohttp/roots.go +320 -0
  34. package/internal/gohttp/typed.go +143 -0
  35. package/internal/goscan/constants.go +85 -0
  36. package/internal/goscan/goscan_test.go +227 -0
  37. package/internal/goscan/names.go +52 -0
  38. package/internal/goscan/parse_test.go +11 -0
  39. package/internal/goscan/source.go +36 -0
  40. package/internal/goscan/tree.go +155 -0
  41. package/internal/goscan/types.go +99 -0
  42. package/internal/wsdl/ids.go +127 -0
  43. package/internal/wsdl/ids_test.go +21 -0
  44. package/internal/wsdl/model.go +62 -0
  45. package/internal/wsdl/parse.go +920 -0
  46. package/internal/wsdl/parse_test.go +133 -0
  47. package/package.json +117 -0
  48. package/plugin/describe.go +107 -0
  49. package/plugin/describe_test.go +114 -0
  50. package/plugin/protocol.go +117 -0
  51. package/plugin/schematest/schematest.go +126 -0
  52. package/plugins/README.md +1160 -0
  53. package/plugins/extract-adr/describe.go +19 -0
  54. package/plugins/extract-adr/describe_test.go +11 -0
  55. package/plugins/extract-adr/extract.go +153 -0
  56. package/plugins/extract-adr/extract_test.go +350 -0
  57. package/plugins/extract-adr/history.go +99 -0
  58. package/plugins/extract-adr/main.go +65 -0
  59. package/plugins/extract-adr/options.schema.json +26 -0
  60. package/plugins/extract-adr/parse.go +642 -0
  61. package/plugins/extract-adr/parse_test.go +405 -0
  62. package/plugins/extract-asyncapi/describe.go +19 -0
  63. package/plugins/extract-asyncapi/describe_test.go +11 -0
  64. package/plugins/extract-asyncapi/extract.go +315 -0
  65. package/plugins/extract-asyncapi/extract_test.go +197 -0
  66. package/plugins/extract-asyncapi/main.go +48 -0
  67. package/plugins/extract-asyncapi/options.schema.json +26 -0
  68. package/plugins/extract-asyncapi/spec.go +151 -0
  69. package/plugins/extract-celery/README.md +169 -0
  70. package/plugins/extract-celery/extract.py +346 -0
  71. package/plugins/extract-celery/extract_test.py +192 -0
  72. package/plugins/extract-celery/main.py +66 -0
  73. package/plugins/extract-celery/options.py +31 -0
  74. package/plugins/extract-celery/options.schema.json +31 -0
  75. package/plugins/extract-celery/producers.py +156 -0
  76. package/plugins/extract-celery/routes_test.py +106 -0
  77. package/plugins/extract-commands/describe.go +19 -0
  78. package/plugins/extract-commands/describe_test.go +11 -0
  79. package/plugins/extract-commands/extract.go +79 -0
  80. package/plugins/extract-commands/extract_test.go +92 -0
  81. package/plugins/extract-commands/main.go +55 -0
  82. package/plugins/extract-commands/options.schema.json +20 -0
  83. package/plugins/extract-csr/avro.go +258 -0
  84. package/plugins/extract-csr/describe.go +19 -0
  85. package/plugins/extract-csr/describe_test.go +11 -0
  86. package/plugins/extract-csr/extract.go +338 -0
  87. package/plugins/extract-csr/extract_test.go +374 -0
  88. package/plugins/extract-csr/jsonschema.go +343 -0
  89. package/plugins/extract-csr/lock.go +25 -0
  90. package/plugins/extract-csr/main.go +84 -0
  91. package/plugins/extract-csr/options.schema.json +43 -0
  92. package/plugins/extract-csr/subject.go +102 -0
  93. package/plugins/extract-django/README.md +386 -0
  94. package/plugins/extract-django/apps.py +100 -0
  95. package/plugins/extract-django/auth.py +302 -0
  96. package/plugins/extract-django/auth_test.py +188 -0
  97. package/plugins/extract-django/clients.py +121 -0
  98. package/plugins/extract-django/contracts.py +458 -0
  99. package/plugins/extract-django/database.py +119 -0
  100. package/plugins/extract-django/domain.py +318 -0
  101. package/plugins/extract-django/events.py +86 -0
  102. package/plugins/extract-django/extract.py +601 -0
  103. package/plugins/extract-django/extract_test.py +397 -0
  104. package/plugins/extract-django/flows.py +1278 -0
  105. package/plugins/extract-django/flows_test.py +338 -0
  106. package/plugins/extract-django/ids.py +42 -0
  107. package/plugins/extract-django/ids_test.py +30 -0
  108. package/plugins/extract-django/lifecycle.py +222 -0
  109. package/plugins/extract-django/lifecycle_test.py +76 -0
  110. package/plugins/extract-django/main.py +66 -0
  111. package/plugins/extract-django/openapi.py +229 -0
  112. package/plugins/extract-django/openapi_test.py +60 -0
  113. package/plugins/extract-django/operations.py +96 -0
  114. package/plugins/extract-django/options.py +61 -0
  115. package/plugins/extract-django/options.schema.json +126 -0
  116. package/plugins/extract-django/routing.py +214 -0
  117. package/plugins/extract-django/routing_test.py +198 -0
  118. package/plugins/extract-django/serializers.py +558 -0
  119. package/plugins/extract-django/store.py +434 -0
  120. package/plugins/extract-django/transport.py +242 -0
  121. package/plugins/extract-flows/describe.go +19 -0
  122. package/plugins/extract-flows/describe_test.go +11 -0
  123. package/plugins/extract-flows/extract.go +91 -0
  124. package/plugins/extract-flows/main.go +45 -0
  125. package/plugins/extract-flows/options.schema.json +16 -0
  126. package/plugins/extract-flows/parse.go +593 -0
  127. package/plugins/extract-flows/parse_test.go +204 -0
  128. package/plugins/extract-glossary/describe.go +19 -0
  129. package/plugins/extract-glossary/describe_test.go +11 -0
  130. package/plugins/extract-glossary/extract.go +115 -0
  131. package/plugins/extract-glossary/extract_test.go +220 -0
  132. package/plugins/extract-glossary/main.go +59 -0
  133. package/plugins/extract-glossary/options.schema.json +21 -0
  134. package/plugins/extract-glossary/parse.go +214 -0
  135. package/plugins/extract-glossary/parse_test.go +203 -0
  136. package/plugins/extract-go/aggregate.go +214 -0
  137. package/plugins/extract-go/client.go +409 -0
  138. package/plugins/extract-go/client_test.go +266 -0
  139. package/plugins/extract-go/describe.go +19 -0
  140. package/plugins/extract-go/describe_test.go +11 -0
  141. package/plugins/extract-go/enum.go +195 -0
  142. package/plugins/extract-go/enum_test.go +82 -0
  143. package/plugins/extract-go/event.go +99 -0
  144. package/plugins/extract-go/extract.go +191 -0
  145. package/plugins/extract-go/extract_test.go +261 -0
  146. package/plugins/extract-go/flow.go +1441 -0
  147. package/plugins/extract-go/flow_test.go +609 -0
  148. package/plugins/extract-go/httpclient.go +174 -0
  149. package/plugins/extract-go/httpclient_test.go +296 -0
  150. package/plugins/extract-go/ids.go +92 -0
  151. package/plugins/extract-go/layout.go +225 -0
  152. package/plugins/extract-go/layout_test.go +125 -0
  153. package/plugins/extract-go/lifecycle.go +324 -0
  154. package/plugins/extract-go/lifecycle_test.go +108 -0
  155. package/plugins/extract-go/main.go +79 -0
  156. package/plugins/extract-go/operation.go +157 -0
  157. package/plugins/extract-go/options.schema.json +66 -0
  158. package/plugins/extract-go/source.go +316 -0
  159. package/plugins/extract-go/transport.go +321 -0
  160. package/plugins/extract-go/transport_test.go +145 -0
  161. package/plugins/extract-go/wiring.go +434 -0
  162. package/plugins/extract-go-nats/describe.go +19 -0
  163. package/plugins/extract-go-nats/describe_test.go +11 -0
  164. package/plugins/extract-go-nats/extract.go +177 -0
  165. package/plugins/extract-go-nats/extract_test.go +389 -0
  166. package/plugins/extract-go-nats/index.go +379 -0
  167. package/plugins/extract-go-nats/main.go +42 -0
  168. package/plugins/extract-go-nats/options.schema.json +21 -0
  169. package/plugins/extract-go-nats/resolve.go +161 -0
  170. package/plugins/extract-go-nats/sites.go +224 -0
  171. package/plugins/extract-graphql/describe.go +19 -0
  172. package/plugins/extract-graphql/describe_test.go +11 -0
  173. package/plugins/extract-graphql/extract.go +433 -0
  174. package/plugins/extract-graphql/extract_test.go +256 -0
  175. package/plugins/extract-graphql/ids.go +49 -0
  176. package/plugins/extract-graphql/lex.go +237 -0
  177. package/plugins/extract-graphql/main.go +51 -0
  178. package/plugins/extract-graphql/options.schema.json +31 -0
  179. package/plugins/extract-graphql/parse.go +621 -0
  180. package/plugins/extract-graphql/parse_test.go +122 -0
  181. package/plugins/extract-http-clients/describe.go +19 -0
  182. package/plugins/extract-http-clients/describe_test.go +11 -0
  183. package/plugins/extract-http-clients/extract.go +705 -0
  184. package/plugins/extract-http-clients/extract_test.go +1263 -0
  185. package/plugins/extract-http-clients/main.go +42 -0
  186. package/plugins/extract-http-clients/options.schema.json +33 -0
  187. package/plugins/extract-java/README.md +190 -0
  188. package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
  189. package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
  190. package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
  191. package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
  192. package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
  193. package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
  194. package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
  195. package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
  196. package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
  197. package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
  198. package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
  199. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  200. package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
  201. package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
  202. package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
  203. package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
  204. package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
  205. package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
  206. package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
  207. package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
  208. package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
  209. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
  210. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
  211. package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
  212. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  213. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
  214. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
  215. package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
  216. package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
  217. package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
  218. package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
  219. package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
  220. package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
  221. package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
  222. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  223. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  224. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  225. package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
  226. package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
  227. package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
  228. package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
  229. package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
  230. package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
  231. package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
  232. package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
  233. package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
  234. package/plugins/extract-java/options.schema.json +70 -0
  235. package/plugins/extract-openapi/describe.go +19 -0
  236. package/plugins/extract-openapi/describe_test.go +11 -0
  237. package/plugins/extract-openapi/discover.go +243 -0
  238. package/plugins/extract-openapi/extract.go +526 -0
  239. package/plugins/extract-openapi/extract_test.go +545 -0
  240. package/plugins/extract-openapi/main.go +75 -0
  241. package/plugins/extract-openapi/options.schema.json +62 -0
  242. package/plugins/extract-openapi/spec.go +350 -0
  243. package/plugins/extract-project/README.md +22 -0
  244. package/plugins/extract-project/describe.go +19 -0
  245. package/plugins/extract-project/describe_test.go +11 -0
  246. package/plugins/extract-project/extract.go +221 -0
  247. package/plugins/extract-project/extract_test.go +109 -0
  248. package/plugins/extract-project/main.go +41 -0
  249. package/plugins/extract-project/options.schema.json +48 -0
  250. package/plugins/extract-proto/ast.go +125 -0
  251. package/plugins/extract-proto/consumes.go +77 -0
  252. package/plugins/extract-proto/describe.go +19 -0
  253. package/plugins/extract-proto/extract.go +293 -0
  254. package/plugins/extract-proto/extract_test.go +459 -0
  255. package/plugins/extract-proto/ids.go +89 -0
  256. package/plugins/extract-proto/ids_test.go +57 -0
  257. package/plugins/extract-proto/lex.go +285 -0
  258. package/plugins/extract-proto/main.go +100 -0
  259. package/plugins/extract-proto/module.go +120 -0
  260. package/plugins/extract-proto/options.schema.json +51 -0
  261. package/plugins/extract-proto/parse.go +720 -0
  262. package/plugins/extract-proto/parse_test.go +307 -0
  263. package/plugins/extract-proto/provides.go +236 -0
  264. package/plugins/extract-proto/resolve.go +222 -0
  265. package/plugins/extract-proto/resolve_test.go +119 -0
  266. package/plugins/extract-python-kafka/README.md +36 -0
  267. package/plugins/extract-python-kafka/extract.py +211 -0
  268. package/plugins/extract-python-kafka/extract_test.py +68 -0
  269. package/plugins/extract-python-kafka/main.py +59 -0
  270. package/plugins/extract-python-kafka/options.py +29 -0
  271. package/plugins/extract-python-kafka/options.schema.json +31 -0
  272. package/plugins/extract-redis/README.md +11 -0
  273. package/plugins/extract-redis/describe.go +19 -0
  274. package/plugins/extract-redis/describe_test.go +11 -0
  275. package/plugins/extract-redis/extract.go +183 -0
  276. package/plugins/extract-redis/extract_test.go +168 -0
  277. package/plugins/extract-redis/keyspaces.go +469 -0
  278. package/plugins/extract-redis/main.go +44 -0
  279. package/plugins/extract-redis/options.schema.json +30 -0
  280. package/plugins/extract-river/describe.go +19 -0
  281. package/plugins/extract-river/describe_test.go +11 -0
  282. package/plugins/extract-river/extract.go +507 -0
  283. package/plugins/extract-river/extract_test.go +132 -0
  284. package/plugins/extract-river/main.go +42 -0
  285. package/plugins/extract-river/options.schema.json +21 -0
  286. package/plugins/extract-rust/Cargo.lock +133 -0
  287. package/plugins/extract-rust/Cargo.toml +23 -0
  288. package/plugins/extract-rust/README.md +204 -0
  289. package/plugins/extract-rust/options.schema.json +65 -0
  290. package/plugins/extract-rust/rustfmt.toml +1 -0
  291. package/plugins/extract-sql/ddl.go +893 -0
  292. package/plugins/extract-sql/ddl_test.go +401 -0
  293. package/plugins/extract-sql/describe.go +19 -0
  294. package/plugins/extract-sql/describe_test.go +11 -0
  295. package/plugins/extract-sql/layout.go +221 -0
  296. package/plugins/extract-sql/layout_test.go +55 -0
  297. package/plugins/extract-sql/lineage.go +107 -0
  298. package/plugins/extract-sql/main.go +141 -0
  299. package/plugins/extract-sql/maps.go +564 -0
  300. package/plugins/extract-sql/maps_java.go +117 -0
  301. package/plugins/extract-sql/maps_rust.go +333 -0
  302. package/plugins/extract-sql/maps_rust_test.go +70 -0
  303. package/plugins/extract-sql/maps_test.go +204 -0
  304. package/plugins/extract-sql/maps_ts.go +398 -0
  305. package/plugins/extract-sql/maps_ts_test.go +136 -0
  306. package/plugins/extract-sql/options.schema.json +53 -0
  307. package/plugins/extract-sql/projection.go +70 -0
  308. package/plugins/extract-sql/projection_test.go +50 -0
  309. package/plugins/extract-sql/store.go +420 -0
  310. package/plugins/extract-sql/store_test.go +233 -0
  311. package/plugins/extract-sql/view.go +295 -0
  312. package/plugins/extract-ts/README.md +306 -0
  313. package/plugins/extract-ts/ast.test.ts +178 -0
  314. package/plugins/extract-ts/ast.ts +648 -0
  315. package/plugins/extract-ts/clients.ts +185 -0
  316. package/plugins/extract-ts/domain.ts +164 -0
  317. package/plugins/extract-ts/extract.test.ts +208 -0
  318. package/plugins/extract-ts/extract.ts +200 -0
  319. package/plugins/extract-ts/flows.ts +783 -0
  320. package/plugins/extract-ts/graphql.test.ts +73 -0
  321. package/plugins/extract-ts/graphql.ts +181 -0
  322. package/plugins/extract-ts/ids.test.ts +21 -0
  323. package/plugins/extract-ts/ids.ts +52 -0
  324. package/plugins/extract-ts/lifecycle.ts +138 -0
  325. package/plugins/extract-ts/main.ts +48 -0
  326. package/plugins/extract-ts/openapi.test.ts +64 -0
  327. package/plugins/extract-ts/openapi.ts +99 -0
  328. package/plugins/extract-ts/operations.ts +93 -0
  329. package/plugins/extract-ts/options.schema.json +96 -0
  330. package/plugins/extract-ts/source.test.ts +174 -0
  331. package/plugins/extract-ts/source.ts +465 -0
  332. package/plugins/extract-ts/transport.ts +147 -0
  333. package/plugins/extract-ts/tsconfig.json +13 -0
  334. package/plugins/extract-ts/wiring.ts +190 -0
  335. package/plugins/extract-watermill/describe.go +19 -0
  336. package/plugins/extract-watermill/describe_test.go +11 -0
  337. package/plugins/extract-watermill/extract.go +1228 -0
  338. package/plugins/extract-watermill/extract_test.go +234 -0
  339. package/plugins/extract-watermill/main.go +41 -0
  340. package/plugins/extract-watermill/options.schema.json +21 -0
  341. package/plugins/extract-wsdl/describe.go +19 -0
  342. package/plugins/extract-wsdl/describe_test.go +11 -0
  343. package/plugins/extract-wsdl/extract.go +148 -0
  344. package/plugins/extract-wsdl/extract_test.go +45 -0
  345. package/plugins/extract-wsdl/main.go +53 -0
  346. package/plugins/extract-wsdl/options.schema.json +59 -0
  347. package/plugins/fetch-bsr/auth.go +114 -0
  348. package/plugins/fetch-bsr/bsr.go +240 -0
  349. package/plugins/fetch-bsr/cache.go +66 -0
  350. package/plugins/fetch-bsr/describe.go +19 -0
  351. package/plugins/fetch-bsr/fetch.go +192 -0
  352. package/plugins/fetch-bsr/fetch_test.go +465 -0
  353. package/plugins/fetch-bsr/lock.go +71 -0
  354. package/plugins/fetch-bsr/main.go +117 -0
  355. package/plugins/fetch-bsr/options.schema.json +42 -0
  356. package/plugins/fetch-csr/auth.go +95 -0
  357. package/plugins/fetch-csr/cache.go +69 -0
  358. package/plugins/fetch-csr/describe.go +19 -0
  359. package/plugins/fetch-csr/describe_test.go +11 -0
  360. package/plugins/fetch-csr/fetch.go +254 -0
  361. package/plugins/fetch-csr/fetch_test.go +494 -0
  362. package/plugins/fetch-csr/lock.go +95 -0
  363. package/plugins/fetch-csr/main.go +122 -0
  364. package/plugins/fetch-csr/options.schema.json +42 -0
  365. package/plugins/fetch-csr/registry.go +209 -0
  366. package/plugins/fetch-git/cache.go +65 -0
  367. package/plugins/fetch-git/describe.go +19 -0
  368. package/plugins/fetch-git/describe_test.go +11 -0
  369. package/plugins/fetch-git/fetch.go +181 -0
  370. package/plugins/fetch-git/fetch_test.go +332 -0
  371. package/plugins/fetch-git/git.go +169 -0
  372. package/plugins/fetch-git/lock.go +72 -0
  373. package/plugins/fetch-git/main.go +127 -0
  374. package/plugins/fetch-git/offline.go +39 -0
  375. package/plugins/fetch-git/options.schema.json +42 -0
  376. package/plugins/fetch-git/pin.go +90 -0
  377. package/plugins/fetch-git/pin_test.go +74 -0
  378. package/plugins/gen-backstage/describe.go +17 -0
  379. package/plugins/gen-backstage/main.go +22 -0
  380. package/plugins/gen-backstage/options.schema.json +30 -0
  381. package/plugins/gen-backstage/plugin.go +473 -0
  382. package/plugins/gen-backstage/plugin_test.go +145 -0
  383. package/plugins/gen-backstage.wasm +0 -0
  384. package/plugins/gen-markdown/adr.go +162 -0
  385. package/plugins/gen-markdown/aggregate.go +362 -0
  386. package/plugins/gen-markdown/canonical.go +93 -0
  387. package/plugins/gen-markdown/context.go +90 -0
  388. package/plugins/gen-markdown/coverage_test.go +89 -0
  389. package/plugins/gen-markdown/describe.go +19 -0
  390. package/plugins/gen-markdown/describe_test.go +11 -0
  391. package/plugins/gen-markdown/external.go +71 -0
  392. package/plugins/gen-markdown/flow.go +277 -0
  393. package/plugins/gen-markdown/glossary.go +76 -0
  394. package/plugins/gen-markdown/glossary_test.go +148 -0
  395. package/plugins/gen-markdown/llms.go +302 -0
  396. package/plugins/gen-markdown/main.go +22 -0
  397. package/plugins/gen-markdown/markdown.go +254 -0
  398. package/plugins/gen-markdown/markdown_test.go +100 -0
  399. package/plugins/gen-markdown/module.go +107 -0
  400. package/plugins/gen-markdown/options.schema.json +16 -0
  401. package/plugins/gen-markdown/plugin.go +39 -0
  402. package/plugins/gen-markdown/quality_test.go +176 -0
  403. package/plugins/gen-markdown/redis_test.go +38 -0
  404. package/plugins/gen-markdown/render.go +344 -0
  405. package/plugins/gen-markdown/render_test.go +186 -0
  406. package/plugins/gen-markdown/service.go +456 -0
  407. package/plugins/gen-markdown/source.go +177 -0
  408. package/plugins/gen-markdown/store.go +201 -0
  409. package/plugins/gen-markdown.wasm +0 -0
  410. package/plugins/gen-mermaid/describe.go +17 -0
  411. package/plugins/gen-mermaid/main.go +22 -0
  412. package/plugins/gen-mermaid/options.schema.json +10 -0
  413. package/plugins/gen-mermaid/plugin.go +104 -0
  414. package/plugins/gen-mermaid/plugin_test.go +33 -0
  415. package/plugins/gen-mermaid.wasm +0 -0
  416. package/plugins/openapi/ids.go +261 -0
  417. package/plugins/openapi/ids_test.go +98 -0
  418. package/plugins/pyplugin/catalog.py +209 -0
  419. package/plugins/pyplugin/celery_conf.py +436 -0
  420. package/plugins/pyplugin/celery_tasks.py +76 -0
  421. package/plugins/pyplugin/kafka.py +570 -0
  422. package/plugins/pyplugin/names.py +38 -0
  423. package/plugins/pyplugin/protocol.py +79 -0
  424. package/plugins/pyplugin/source.py +285 -0
  425. package/plugins/verify-codeowners/describe.go +19 -0
  426. package/plugins/verify-codeowners/describe_test.go +11 -0
  427. package/plugins/verify-codeowners/main.go +75 -0
  428. package/plugins/verify-codeowners/match.go +85 -0
  429. package/plugins/verify-codeowners/match_test.go +47 -0
  430. package/plugins/verify-codeowners/options.schema.json +15 -0
  431. package/plugins/verify-codeowners/owners.go +164 -0
  432. package/plugins/verify-codeowners/owners_test.go +225 -0
  433. package/plugins/verify-codeowners/parse.go +90 -0
  434. package/plugins/verify-codeowners/parse_test.go +62 -0
  435. package/plugins/verify-otel/describe.go +19 -0
  436. package/plugins/verify-otel/describe_test.go +11 -0
  437. package/plugins/verify-otel/main.go +53 -0
  438. package/plugins/verify-otel/match.go +336 -0
  439. package/plugins/verify-otel/options.schema.json +30 -0
  440. package/plugins/verify-otel/otlp.go +200 -0
  441. package/plugins/verify-otel/verify.go +734 -0
  442. package/plugins/verify-otel/verify_test.go +460 -0
  443. package/portolan.json +1073 -0
  444. package/public/favicon.svg +12 -0
  445. package/schema/portolan.schema.json +2558 -0
  446. package/scripts/README.md +53 -0
  447. package/scripts/build-report.mjs +60 -0
  448. package/scripts/build-report.test.mjs +44 -0
  449. package/scripts/builtin-plugins.mjs +49 -0
  450. package/scripts/catalog-sources.mjs +67 -0
  451. package/scripts/diff.mjs +315 -0
  452. package/scripts/diff.test.mjs +151 -0
  453. package/scripts/forge-comment.mjs +144 -0
  454. package/scripts/forge-comment.test.mjs +151 -0
  455. package/scripts/forge-release.mjs +138 -0
  456. package/scripts/forge-release.test.mjs +106 -0
  457. package/scripts/forge.mjs +61 -0
  458. package/scripts/gen-likec4.mjs +854 -0
  459. package/scripts/gen-likec4.test.mjs +258 -0
  460. package/scripts/gen.mjs +603 -0
  461. package/scripts/generated-docs.test.mjs +66 -0
  462. package/scripts/local-api.mjs +1192 -0
  463. package/scripts/local-api.test.mjs +387 -0
  464. package/scripts/manifest.mjs +214 -0
  465. package/scripts/manifest.test.mjs +148 -0
  466. package/scripts/output-path.mjs +79 -0
  467. package/scripts/output-path.test.mjs +45 -0
  468. package/scripts/package-smoke.mjs +37 -0
  469. package/scripts/plugin-host.mjs +419 -0
  470. package/scripts/plugin-host.test.mjs +109 -0
  471. package/scripts/plugin-wasm-worker.mjs +34 -0
  472. package/scripts/run-builtin.mjs +81 -0
  473. package/scripts/schema.mjs +357 -0
  474. package/scripts/site-docs.mjs +295 -0
  475. package/scripts/site-docs.test.mjs +181 -0
  476. package/scripts/vendor-lock.mjs +58 -0
  477. package/scripts/vendor-lock.test.mjs +69 -0
  478. package/src/README.md +38 -0
  479. package/src/app/App.tsx +448 -0
  480. package/src/app/BranchPicker.test.ts +46 -0
  481. package/src/app/BranchPicker.tsx +153 -0
  482. package/src/app/Breadcrumbs.test.ts +38 -0
  483. package/src/app/Breadcrumbs.tsx +223 -0
  484. package/src/app/BuildStamp.tsx +146 -0
  485. package/src/app/CommandPalette.tsx +292 -0
  486. package/src/app/HashScroll.tsx +45 -0
  487. package/src/app/Sidebar.tsx +1742 -0
  488. package/src/app/TopBar.tsx +393 -0
  489. package/src/app/density.tsx +64 -0
  490. package/src/app/forge-access.test.ts +27 -0
  491. package/src/app/forge-access.tsx +80 -0
  492. package/src/app/panels.tsx +141 -0
  493. package/src/app/pins.tsx +242 -0
  494. package/src/app/query-client.ts +24 -0
  495. package/src/app/responsive.ts +55 -0
  496. package/src/app/shortcuts.test.ts +39 -0
  497. package/src/app/shortcuts.tsx +282 -0
  498. package/src/app/theme.tsx +51 -0
  499. package/src/app/title.test.ts +14 -0
  500. package/src/app/title.ts +30 -0
  501. package/src/app/toast.tsx +59 -0
  502. package/src/app/tree-keys.test.ts +80 -0
  503. package/src/app/tree-keys.ts +87 -0
  504. package/src/app/ui-store.ts +52 -0
  505. package/src/catalog-error.test.ts +105 -0
  506. package/src/catalog-profile.test.ts +59 -0
  507. package/src/catalog-profile.ts +101 -0
  508. package/src/catalog-stores.test.ts +431 -0
  509. package/src/catalog.test.ts +1215 -0
  510. package/src/catalog.ts +3125 -0
  511. package/src/chat/Cards.tsx +408 -0
  512. package/src/chat/ChatPanel.tsx +79 -0
  513. package/src/chat/ChatSettings.tsx +219 -0
  514. package/src/chat/Composer.tsx +99 -0
  515. package/src/chat/Conversation.tsx +281 -0
  516. package/src/chat/Header.tsx +46 -0
  517. package/src/chat/Message.tsx +49 -0
  518. package/src/chat/Notice.tsx +195 -0
  519. package/src/chat/Starter.tsx +67 -0
  520. package/src/chat/Steps.tsx +45 -0
  521. package/src/chat/Waiting.tsx +87 -0
  522. package/src/chat/flags.test.ts +59 -0
  523. package/src/chat/flags.ts +155 -0
  524. package/src/chat/linkify.test.ts +30 -0
  525. package/src/chat/linkify.ts +45 -0
  526. package/src/chat/prefs.ts +56 -0
  527. package/src/chat/prompt.test.ts +43 -0
  528. package/src/chat/prompt.ts +157 -0
  529. package/src/chat/store.ts +35 -0
  530. package/src/chat/tools.ts +58 -0
  531. package/src/chat/transport.ts +70 -0
  532. package/src/components/AdrRow.tsx +29 -0
  533. package/src/components/AnchorLink.tsx +67 -0
  534. package/src/components/ApiReference.tsx +134 -0
  535. package/src/components/AsyncApiReference.tsx +120 -0
  536. package/src/components/CatalogStamp.tsx +155 -0
  537. package/src/components/ChannelRows.tsx +132 -0
  538. package/src/components/CommandRows.tsx +139 -0
  539. package/src/components/DiagramSkeleton.tsx +71 -0
  540. package/src/components/EditorLink.tsx +85 -0
  541. package/src/components/FieldTree.tsx +305 -0
  542. package/src/components/FlowTrigger.tsx +34 -0
  543. package/src/components/Ident.tsx +89 -0
  544. package/src/components/Integrations.tsx +243 -0
  545. package/src/components/LifecycleDiagram.tsx +110 -0
  546. package/src/components/MachineDocs.tsx +42 -0
  547. package/src/components/Markdown.tsx +173 -0
  548. package/src/components/MarkdownTable.tsx +206 -0
  549. package/src/components/Mermaid.tsx +211 -0
  550. package/src/components/MethodRows.tsx +309 -0
  551. package/src/components/Overlay.tsx +125 -0
  552. package/src/components/PageHeader.tsx +189 -0
  553. package/src/components/RowActions.tsx +97 -0
  554. package/src/components/SchemaDocument.tsx +89 -0
  555. package/src/components/Select.tsx +126 -0
  556. package/src/components/ShapeRows.tsx +118 -0
  557. package/src/components/SourceDoc.tsx +82 -0
  558. package/src/components/SourcePreview.tsx +441 -0
  559. package/src/components/TabRow.tsx +182 -0
  560. package/src/components/TechIcon.tsx +34 -0
  561. package/src/components/Toc.tsx +132 -0
  562. package/src/components/WhatLinksHere.tsx +273 -0
  563. package/src/components/WsdlReference.tsx +84 -0
  564. package/src/components/ddd-icons.tsx +107 -0
  565. package/src/components/kind.tsx +157 -0
  566. package/src/components/logo.tsx +61 -0
  567. package/src/components/primitives.tsx +264 -0
  568. package/src/data.test.ts +18 -0
  569. package/src/data.ts +202 -0
  570. package/src/enrich.test.ts +1150 -0
  571. package/src/enrich.ts +672 -0
  572. package/src/er/ErCanvas.tsx +533 -0
  573. package/src/er/RedisSchema.tsx +74 -0
  574. package/src/er/RelationCard.tsx +267 -0
  575. package/src/er/StoreHeader.tsx +155 -0
  576. package/src/er/TableNode.tsx +84 -0
  577. package/src/er/ViewNode.tsx +83 -0
  578. package/src/er/layout.test.ts +115 -0
  579. package/src/er/layout.ts +77 -0
  580. package/src/er/lineage.test.ts +140 -0
  581. package/src/er/lineage.ts +107 -0
  582. package/src/er/markers.tsx +79 -0
  583. package/src/er/spec.test.ts +344 -0
  584. package/src/er/spec.ts +426 -0
  585. package/src/flow/ChainList.tsx +162 -0
  586. package/src/flow/FlowTable.tsx +123 -0
  587. package/src/flow/FlowToolbar.tsx +372 -0
  588. package/src/flow/StepDetail.tsx +380 -0
  589. package/src/flow/StepRail.tsx +428 -0
  590. package/src/flow/answers.test.ts +50 -0
  591. package/src/flow/answers.ts +58 -0
  592. package/src/flow/chain.test.ts +383 -0
  593. package/src/flow/chain.ts +353 -0
  594. package/src/flow/chapters.test.ts +185 -0
  595. package/src/flow/chapters.ts +225 -0
  596. package/src/flow/continues.test.ts +102 -0
  597. package/src/flow/continues.ts +65 -0
  598. package/src/flow/cross-context.test.ts +97 -0
  599. package/src/flow/cross-context.ts +35 -0
  600. package/src/flow/mermaid.test.ts +22 -0
  601. package/src/flow/mermaid.ts +141 -0
  602. package/src/flow/outline.test.ts +212 -0
  603. package/src/flow/outline.ts +170 -0
  604. package/src/flow/paths.test.ts +129 -0
  605. package/src/flow/paths.ts +147 -0
  606. package/src/flow/prefs.test.ts +80 -0
  607. package/src/flow/prefs.ts +104 -0
  608. package/src/graph/DependencyGraph.tsx +283 -0
  609. package/src/graph/DependencyNodes.tsx +162 -0
  610. package/src/graph/EdgeLabel.tsx +61 -0
  611. package/src/graph/FocusedEventGraph.tsx +211 -0
  612. package/src/graph/GraphToolbar.tsx +187 -0
  613. package/src/graph/RoutedEdge.tsx +80 -0
  614. package/src/graph/dependency-layout.test.ts +360 -0
  615. package/src/graph/dependency-layout.ts +444 -0
  616. package/src/graph/elk.test.ts +70 -0
  617. package/src/graph/elk.ts +287 -0
  618. package/src/graph/nodes.tsx +97 -0
  619. package/src/graph/theme.ts +81 -0
  620. package/src/graph/useElkFlow.ts +125 -0
  621. package/src/index.css +1547 -0
  622. package/src/language/InLanguage.tsx +40 -0
  623. package/src/language/TermCard.tsx +132 -0
  624. package/src/language/cards.test.ts +114 -0
  625. package/src/language/cards.ts +111 -0
  626. package/src/lib/admonition.test.ts +49 -0
  627. package/src/lib/admonition.ts +48 -0
  628. package/src/lib/adr.test.ts +131 -0
  629. package/src/lib/adr.ts +68 -0
  630. package/src/lib/api.test.ts +122 -0
  631. package/src/lib/api.ts +135 -0
  632. package/src/lib/backlinks.test.ts +330 -0
  633. package/src/lib/backlinks.ts +567 -0
  634. package/src/lib/branch-compare.ts +16 -0
  635. package/src/lib/build-info.test.ts +76 -0
  636. package/src/lib/build-info.ts +80 -0
  637. package/src/lib/catalog-diff.test.ts +275 -0
  638. package/src/lib/catalog-diff.ts +655 -0
  639. package/src/lib/clipboard.ts +43 -0
  640. package/src/lib/comparison-memory.test.ts +40 -0
  641. package/src/lib/comparison-memory.ts +39 -0
  642. package/src/lib/context-color.ts +27 -0
  643. package/src/lib/context-map.test.ts +148 -0
  644. package/src/lib/context-map.ts +399 -0
  645. package/src/lib/data-model.test.ts +245 -0
  646. package/src/lib/data-model.ts +399 -0
  647. package/src/lib/data-problems.test.ts +178 -0
  648. package/src/lib/data-problems.ts +314 -0
  649. package/src/lib/derive.test.ts +251 -0
  650. package/src/lib/derive.ts +515 -0
  651. package/src/lib/editor-link.test.ts +125 -0
  652. package/src/lib/editor-link.ts +96 -0
  653. package/src/lib/editor-prefs.ts +27 -0
  654. package/src/lib/event-graph.test.ts +237 -0
  655. package/src/lib/event-graph.ts +327 -0
  656. package/src/lib/export-canvas.ts +37 -0
  657. package/src/lib/export-file.test.ts +30 -0
  658. package/src/lib/export-file.ts +20 -0
  659. package/src/lib/flow-tree.test.ts +177 -0
  660. package/src/lib/flow-tree.ts +160 -0
  661. package/src/lib/forge-refs.test.ts +50 -0
  662. package/src/lib/forge-refs.ts +60 -0
  663. package/src/lib/format.test.ts +35 -0
  664. package/src/lib/format.ts +64 -0
  665. package/src/lib/github-catalog.test.ts +301 -0
  666. package/src/lib/github-catalog.ts +610 -0
  667. package/src/lib/integrations.test.ts +119 -0
  668. package/src/lib/integrations.ts +133 -0
  669. package/src/lib/kinds.test.ts +91 -0
  670. package/src/lib/kinds.ts +204 -0
  671. package/src/lib/lifecycle.test.ts +93 -0
  672. package/src/lib/lifecycle.ts +165 -0
  673. package/src/lib/local-api.ts +194 -0
  674. package/src/lib/motion-features.ts +11 -0
  675. package/src/lib/motion.test.ts +120 -0
  676. package/src/lib/motion.tsx +269 -0
  677. package/src/lib/palette-recent.test.ts +80 -0
  678. package/src/lib/palette-recent.ts +90 -0
  679. package/src/lib/palette.test.ts +365 -0
  680. package/src/lib/palette.ts +593 -0
  681. package/src/lib/pins.test.ts +139 -0
  682. package/src/lib/pins.ts +130 -0
  683. package/src/lib/problems.test.ts +112 -0
  684. package/src/lib/project-preview.ts +5 -0
  685. package/src/lib/proto-problems.test.ts +121 -0
  686. package/src/lib/proto-problems.ts +65 -0
  687. package/src/lib/queries.test.ts +113 -0
  688. package/src/lib/queries.ts +131 -0
  689. package/src/lib/registry.test.ts +219 -0
  690. package/src/lib/registry.ts +208 -0
  691. package/src/lib/scenarios.ts +547 -0
  692. package/src/lib/setup-info.test.ts +199 -0
  693. package/src/lib/setup-info.ts +415 -0
  694. package/src/lib/shape.test.ts +289 -0
  695. package/src/lib/shape.ts +454 -0
  696. package/src/lib/sidebar-prefs.test.ts +32 -0
  697. package/src/lib/sidebar-prefs.ts +75 -0
  698. package/src/lib/source-code.test.ts +159 -0
  699. package/src/lib/source-code.ts +286 -0
  700. package/src/lib/source-doc.test.ts +131 -0
  701. package/src/lib/source-doc.ts +103 -0
  702. package/src/lib/source-highlight.test.ts +29 -0
  703. package/src/lib/source-highlight.ts +24 -0
  704. package/src/lib/source-link.test.ts +153 -0
  705. package/src/lib/source-link.ts +201 -0
  706. package/src/lib/spec-files.test.ts +24 -0
  707. package/src/lib/spec-files.ts +23 -0
  708. package/src/lib/table-directive.test.ts +139 -0
  709. package/src/lib/table-directive.ts +106 -0
  710. package/src/lib/tech.test.ts +120 -0
  711. package/src/lib/tech.ts +421 -0
  712. package/src/lib/terms.test.ts +67 -0
  713. package/src/lib/terms.ts +179 -0
  714. package/src/lib/wire-problems.test.ts +263 -0
  715. package/src/lib/wire-problems.ts +281 -0
  716. package/src/likec4/C4View.tsx +55 -0
  717. package/src/likec4/CanvasBridge.tsx +187 -0
  718. package/src/likec4/FlowView.tsx +235 -0
  719. package/src/likec4/InteractiveView.tsx +144 -0
  720. package/src/likec4/canvas-viewport.test.ts +149 -0
  721. package/src/likec4/canvas-viewport.ts +135 -0
  722. package/src/likec4/flow-edges.test.ts +51 -0
  723. package/src/likec4/flow-edges.ts +89 -0
  724. package/src/likec4/frame-theme.test.ts +76 -0
  725. package/src/likec4/frame-theme.ts +101 -0
  726. package/src/likec4/highlight-css.test.ts +49 -0
  727. package/src/likec4/highlight-css.ts +52 -0
  728. package/src/likec4/ids.test.ts +151 -0
  729. package/src/likec4/ids.ts +115 -0
  730. package/src/likec4/levels.tsx +70 -0
  731. package/src/likec4/mapping.test.ts +66 -0
  732. package/src/likec4/mapping.ts +54 -0
  733. package/src/likec4/reserved.json +141 -0
  734. package/src/likec4/view-index.ts +78 -0
  735. package/src/main.tsx +13 -0
  736. package/src/map/ContextMapGraph.tsx +274 -0
  737. package/src/map/ContextNode.tsx +70 -0
  738. package/src/map/FloatingEdge.tsx +86 -0
  739. package/src/merge.test.ts +1038 -0
  740. package/src/merge.ts +1004 -0
  741. package/src/pages/AdrDetail.tsx +242 -0
  742. package/src/pages/AdrIndex.tsx +114 -0
  743. package/src/pages/AggregatePage.tsx +727 -0
  744. package/src/pages/BlockPage.tsx +478 -0
  745. package/src/pages/CatalogFailure.tsx +111 -0
  746. package/src/pages/Changes.tsx +313 -0
  747. package/src/pages/ContextMap.tsx +534 -0
  748. package/src/pages/ContextPage.tsx +346 -0
  749. package/src/pages/EnumPage.tsx +282 -0
  750. package/src/pages/EventPage.tsx +686 -0
  751. package/src/pages/ExternalPage.tsx +204 -0
  752. package/src/pages/FlowDetail.tsx +663 -0
  753. package/src/pages/FlowIndex.tsx +303 -0
  754. package/src/pages/GraphPage.tsx +217 -0
  755. package/src/pages/Language.tsx +257 -0
  756. package/src/pages/ModulePage.tsx +366 -0
  757. package/src/pages/NotFound.tsx +54 -0
  758. package/src/pages/Overview.tsx +344 -0
  759. package/src/pages/Problems.tsx +360 -0
  760. package/src/pages/RegistryIndex.tsx +118 -0
  761. package/src/pages/ServicePage.tsx +746 -0
  762. package/src/pages/Settings.tsx +975 -0
  763. package/src/pages/StorePage.tsx +114 -0
  764. package/src/peek/PeekCard.test.ts +35 -0
  765. package/src/peek/PeekCard.tsx +118 -0
  766. package/src/peek/PeekLayer.tsx +147 -0
  767. package/src/peek/model.test.ts +156 -0
  768. package/src/peek/model.ts +253 -0
  769. package/src/routes.test.ts +222 -0
  770. package/src/routes.ts +449 -0
  771. package/src/selection/DetailPanel.tsx +1407 -0
  772. package/src/selection/hash.test.ts +73 -0
  773. package/src/selection/hash.ts +58 -0
  774. package/src/selection/model.test.ts +199 -0
  775. package/src/selection/model.ts +473 -0
  776. package/src/selection/pages.test.ts +115 -0
  777. package/src/selection/pages.ts +268 -0
  778. package/src/selection/store.test.ts +62 -0
  779. package/src/selection/store.ts +46 -0
  780. package/src/selection/sync.tsx +99 -0
  781. package/src/table/ColumnsMenu.tsx +72 -0
  782. package/src/table/DataTable.tsx +780 -0
  783. package/src/table/Facets.tsx +54 -0
  784. package/src/table/GroupMenu.tsx +69 -0
  785. package/src/table/Toolbar.tsx +190 -0
  786. package/src/table/cells.tsx +117 -0
  787. package/src/table/compare.test.ts +117 -0
  788. package/src/table/compare.ts +196 -0
  789. package/src/table/export.test.ts +88 -0
  790. package/src/table/export.ts +66 -0
  791. package/src/table/facet-groups.test.ts +74 -0
  792. package/src/table/facet-groups.ts +56 -0
  793. package/src/table/grouping.test.ts +84 -0
  794. package/src/table/grouping.ts +82 -0
  795. package/src/table/infer.test.ts +57 -0
  796. package/src/table/infer.ts +50 -0
  797. package/src/table/persist.test.ts +76 -0
  798. package/src/table/persist.ts +112 -0
  799. package/src/table/sort-url.test.ts +75 -0
  800. package/src/table/sort-url.ts +52 -0
  801. package/src/table/types.ts +80 -0
  802. package/src/table/useTableState.ts +220 -0
  803. package/src/test-catalog.ts +7 -0
  804. package/src/testing/estate/data__catalog.json +2847 -0
  805. package/src/testing/estate/data__flows.json +1724 -0
  806. package/src/testing/estate/examples__auth__portolan__api.json +212 -0
  807. package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
  808. package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
  809. package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
  810. package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
  811. package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
  812. package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
  813. package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
  814. package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
  815. package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
  816. package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
  817. package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
  818. package/src/testing/estate.ts +49 -0
  819. package/src/trail/Trail.tsx +81 -0
  820. package/src/trail/model.test.ts +157 -0
  821. package/src/trail/model.ts +189 -0
  822. package/src/trail/record.tsx +32 -0
  823. package/src/trail/store.test.ts +66 -0
  824. package/src/trail/store.ts +102 -0
  825. package/tsconfig.json +35 -0
  826. package/vite.config.ts +171 -0
@@ -0,0 +1,780 @@
1
+ // One table, everywhere.
2
+ //
3
+ // Index pages used to hand-write a <table> each, which meant each one sorted
4
+ // (or did not) in its own way, and a table in a README was a third thing
5
+ // again. This is the one primitive underneath all of them: the page declares
6
+ // what its columns HOLD, and this decides what that means - how they sort,
7
+ // which way they line up, what a cell looks like, what the toolbar offers.
8
+ //
9
+ // It owns the parts a page should not have to re-litigate: the sticky header,
10
+ // the density, the j/k list, the selected row, the widths the reader dragged.
11
+ // TanStack owns the models. Neither owns the markup, which stays here as the
12
+ // same .tbl the app already had.
13
+ //
14
+ // Facet counts and groups come from TanStack's models too, never from a second
15
+ // pass over the data. Grouping is the reader's, one column at a time, chosen
16
+ // from the columns that already offer a chip-set; a group is one full-width
17
+ // row, so there is nothing to aggregate. The grouped column is sorted first -
18
+ // derived here and never written back, so the URL still says only what the
19
+ // reader clicked.
20
+
21
+ import {
22
+ Fragment,
23
+ useCallback,
24
+ useEffect,
25
+ useLayoutEffect,
26
+ useMemo,
27
+ useRef,
28
+ useState,
29
+ } from "react";
30
+ import { Link, useNavigate } from "react-router";
31
+ import { ChevronDown, ChevronRight, ChevronUp } from "lucide-react";
32
+ import { useVirtualizer } from "@tanstack/react-virtual";
33
+ import {
34
+ columnFacetingFeature,
35
+ columnFilteringFeature,
36
+ columnGroupingFeature,
37
+ columnResizingFeature,
38
+ columnSizingFeature,
39
+ columnVisibilityFeature,
40
+ createExpandedRowModel,
41
+ createFacetedUniqueValues,
42
+ createFilteredRowModel,
43
+ createGroupedRowModel,
44
+ createSortedRowModel,
45
+ globalFilteringFeature,
46
+ rowExpandingFeature,
47
+ rowSortingFeature,
48
+ tableFeatures,
49
+ useTable,
50
+ } from "@tanstack/react-table";
51
+ import type { ColumnDef, GroupingState, Row, RowData } from "@tanstack/react-table";
52
+ import type { ReactNode } from "react";
53
+ import { useDensity } from "../app/density";
54
+ import { useToastStore } from "../app/toast";
55
+ import { useUiStore } from "../app/ui-store";
56
+ import { useSelectionStore } from "../selection/store";
57
+ import { toClipboard } from "../lib/clipboard";
58
+ import { comparatorFor } from "./compare";
59
+ import { defaultCell } from "./cells";
60
+ import { Toolbar } from "./Toolbar";
61
+ import type { ExportActions } from "./Toolbar";
62
+ import { csvFilename, toCsv, toExcelCsv, toMarkdown } from "./export";
63
+ import { facetGroupFrom } from "./facet-groups";
64
+ import type { FacetGroup } from "./facet-groups";
65
+ import {
66
+ activeGroup,
67
+ groupOptions,
68
+ groupingSorting,
69
+ leafRows,
70
+ stripeParity,
71
+ } from "./grouping";
72
+ import { applyUpdater, useTableState } from "./useTableState";
73
+ import type { SortEntry } from "./sort-url";
74
+ import type { CellValue, ColumnSpec } from "./types";
75
+ import { canFacet, cellText, defaultAlign, isTextish } from "./types";
76
+
77
+ /**
78
+ * Registered features, and only those: v9 installs nothing it is not asked for.
79
+ *
80
+ * No `facetedRowModel`: without it a column's unique values are counted over
81
+ * every row, so a chip's count is the whole table's and the set never changes
82
+ * shape under the reader. Registering `createFacetedRowModel()` here would
83
+ * make the counts follow the other filters and the search instead, and would
84
+ * finally reach the `count === 0` branch in Facets.tsx.
85
+ *
86
+ * No `rowAggregationFeature`: a group is drawn as one full-width row, so its
87
+ * other cells are never asked for a value.
88
+ */
89
+ const FEATURES = tableFeatures({
90
+ rowSortingFeature,
91
+ sortedRowModel: createSortedRowModel(),
92
+ columnFilteringFeature,
93
+ globalFilteringFeature,
94
+ filteredRowModel: createFilteredRowModel(),
95
+ columnFacetingFeature,
96
+ facetedUniqueValues: createFacetedUniqueValues(),
97
+ columnGroupingFeature,
98
+ groupedRowModel: createGroupedRowModel(),
99
+ rowExpandingFeature,
100
+ expandedRowModel: createExpandedRowModel(),
101
+ columnVisibilityFeature,
102
+ columnSizingFeature,
103
+ columnResizingFeature,
104
+ });
105
+
106
+ type Features = typeof FEATURES;
107
+
108
+ /**
109
+ * Above this many rows the body is virtualized. Never paginated: a page
110
+ * boundary breaks find-in-page, breaks "sort by date and look at the end",
111
+ * and turns one list into an argument about which page a row is on.
112
+ */
113
+ const VIRTUALIZE_AT = 200;
114
+
115
+ /** Row heights, matching the two --row-h settings the density toggle writes. */
116
+ const ROW_H = { comfortable: 40, compact: 30 } as const;
117
+
118
+ /** How far a column may be auto-fitted before it is just a wide column. */
119
+ const AUTOFIT_MAX = 560;
120
+ const MIN_WIDTH = 56;
121
+
122
+ export interface DataTableProps<T extends RowData> {
123
+ /** Stable id. Widths and hidden columns are remembered under it. */
124
+ tableId: string;
125
+ columns: readonly ColumnSpec<T>[];
126
+ rows: readonly T[];
127
+ rowId: (row: T) => string;
128
+ /** The order the page declares. */
129
+ defaultSort?: SortEntry[];
130
+ /** Index pages: put the sort in the address so the view can be sent. */
131
+ sortInUrl?: boolean;
132
+ /** Where a row goes when it is clicked. Omitted where a row goes nowhere. */
133
+ rowLink?: (row: T) => string | null | undefined;
134
+ /** Copy and reveal, per row. */
135
+ rowActions?: (row: T) => ReactNode;
136
+ /** The catalog id this row is, so the global selection can light it up. */
137
+ selectionId?: (row: T) => string | null | undefined;
138
+ /**
139
+ * A second row underneath, spanning every column. Returns null for the rows
140
+ * that have nothing to expand - which is most of them, most of the time.
141
+ * A table with detail rows is never virtualized: the rows are no longer one
142
+ * height, and a virtualizer that guesses wrong scrolls to the wrong place.
143
+ */
144
+ subRow?: (row: T) => ReactNode | null;
145
+ /** Rows at which the toolbar appears. 8 on an index page, 10 in a README. */
146
+ toolbarAt?: number;
147
+ /** README tables: a text filter and nothing else. */
148
+ minimal?: boolean;
149
+ /** No toolbar, no sort, no memory. Small README tables stay quiet. */
150
+ plain?: boolean;
151
+ empty?: ReactNode;
152
+ /** Accessible name, and the name the export menu reports. */
153
+ caption?: string;
154
+ className?: string;
155
+ }
156
+
157
+ /** The columns a menu may hide: everything but the one that names the row. */
158
+ function hideable<T>(spec: ColumnSpec<T>, primaryId: string): boolean {
159
+ return spec.enableHiding ?? spec.id !== primaryId;
160
+ }
161
+
162
+ export function DataTable<T extends RowData>({
163
+ tableId,
164
+ columns: specs,
165
+ rows: data,
166
+ rowId,
167
+ defaultSort = [],
168
+ sortInUrl = false,
169
+ rowLink,
170
+ rowActions,
171
+ selectionId,
172
+ subRow,
173
+ toolbarAt = 8,
174
+ minimal = false,
175
+ plain = false,
176
+ empty,
177
+ caption,
178
+ className = "",
179
+ }: DataTableProps<T>) {
180
+ const navigate = useNavigate();
181
+ const { density } = useDensity();
182
+ const zebra = useUiStore((s) => s.zebra);
183
+ const toggleZebra = useUiStore((s) => s.toggleZebra);
184
+ const selection = useSelectionStore((s) => s.selection);
185
+ const say = useToastStore((s) => s.say);
186
+
187
+ const tableRef = useRef<HTMLTableElement | null>(null);
188
+ const scrollRef = useRef<HTMLDivElement | null>(null);
189
+
190
+ const specById = useMemo(
191
+ () => new Map(specs.map((spec) => [spec.id, spec])),
192
+ [specs],
193
+ );
194
+ const columnIds = useMemo(() => specs.map((spec) => spec.id), [specs]);
195
+ // The column that names the row: it carries the link and the j/k focus, and
196
+ // it is the one column a reader may not switch off.
197
+ const primaryId = (specs.find((spec) => spec.primary) ?? specs[0])?.id ?? "";
198
+
199
+ const state = useTableState({
200
+ tableId,
201
+ columnIds,
202
+ defaultSort,
203
+ sortInUrl: sortInUrl && !plain,
204
+ });
205
+
206
+ // Widths measured from the first layout. Transient on purpose: only a width
207
+ // the reader dragged is worth remembering, and a width measured in a narrow
208
+ // window should not follow them into a wide one.
209
+ const [measured, setMeasured] = useState<Record<string, number>>({});
210
+ const sizing = useMemo(
211
+ () => ({ ...measured, ...state.columnSizing }),
212
+ [measured, state.columnSizing],
213
+ );
214
+ const resized = Object.keys(state.columnSizing).length > 0;
215
+
216
+ const columnDefs = useMemo<ColumnDef<Features, T, unknown>[]>(
217
+ () =>
218
+ specs.map((spec) => {
219
+ const compare = comparatorFor(spec.type);
220
+ return {
221
+ id: spec.id,
222
+ accessorFn: (row: T) => spec.value(row),
223
+ header: spec.header,
224
+ // Ascending only. The table reverses this itself for descending.
225
+ sortFn: (a: Row<Features, T>, b: Row<Features, T>, id: string) =>
226
+ compare(a.getValue<CellValue>(id), b.getValue<CellValue>(id)),
227
+ // The empty cell goes last in both directions, which is a promise
228
+ // no comparator can keep on its own.
229
+ sortUndefined: "last" as const,
230
+ enableSorting: (spec.enableSorting ?? true) && !plain,
231
+ enableHiding: hideable(spec, primaryId),
232
+ // Foldable where there is a chip-set. Static on purpose: the other
233
+ // half of that - two or more values - is read off the table, which
234
+ // does not exist yet here, and a fold with one group is harmless.
235
+ enableGrouping: Boolean(spec.facet) && canFacet(spec.type) && !plain && !minimal,
236
+ // A chip-set narrows to the values it has switched on; an empty set
237
+ // is not a filter.
238
+ filterFn: (row: Row<Features, T>, id: string, value: unknown) => {
239
+ if (!Array.isArray(value) || value.length === 0) return true;
240
+ return (value as string[]).includes(cellText(row.getValue(id)));
241
+ },
242
+ size: spec.size,
243
+ minSize: spec.minSize ?? MIN_WIDTH,
244
+ };
245
+ }),
246
+ [specs, plain, minimal, primaryId],
247
+ );
248
+
249
+ // The columns that may fold the rows, and the one that does. A fold is only
250
+ // offered where the toolbar is: below that, there is nothing to fold.
251
+ const foldable = useMemo(
252
+ () =>
253
+ plain || minimal || data.length < toolbarAt
254
+ ? []
255
+ : specs.filter((spec) => spec.facet && canFacet(spec.type)).map((spec) => spec.id),
256
+ [specs, plain, minimal, data.length, toolbarAt],
257
+ );
258
+ const groupBy = activeGroup(state.groupBy, foldable);
259
+ const grouping = useMemo<GroupingState>(() => (groupBy ? [groupBy] : []), [groupBy]);
260
+ // The grouped column sorted first, derived and never written back: the
261
+ // updater TanStack hands to setSorting is applied to the reader's own sort.
262
+ const sorting = useMemo(() => groupingSorting(state.sorting, groupBy), [state.sorting, groupBy]);
263
+
264
+ const table = useTable<Features, T>({
265
+ features: FEATURES,
266
+ columns: columnDefs,
267
+ data: data as T[],
268
+ getRowId: (row) => rowId(row),
269
+ state: {
270
+ sorting,
271
+ globalFilter: state.globalFilter,
272
+ columnFilters: state.columnFilters,
273
+ columnVisibility: state.columnVisibility,
274
+ columnSizing: sizing,
275
+ grouping,
276
+ expanded: state.expanded,
277
+ },
278
+ onSortingChange: state.setSorting,
279
+ onColumnFiltersChange: state.setColumnFilters,
280
+ onColumnVisibilityChange: state.setColumnVisibility,
281
+ onGroupingChange: (updater) => state.setGroupBy(applyUpdater(updater, grouping)[0] ?? null),
282
+ onExpandedChange: state.setExpanded,
283
+ // Columns stay where they are when one of them folds the rows; the fold
284
+ // is a row above them, not a column moved to the front.
285
+ groupedColumnMode: false,
286
+ enableGrouping: !plain && !minimal,
287
+ // TanStack would open every group again whenever a filter or the data
288
+ // changed; here a fold lasts until the grouping column changes.
289
+ autoResetExpanded: false,
290
+ onColumnSizingChange: (updater) => {
291
+ const next = applyUpdater(updater, sizing);
292
+ // Only what differs from the measured layout is the reader's doing, and
293
+ // only the reader's doing is worth storing.
294
+ state.setColumnSizing((previous) => {
295
+ const out = { ...previous };
296
+ for (const [id, width] of Object.entries(next)) {
297
+ if (measured[id] !== width) out[id] = width;
298
+ }
299
+ return out;
300
+ });
301
+ },
302
+ columnResizeMode: "onChange",
303
+ enableMultiSort: true,
304
+ enableSortingRemoval: true,
305
+ globalFilterFn: (row: Row<Features, T>, id: string, value: unknown) => {
306
+ const needle = String(value).trim().toLowerCase();
307
+ if (needle === "") return true;
308
+ return cellText(row.getValue<CellValue>(id))
309
+ .toLowerCase()
310
+ .includes(needle);
311
+ },
312
+ // The free-text filter searches what is written in words, not the numbers
313
+ // and dates beside them: typing "3" should not match a version column.
314
+ getColumnCanGlobalFilter: (column) => {
315
+ const spec = specById.get(column.id);
316
+ return spec !== undefined && isTextish(spec.type);
317
+ },
318
+ });
319
+
320
+ const visibleColumns = table.getVisibleLeafColumns();
321
+ const visibleIds = visibleColumns.map((column) => column.id).join(",");
322
+ // What is drawn: group rows with the leaves of the open ones beneath them.
323
+ const rows = table.getRowModel().rows;
324
+ // What the filter left: leaves only, folded or not.
325
+ const shown = table.getFilteredRowModel().rows.length;
326
+ const total = data.length;
327
+
328
+ // Measure once per column set, before paint, so the resize handles have a
329
+ // real width to start from and nothing is ever seen mid-settle.
330
+ useLayoutEffect(() => {
331
+ const element = tableRef.current;
332
+ if (!element) return;
333
+ const ids = visibleIds === "" ? [] : visibleIds.split(",");
334
+ const missing = ids.filter((id) => measured[id] === undefined);
335
+ if (missing.length === 0) return;
336
+ const next: Record<string, number> = {};
337
+ for (const id of missing) {
338
+ const th = element.querySelector<HTMLElement>(
339
+ `th[data-col="${CSS.escape(id)}"]`,
340
+ );
341
+ if (th) next[id] = Math.round(th.getBoundingClientRect().width);
342
+ }
343
+ if (Object.keys(next).length > 0) {
344
+ setMeasured((previous) => ({ ...previous, ...next }));
345
+ }
346
+ }, [visibleIds, measured]);
347
+
348
+ /**
349
+ * Double-clicking an edge fits the column to what is in it. The cells are
350
+ * briefly let out of their box, read, and put back inside one frame, which
351
+ * is the only way to ask a clipped element how wide it would rather be.
352
+ */
353
+ const autoFit = useCallback(
354
+ (columnId: string) => {
355
+ const element = tableRef.current;
356
+ if (!element) return;
357
+ element.classList.add("cell-measure");
358
+ let widest = MIN_WIDTH;
359
+ const cells = element.querySelectorAll<HTMLElement>(
360
+ `[data-col="${CSS.escape(columnId)}"] .cell-body`,
361
+ );
362
+ for (const cell of cells) widest = Math.max(widest, cell.scrollWidth);
363
+ element.classList.remove("cell-measure");
364
+ // The padding the cell itself adds, which scrollWidth does not include.
365
+ const width = Math.min(AUTOFIT_MAX, widest + 34);
366
+ state.setColumnSizing((previous) => ({ ...previous, [columnId]: width }));
367
+ },
368
+ [state.setColumnSizing],
369
+ );
370
+
371
+ // --- The toolbar's inputs ------------------------------------------------
372
+
373
+ // TanStack counts a column's values once per data set; `data` is in the
374
+ // dependencies so the toolbar is rebuilt when it does.
375
+ const facetGroups = useMemo<FacetGroup[]>(() => {
376
+ if (plain || minimal) return [];
377
+ return specs.flatMap((spec) => {
378
+ const counts = table.getColumn(spec.id)?.getFacetedUniqueValues();
379
+ const group = counts ? facetGroupFrom(spec, counts, state.facetValues(spec.id)) : null;
380
+ return group ? [group] : [];
381
+ });
382
+ }, [table, specs, data, plain, minimal, state.facetValues]);
383
+
384
+ // Leaves only, in display order, folded or not: an export is the view, and
385
+ // a folded group is still in it.
386
+ const sheet = useCallback(() => {
387
+ const leaves = table.getVisibleLeafColumns();
388
+ return {
389
+ headers: leaves.map((column) => specById.get(column.id)?.header ?? column.id),
390
+ rows: leafRows(table.getSortedRowModel().rows).map((row) =>
391
+ leaves.map((column) => cellText(row.getValue<CellValue>(column.id))),
392
+ ),
393
+ };
394
+ }, [table, rows, specById]);
395
+
396
+ const exports = useMemo<ExportActions>(
397
+ () => ({
398
+ copyMarkdown: () => void toClipboard(toMarkdown(sheet())).then((ok) => say(ok ? "Markdown table copied" : "could not reach the clipboard")),
399
+ copyCsv: () => void toClipboard(toCsv(sheet())).then((ok) => say(ok ? "CSV table copied" : "could not reach the clipboard")),
400
+ downloadCsv: () => {
401
+ const blob = new Blob([toExcelCsv(sheet())], {
402
+ type: "text/csv;charset=utf-8",
403
+ });
404
+ const url = URL.createObjectURL(blob);
405
+ const link = document.createElement("a");
406
+ link.href = url;
407
+ link.download = csvFilename(tableId);
408
+ document.body.append(link);
409
+ link.click();
410
+ link.remove();
411
+ window.setTimeout(() => URL.revokeObjectURL(url), 0);
412
+ say(`CSV downloaded — ${link.download}`);
413
+ },
414
+ }),
415
+ [sheet, tableId, say],
416
+ );
417
+
418
+ const columnToggles = useMemo(
419
+ () =>
420
+ table
421
+ .getAllLeafColumns()
422
+ .filter((column) => column.getCanHide())
423
+ .map((column) => ({
424
+ id: column.id,
425
+ header: specById.get(column.id)?.header ?? column.id,
426
+ visible: column.getIsVisible(),
427
+ })),
428
+ [table, specById, state.columnVisibility],
429
+ );
430
+
431
+ const showToolbar = !plain && total >= toolbarAt;
432
+
433
+ // --- Rows ----------------------------------------------------------------
434
+
435
+ const virtual = rows.length > VIRTUALIZE_AT && !subRow;
436
+ const rowHeight = ROW_H[density];
437
+ const virtualizer = useVirtualizer({
438
+ count: virtual ? rows.length : 0,
439
+ getScrollElement: () => scrollRef.current,
440
+ estimateSize: () => rowHeight,
441
+ getItemKey: (index) => rows[index]?.id ?? index,
442
+ overscan: 12,
443
+ });
444
+ const items = virtual ? virtualizer.getVirtualItems() : [];
445
+ const padTop = items.length > 0 ? (items[0]?.start ?? 0) : 0;
446
+ const padBottom =
447
+ items.length > 0
448
+ ? virtualizer.getTotalSize() - (items[items.length - 1]?.end ?? 0)
449
+ : 0;
450
+
451
+ const selectedRowId = useMemo(() => {
452
+ if (!selectionId || !selection) return null;
453
+ const hit = data.find((row) => selectionId(row) === selection.id);
454
+ return hit ? rowId(hit) : null;
455
+ }, [selectionId, selection, data, rowId]);
456
+
457
+ // A selection made somewhere else - the tree, a diagram, the palette - has
458
+ // to be visible here, which means scrolled to when it is off screen.
459
+ useEffect(() => {
460
+ if (!selectedRowId) return;
461
+ const row = tableRef.current?.querySelector<HTMLElement>(
462
+ `tr[data-row="${CSS.escape(selectedRowId)}"]`,
463
+ );
464
+ row?.scrollIntoView({ block: "nearest" });
465
+ }, [selectedRowId]);
466
+
467
+ const columnCount = visibleColumns.length + (rowActions ? 1 : 0);
468
+
469
+ // Striping reads off the display position rather than nth-child, so a
470
+ // virtualized body's spacer rows cannot flip the parity; a group row has
471
+ // none, and the count starts again beneath it.
472
+ const parity = useMemo(() => stripeParity(rows.map((row) => row.getIsGrouped())), [rows]);
473
+
474
+ /**
475
+ * A group: one row across every column, at the same height as the rows it
476
+ * holds so the virtualizer's arithmetic still stands. Its label is the
477
+ * column's own cell drawn over the first leaf - a facet column's cell
478
+ * renders its value and nothing else, which is what makes the first leaf a
479
+ * fair stand-in for the group. Not a j/k stop and not a row link: it is a
480
+ * heading, and the only thing it does is fold.
481
+ */
482
+ const renderGroupRow = (row: Row<Features, T>) => {
483
+ const spec = row.groupingColumnId ? specById.get(row.groupingColumnId) : undefined;
484
+ const label = spec
485
+ ? spec.cell
486
+ ? spec.cell(row.original)
487
+ : defaultCell(spec.type, row.getValue<CellValue>(spec.id))
488
+ : null;
489
+ const open = row.getIsExpanded();
490
+ return (
491
+ <tr
492
+ key={row.id}
493
+ data-group={row.id}
494
+ className="tbl-group"
495
+ style={virtual ? { height: rowHeight } : undefined}
496
+ >
497
+ <td colSpan={columnCount} className="px-4 align-middle">
498
+ <button
499
+ type="button"
500
+ onClick={row.getToggleExpandedHandler()}
501
+ aria-expanded={open}
502
+ title={open ? "Fold this group" : "Open this group"}
503
+ className="mono flex max-w-full items-center gap-1.5 rounded-control text-muted hover:text-ink"
504
+ >
505
+ {open ? (
506
+ <ChevronDown size={12} aria-hidden className="shrink-0" />
507
+ ) : (
508
+ <ChevronRight size={12} aria-hidden className="shrink-0" />
509
+ )}
510
+ <span className="min-w-0 truncate text-ink">{label}</span>
511
+ <span className="tnum">{row.subRows.length}</span>
512
+ </button>
513
+ </td>
514
+ </tr>
515
+ );
516
+ };
517
+
518
+ const renderRow = (row: Row<Features, T>, index: number) => {
519
+ if (row.getIsGrouped()) return renderGroupRow(row);
520
+ const original = row.original;
521
+ const link = rowLink?.(original) ?? null;
522
+ const active = selectedRowId !== null && row.id === selectedRowId;
523
+ const row_ = (
524
+ <tr
525
+ key={row.id}
526
+ data-row={row.id}
527
+ data-even={parity[index] ?? undefined}
528
+ data-selected={active || undefined}
529
+ style={virtual ? { height: rowHeight } : undefined}
530
+ className={active ? "bg-surface" : undefined}
531
+ onClick={
532
+ link
533
+ ? (event) => {
534
+ // Ids, row actions and links inside cells are their own
535
+ // targets; the row is what is left over.
536
+ if (
537
+ (event.target as HTMLElement).closest("a,button,input,label")
538
+ ) {
539
+ return;
540
+ }
541
+ navigate(link);
542
+ }
543
+ : undefined
544
+ }
545
+ >
546
+ {row.getVisibleCells().map((cell) => {
547
+ const spec = specById.get(cell.column.id);
548
+ if (!spec) return null;
549
+ const value = cell.getValue<CellValue>();
550
+ const align = spec.align ?? defaultAlign(spec.type);
551
+ const body = spec.cell
552
+ ? spec.cell(original)
553
+ : defaultCell(spec.type, value, spec.href?.(original));
554
+ const isPrimary = spec.id === primaryId;
555
+ return (
556
+ <td
557
+ key={cell.id}
558
+ data-col={spec.id}
559
+ title={spec.title?.(original)}
560
+ className={`px-4 align-middle ${align === "right" ? "text-right" : ""} ${
561
+ link && isPrimary ? "cursor-pointer" : ""
562
+ }`}
563
+ style={resized ? { width: sizing[spec.id] } : undefined}
564
+ >
565
+ <span
566
+ className={`cell-body ${spec.type === "text" ? "" : "cell-body-nowrap"} ${
567
+ align === "right" ? "text-right" : ""
568
+ }`}
569
+ >
570
+ {link && isPrimary ? (
571
+ // The row navigates on click; this is the same trip for a
572
+ // keyboard, and the link j/k walks. A real <Link>, so the
573
+ // basename is applied and a middle click still opens a tab.
574
+ <Link
575
+ to={link}
576
+ data-nav-item
577
+ className="rounded-control hover:underline"
578
+ >
579
+ {body}
580
+ </Link>
581
+ ) : (
582
+ body
583
+ )}
584
+ </span>
585
+ </td>
586
+ );
587
+ })}
588
+ {rowActions ? (
589
+ <td className="px-4 align-middle whitespace-nowrap">
590
+ {rowActions(original)}
591
+ </td>
592
+ ) : null}
593
+ </tr>
594
+ );
595
+
596
+ const detail = subRow?.(original) ?? null;
597
+ if (!detail) return row_;
598
+
599
+ return (
600
+ <Fragment key={row.id}>
601
+ {row_}
602
+ <tr className="bg-surface">
603
+ <td colSpan={columnCount} className="px-4 py-2">
604
+ {detail}
605
+ </td>
606
+ </tr>
607
+ </Fragment>
608
+ );
609
+ };
610
+
611
+ return (
612
+ <div className={className}>
613
+ {showToolbar ? (
614
+ <Toolbar
615
+ filter={state.globalFilter}
616
+ onFilter={state.setGlobalFilter}
617
+ facets={facetGroups}
618
+ onToggleFacet={state.toggleFacet}
619
+ shown={shown}
620
+ total={total}
621
+ filtersActive={state.filtersActive}
622
+ onClearFilters={state.clearFilters}
623
+ /* Offered above five columns - counted as the table has, not as the
624
+ menu would list: the column that names the row is not hideable,
625
+ and a six-column table is still a six-column table. */
626
+ columns={!minimal && specs.length > 5 ? columnToggles : undefined}
627
+ onToggleColumn={
628
+ !minimal && specs.length > 5
629
+ ? (id) => table.getColumn(id)?.toggleVisibility()
630
+ : undefined
631
+ }
632
+ groupBy={groupBy}
633
+ groupOptions={minimal ? undefined : groupOptions(facetGroups)}
634
+ onGroupBy={minimal ? undefined : state.setGroupBy}
635
+ /* A README gets a text filter and nothing else; striping is a
636
+ preference about the app, and a README is not where it is set. */
637
+ zebra={zebra}
638
+ onToggleZebra={minimal ? undefined : toggleZebra}
639
+ exports={minimal ? undefined : exports}
640
+ />
641
+ ) : null}
642
+
643
+ {/* `isolate`: the sticky head and column stack by z-index inside the
644
+ table, and those numbers must not reach the page - a corner cell at
645
+ z-30 would otherwise ride over the pinned name row as it passes. */}
646
+ <div
647
+ ref={scrollRef}
648
+ className="isolate overflow-x-auto rounded-card border border-line shadow-xs"
649
+ style={virtual ? { maxHeight: "70vh", overflowY: "auto" } : undefined}
650
+ >
651
+ <table
652
+ ref={tableRef}
653
+ data-nav-list
654
+ className={`tbl tbl-sticky ${zebra && !plain ? "tbl-zebra" : ""}`}
655
+ style={
656
+ resized
657
+ ? { tableLayout: "fixed", width: "max-content", minWidth: "100%" }
658
+ : undefined
659
+ }
660
+ >
661
+ {caption ? <caption className="sr-only">{caption}</caption> : null}
662
+ <thead>
663
+ {table.getHeaderGroups().map((group) => (
664
+ <tr key={group.id} className="text-left">
665
+ {group.headers.map((header) => {
666
+ const spec = specById.get(header.column.id);
667
+ if (!spec) return null;
668
+ const align = spec.align ?? defaultAlign(spec.type);
669
+ // The direction is the table's, so a grouped column shows
670
+ // the way its groups run; the rank is the reader's, so their
671
+ // first key is not badged as the second.
672
+ const sorted = header.column.getIsSorted();
673
+ const canSort = header.column.getCanSort();
674
+ const rank = state.sorting.findIndex((entry) => entry.id === header.column.id);
675
+ return (
676
+ <th
677
+ key={header.id}
678
+ data-col={spec.id}
679
+ scope="col"
680
+ /* No `relative` here: the sticky header is already a
681
+ containing block for the resize handle, and a utility
682
+ `position` would take the header off the top of the
683
+ scroller it is supposed to be pinned to. */
684
+ className="px-4 font-normal"
685
+ style={resized ? { width: sizing[spec.id] } : undefined}
686
+ aria-sort={
687
+ sorted === "asc"
688
+ ? "ascending"
689
+ : sorted === "desc"
690
+ ? "descending"
691
+ : undefined
692
+ }
693
+ >
694
+ {canSort ? (
695
+ <button
696
+ type="button"
697
+ onClick={header.column.getToggleSortingHandler()}
698
+ data-sorted={sorted !== false}
699
+ title={`Sort by ${spec.header}. Shift-click to add a second key.`}
700
+ className={`th-sort ${align === "right" ? "justify-end" : ""}`}
701
+ >
702
+ <span className="truncate">{spec.header}</span>
703
+ {sorted === "desc" ? (
704
+ <ChevronDown
705
+ size={12}
706
+ aria-hidden
707
+ className="sort-mark"
708
+ />
709
+ ) : (
710
+ <ChevronUp
711
+ size={12}
712
+ aria-hidden
713
+ className="sort-mark"
714
+ />
715
+ )}
716
+ {/* Only worth saying when there is more than one key. */}
717
+ {rank > 0 ? (
718
+ <span className="sort-rank">{rank + 1}</span>
719
+ ) : null}
720
+ </button>
721
+ ) : (
722
+ <span className="label">{spec.header}</span>
723
+ )}
724
+ {!plain ? (
725
+ <span
726
+ /* A pointer affordance and nothing else. Given no
727
+ role: the app does not implement keyboard column
728
+ resizing, and a control that announces itself
729
+ without answering the keyboard is worse than one
730
+ that stays quiet. */
731
+ aria-hidden
732
+ title={`Drag to resize ${spec.header}, double-click to fit`}
733
+ className={`col-resize ${
734
+ header.column.getIsResizing() ? "col-resize-on" : ""
735
+ }`}
736
+ onMouseDown={header.getResizeHandler()}
737
+ onTouchStart={header.getResizeHandler()}
738
+ onDoubleClick={() => autoFit(spec.id)}
739
+ />
740
+ ) : null}
741
+ </th>
742
+ );
743
+ })}
744
+ {rowActions ? <th className="px-4" /> : null}
745
+ </tr>
746
+ ))}
747
+ </thead>
748
+ <tbody>
749
+ {rows.length === 0 ? (
750
+ <tr>
751
+ <td colSpan={columnCount} className="px-4 text-muted">
752
+ {empty ?? "nothing here matches this filter"}
753
+ </td>
754
+ </tr>
755
+ ) : virtual ? (
756
+ <>
757
+ {padTop > 0 ? (
758
+ <tr aria-hidden style={{ height: padTop }}>
759
+ <td colSpan={columnCount} />
760
+ </tr>
761
+ ) : null}
762
+ {items.map((item) => {
763
+ const row = rows[item.index];
764
+ return row ? renderRow(row, item.index) : null;
765
+ })}
766
+ {padBottom > 0 ? (
767
+ <tr aria-hidden style={{ height: padBottom }}>
768
+ <td colSpan={columnCount} />
769
+ </tr>
770
+ ) : null}
771
+ </>
772
+ ) : (
773
+ rows.map((row, index) => renderRow(row, index))
774
+ )}
775
+ </tbody>
776
+ </table>
777
+ </div>
778
+ </div>
779
+ );
780
+ }