@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.
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/model.go +932 -0
- package/catalog/roundtrip_test.go +185 -0
- package/catalog/via_test.go +38 -0
- package/cli/portolan.mjs +368 -0
- package/go.mod +14 -0
- package/go.sum +14 -0
- package/index.html +13 -0
- package/internal/commands/cargo.go +154 -0
- package/internal/commands/commands.go +143 -0
- package/internal/commands/commands_test.go +323 -0
- package/internal/commands/gradle.go +77 -0
- package/internal/commands/justfile.go +82 -0
- package/internal/commands/makefile.go +106 -0
- package/internal/commands/maven.go +93 -0
- package/internal/commands/packagejson.go +159 -0
- package/internal/commands/pyproject.go +169 -0
- package/internal/commands/taskfile.go +101 -0
- package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
- package/internal/commands/testdata/estate/Makefile +33 -0
- package/internal/commands/testdata/estate/Taskfile.yml +21 -0
- package/internal/commands/testdata/estate/build.gradle.kts +21 -0
- package/internal/commands/testdata/estate/justfile +20 -0
- package/internal/commands/testdata/estate/package.json +13 -0
- package/internal/commands/testdata/estate/pom.xml +29 -0
- package/internal/commands/testdata/estate/pyproject.toml +27 -0
- package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
- package/internal/commands/testdata/golden/commands.json +305 -0
- package/internal/gohttp/analyze.go +2500 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +143 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +36 -0
- package/internal/goscan/tree.go +155 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +62 -0
- package/internal/wsdl/parse.go +920 -0
- package/internal/wsdl/parse_test.go +133 -0
- package/package.json +117 -0
- package/plugin/describe.go +107 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +117 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +1160 -0
- package/plugins/extract-adr/describe.go +19 -0
- package/plugins/extract-adr/describe_test.go +11 -0
- package/plugins/extract-adr/extract.go +153 -0
- package/plugins/extract-adr/extract_test.go +350 -0
- package/plugins/extract-adr/history.go +99 -0
- package/plugins/extract-adr/main.go +65 -0
- package/plugins/extract-adr/options.schema.json +26 -0
- package/plugins/extract-adr/parse.go +642 -0
- package/plugins/extract-adr/parse_test.go +405 -0
- package/plugins/extract-asyncapi/describe.go +19 -0
- package/plugins/extract-asyncapi/describe_test.go +11 -0
- package/plugins/extract-asyncapi/extract.go +315 -0
- package/plugins/extract-asyncapi/extract_test.go +197 -0
- package/plugins/extract-asyncapi/main.go +48 -0
- package/plugins/extract-asyncapi/options.schema.json +26 -0
- package/plugins/extract-asyncapi/spec.go +151 -0
- package/plugins/extract-celery/README.md +169 -0
- package/plugins/extract-celery/extract.py +346 -0
- package/plugins/extract-celery/extract_test.py +192 -0
- package/plugins/extract-celery/main.py +66 -0
- package/plugins/extract-celery/options.py +31 -0
- package/plugins/extract-celery/options.schema.json +31 -0
- package/plugins/extract-celery/producers.py +156 -0
- package/plugins/extract-celery/routes_test.py +106 -0
- package/plugins/extract-commands/describe.go +19 -0
- package/plugins/extract-commands/describe_test.go +11 -0
- package/plugins/extract-commands/extract.go +79 -0
- package/plugins/extract-commands/extract_test.go +92 -0
- package/plugins/extract-commands/main.go +55 -0
- package/plugins/extract-commands/options.schema.json +20 -0
- package/plugins/extract-csr/avro.go +258 -0
- package/plugins/extract-csr/describe.go +19 -0
- package/plugins/extract-csr/describe_test.go +11 -0
- package/plugins/extract-csr/extract.go +338 -0
- package/plugins/extract-csr/extract_test.go +374 -0
- package/plugins/extract-csr/jsonschema.go +343 -0
- package/plugins/extract-csr/lock.go +25 -0
- package/plugins/extract-csr/main.go +84 -0
- package/plugins/extract-csr/options.schema.json +43 -0
- package/plugins/extract-csr/subject.go +102 -0
- package/plugins/extract-django/README.md +386 -0
- package/plugins/extract-django/apps.py +100 -0
- package/plugins/extract-django/auth.py +302 -0
- package/plugins/extract-django/auth_test.py +188 -0
- package/plugins/extract-django/clients.py +121 -0
- package/plugins/extract-django/contracts.py +458 -0
- package/plugins/extract-django/database.py +119 -0
- package/plugins/extract-django/domain.py +318 -0
- package/plugins/extract-django/events.py +86 -0
- package/plugins/extract-django/extract.py +601 -0
- package/plugins/extract-django/extract_test.py +397 -0
- package/plugins/extract-django/flows.py +1278 -0
- package/plugins/extract-django/flows_test.py +338 -0
- package/plugins/extract-django/ids.py +42 -0
- package/plugins/extract-django/ids_test.py +30 -0
- package/plugins/extract-django/lifecycle.py +222 -0
- package/plugins/extract-django/lifecycle_test.py +76 -0
- package/plugins/extract-django/main.py +66 -0
- package/plugins/extract-django/openapi.py +229 -0
- package/plugins/extract-django/openapi_test.py +60 -0
- package/plugins/extract-django/operations.py +96 -0
- package/plugins/extract-django/options.py +61 -0
- package/plugins/extract-django/options.schema.json +126 -0
- package/plugins/extract-django/routing.py +214 -0
- package/plugins/extract-django/routing_test.py +198 -0
- package/plugins/extract-django/serializers.py +558 -0
- package/plugins/extract-django/store.py +434 -0
- package/plugins/extract-django/transport.py +242 -0
- package/plugins/extract-flows/describe.go +19 -0
- package/plugins/extract-flows/describe_test.go +11 -0
- package/plugins/extract-flows/extract.go +91 -0
- package/plugins/extract-flows/main.go +45 -0
- package/plugins/extract-flows/options.schema.json +16 -0
- package/plugins/extract-flows/parse.go +593 -0
- package/plugins/extract-flows/parse_test.go +204 -0
- package/plugins/extract-glossary/describe.go +19 -0
- package/plugins/extract-glossary/describe_test.go +11 -0
- package/plugins/extract-glossary/extract.go +115 -0
- package/plugins/extract-glossary/extract_test.go +220 -0
- package/plugins/extract-glossary/main.go +59 -0
- package/plugins/extract-glossary/options.schema.json +21 -0
- package/plugins/extract-glossary/parse.go +214 -0
- package/plugins/extract-glossary/parse_test.go +203 -0
- package/plugins/extract-go/aggregate.go +214 -0
- package/plugins/extract-go/client.go +409 -0
- package/plugins/extract-go/client_test.go +266 -0
- package/plugins/extract-go/describe.go +19 -0
- package/plugins/extract-go/describe_test.go +11 -0
- package/plugins/extract-go/enum.go +195 -0
- package/plugins/extract-go/enum_test.go +82 -0
- package/plugins/extract-go/event.go +99 -0
- package/plugins/extract-go/extract.go +191 -0
- package/plugins/extract-go/extract_test.go +261 -0
- package/plugins/extract-go/flow.go +1441 -0
- package/plugins/extract-go/flow_test.go +609 -0
- package/plugins/extract-go/httpclient.go +174 -0
- package/plugins/extract-go/httpclient_test.go +296 -0
- package/plugins/extract-go/ids.go +92 -0
- package/plugins/extract-go/layout.go +225 -0
- package/plugins/extract-go/layout_test.go +125 -0
- package/plugins/extract-go/lifecycle.go +324 -0
- package/plugins/extract-go/lifecycle_test.go +108 -0
- package/plugins/extract-go/main.go +79 -0
- package/plugins/extract-go/operation.go +157 -0
- package/plugins/extract-go/options.schema.json +66 -0
- package/plugins/extract-go/source.go +316 -0
- package/plugins/extract-go/transport.go +321 -0
- package/plugins/extract-go/transport_test.go +145 -0
- package/plugins/extract-go/wiring.go +434 -0
- package/plugins/extract-go-nats/describe.go +19 -0
- package/plugins/extract-go-nats/describe_test.go +11 -0
- package/plugins/extract-go-nats/extract.go +177 -0
- package/plugins/extract-go-nats/extract_test.go +389 -0
- package/plugins/extract-go-nats/index.go +379 -0
- package/plugins/extract-go-nats/main.go +42 -0
- package/plugins/extract-go-nats/options.schema.json +21 -0
- package/plugins/extract-go-nats/resolve.go +161 -0
- package/plugins/extract-go-nats/sites.go +224 -0
- package/plugins/extract-graphql/describe.go +19 -0
- package/plugins/extract-graphql/describe_test.go +11 -0
- package/plugins/extract-graphql/extract.go +433 -0
- package/plugins/extract-graphql/extract_test.go +256 -0
- package/plugins/extract-graphql/ids.go +49 -0
- package/plugins/extract-graphql/lex.go +237 -0
- package/plugins/extract-graphql/main.go +51 -0
- package/plugins/extract-graphql/options.schema.json +31 -0
- package/plugins/extract-graphql/parse.go +621 -0
- package/plugins/extract-graphql/parse_test.go +122 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +705 -0
- package/plugins/extract-http-clients/extract_test.go +1263 -0
- package/plugins/extract-http-clients/main.go +42 -0
- package/plugins/extract-http-clients/options.schema.json +33 -0
- package/plugins/extract-java/README.md +190 -0
- package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
- package/plugins/extract-java/options.schema.json +70 -0
- package/plugins/extract-openapi/describe.go +19 -0
- package/plugins/extract-openapi/describe_test.go +11 -0
- package/plugins/extract-openapi/discover.go +243 -0
- package/plugins/extract-openapi/extract.go +526 -0
- package/plugins/extract-openapi/extract_test.go +545 -0
- package/plugins/extract-openapi/main.go +75 -0
- package/plugins/extract-openapi/options.schema.json +62 -0
- package/plugins/extract-openapi/spec.go +350 -0
- package/plugins/extract-project/README.md +22 -0
- package/plugins/extract-project/describe.go +19 -0
- package/plugins/extract-project/describe_test.go +11 -0
- package/plugins/extract-project/extract.go +221 -0
- package/plugins/extract-project/extract_test.go +109 -0
- package/plugins/extract-project/main.go +41 -0
- package/plugins/extract-project/options.schema.json +48 -0
- package/plugins/extract-proto/ast.go +125 -0
- package/plugins/extract-proto/consumes.go +77 -0
- package/plugins/extract-proto/describe.go +19 -0
- package/plugins/extract-proto/extract.go +293 -0
- package/plugins/extract-proto/extract_test.go +459 -0
- package/plugins/extract-proto/ids.go +89 -0
- package/plugins/extract-proto/ids_test.go +57 -0
- package/plugins/extract-proto/lex.go +285 -0
- package/plugins/extract-proto/main.go +100 -0
- package/plugins/extract-proto/module.go +120 -0
- package/plugins/extract-proto/options.schema.json +51 -0
- package/plugins/extract-proto/parse.go +720 -0
- package/plugins/extract-proto/parse_test.go +307 -0
- package/plugins/extract-proto/provides.go +236 -0
- package/plugins/extract-proto/resolve.go +222 -0
- package/plugins/extract-proto/resolve_test.go +119 -0
- package/plugins/extract-python-kafka/README.md +36 -0
- package/plugins/extract-python-kafka/extract.py +211 -0
- package/plugins/extract-python-kafka/extract_test.py +68 -0
- package/plugins/extract-python-kafka/main.py +59 -0
- package/plugins/extract-python-kafka/options.py +29 -0
- package/plugins/extract-python-kafka/options.schema.json +31 -0
- package/plugins/extract-redis/README.md +11 -0
- package/plugins/extract-redis/describe.go +19 -0
- package/plugins/extract-redis/describe_test.go +11 -0
- package/plugins/extract-redis/extract.go +183 -0
- package/plugins/extract-redis/extract_test.go +168 -0
- package/plugins/extract-redis/keyspaces.go +469 -0
- package/plugins/extract-redis/main.go +44 -0
- package/plugins/extract-redis/options.schema.json +30 -0
- package/plugins/extract-river/describe.go +19 -0
- package/plugins/extract-river/describe_test.go +11 -0
- package/plugins/extract-river/extract.go +507 -0
- package/plugins/extract-river/extract_test.go +132 -0
- package/plugins/extract-river/main.go +42 -0
- package/plugins/extract-river/options.schema.json +21 -0
- package/plugins/extract-rust/Cargo.lock +133 -0
- package/plugins/extract-rust/Cargo.toml +23 -0
- package/plugins/extract-rust/README.md +204 -0
- package/plugins/extract-rust/options.schema.json +65 -0
- package/plugins/extract-rust/rustfmt.toml +1 -0
- package/plugins/extract-sql/ddl.go +893 -0
- package/plugins/extract-sql/ddl_test.go +401 -0
- package/plugins/extract-sql/describe.go +19 -0
- package/plugins/extract-sql/describe_test.go +11 -0
- package/plugins/extract-sql/layout.go +221 -0
- package/plugins/extract-sql/layout_test.go +55 -0
- package/plugins/extract-sql/lineage.go +107 -0
- package/plugins/extract-sql/main.go +141 -0
- package/plugins/extract-sql/maps.go +564 -0
- package/plugins/extract-sql/maps_java.go +117 -0
- package/plugins/extract-sql/maps_rust.go +333 -0
- package/plugins/extract-sql/maps_rust_test.go +70 -0
- package/plugins/extract-sql/maps_test.go +204 -0
- package/plugins/extract-sql/maps_ts.go +398 -0
- package/plugins/extract-sql/maps_ts_test.go +136 -0
- package/plugins/extract-sql/options.schema.json +53 -0
- package/plugins/extract-sql/projection.go +70 -0
- package/plugins/extract-sql/projection_test.go +50 -0
- package/plugins/extract-sql/store.go +420 -0
- package/plugins/extract-sql/store_test.go +233 -0
- package/plugins/extract-sql/view.go +295 -0
- package/plugins/extract-ts/README.md +306 -0
- package/plugins/extract-ts/ast.test.ts +178 -0
- package/plugins/extract-ts/ast.ts +648 -0
- package/plugins/extract-ts/clients.ts +185 -0
- package/plugins/extract-ts/domain.ts +164 -0
- package/plugins/extract-ts/extract.test.ts +208 -0
- package/plugins/extract-ts/extract.ts +200 -0
- package/plugins/extract-ts/flows.ts +783 -0
- package/plugins/extract-ts/graphql.test.ts +73 -0
- package/plugins/extract-ts/graphql.ts +181 -0
- package/plugins/extract-ts/ids.test.ts +21 -0
- package/plugins/extract-ts/ids.ts +52 -0
- package/plugins/extract-ts/lifecycle.ts +138 -0
- package/plugins/extract-ts/main.ts +48 -0
- package/plugins/extract-ts/openapi.test.ts +64 -0
- package/plugins/extract-ts/openapi.ts +99 -0
- package/plugins/extract-ts/operations.ts +93 -0
- package/plugins/extract-ts/options.schema.json +96 -0
- package/plugins/extract-ts/source.test.ts +174 -0
- package/plugins/extract-ts/source.ts +465 -0
- package/plugins/extract-ts/transport.ts +147 -0
- package/plugins/extract-ts/tsconfig.json +13 -0
- package/plugins/extract-ts/wiring.ts +190 -0
- package/plugins/extract-watermill/describe.go +19 -0
- package/plugins/extract-watermill/describe_test.go +11 -0
- package/plugins/extract-watermill/extract.go +1228 -0
- package/plugins/extract-watermill/extract_test.go +234 -0
- package/plugins/extract-watermill/main.go +41 -0
- package/plugins/extract-watermill/options.schema.json +21 -0
- package/plugins/extract-wsdl/describe.go +19 -0
- package/plugins/extract-wsdl/describe_test.go +11 -0
- package/plugins/extract-wsdl/extract.go +148 -0
- package/plugins/extract-wsdl/extract_test.go +45 -0
- package/plugins/extract-wsdl/main.go +53 -0
- package/plugins/extract-wsdl/options.schema.json +59 -0
- package/plugins/fetch-bsr/auth.go +114 -0
- package/plugins/fetch-bsr/bsr.go +240 -0
- package/plugins/fetch-bsr/cache.go +66 -0
- package/plugins/fetch-bsr/describe.go +19 -0
- package/plugins/fetch-bsr/fetch.go +192 -0
- package/plugins/fetch-bsr/fetch_test.go +465 -0
- package/plugins/fetch-bsr/lock.go +71 -0
- package/plugins/fetch-bsr/main.go +117 -0
- package/plugins/fetch-bsr/options.schema.json +42 -0
- package/plugins/fetch-csr/auth.go +95 -0
- package/plugins/fetch-csr/cache.go +69 -0
- package/plugins/fetch-csr/describe.go +19 -0
- package/plugins/fetch-csr/describe_test.go +11 -0
- package/plugins/fetch-csr/fetch.go +254 -0
- package/plugins/fetch-csr/fetch_test.go +494 -0
- package/plugins/fetch-csr/lock.go +95 -0
- package/plugins/fetch-csr/main.go +122 -0
- package/plugins/fetch-csr/options.schema.json +42 -0
- package/plugins/fetch-csr/registry.go +209 -0
- package/plugins/fetch-git/cache.go +65 -0
- package/plugins/fetch-git/describe.go +19 -0
- package/plugins/fetch-git/describe_test.go +11 -0
- package/plugins/fetch-git/fetch.go +181 -0
- package/plugins/fetch-git/fetch_test.go +332 -0
- package/plugins/fetch-git/git.go +169 -0
- package/plugins/fetch-git/lock.go +72 -0
- package/plugins/fetch-git/main.go +127 -0
- package/plugins/fetch-git/offline.go +39 -0
- package/plugins/fetch-git/options.schema.json +42 -0
- package/plugins/fetch-git/pin.go +90 -0
- package/plugins/fetch-git/pin_test.go +74 -0
- package/plugins/gen-backstage/describe.go +17 -0
- package/plugins/gen-backstage/main.go +22 -0
- package/plugins/gen-backstage/options.schema.json +30 -0
- package/plugins/gen-backstage/plugin.go +473 -0
- package/plugins/gen-backstage/plugin_test.go +145 -0
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +162 -0
- package/plugins/gen-markdown/aggregate.go +362 -0
- package/plugins/gen-markdown/canonical.go +93 -0
- package/plugins/gen-markdown/context.go +90 -0
- package/plugins/gen-markdown/coverage_test.go +89 -0
- package/plugins/gen-markdown/describe.go +19 -0
- package/plugins/gen-markdown/describe_test.go +11 -0
- package/plugins/gen-markdown/external.go +71 -0
- package/plugins/gen-markdown/flow.go +277 -0
- package/plugins/gen-markdown/glossary.go +76 -0
- package/plugins/gen-markdown/glossary_test.go +148 -0
- package/plugins/gen-markdown/llms.go +302 -0
- package/plugins/gen-markdown/main.go +22 -0
- package/plugins/gen-markdown/markdown.go +254 -0
- package/plugins/gen-markdown/markdown_test.go +100 -0
- package/plugins/gen-markdown/module.go +107 -0
- package/plugins/gen-markdown/options.schema.json +16 -0
- package/plugins/gen-markdown/plugin.go +39 -0
- package/plugins/gen-markdown/quality_test.go +176 -0
- package/plugins/gen-markdown/redis_test.go +38 -0
- package/plugins/gen-markdown/render.go +344 -0
- package/plugins/gen-markdown/render_test.go +186 -0
- package/plugins/gen-markdown/service.go +456 -0
- package/plugins/gen-markdown/source.go +177 -0
- package/plugins/gen-markdown/store.go +201 -0
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +17 -0
- package/plugins/gen-mermaid/main.go +22 -0
- package/plugins/gen-mermaid/options.schema.json +10 -0
- package/plugins/gen-mermaid/plugin.go +104 -0
- package/plugins/gen-mermaid/plugin_test.go +33 -0
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/pyplugin/catalog.py +209 -0
- package/plugins/pyplugin/celery_conf.py +436 -0
- package/plugins/pyplugin/celery_tasks.py +76 -0
- package/plugins/pyplugin/kafka.py +570 -0
- package/plugins/pyplugin/names.py +38 -0
- package/plugins/pyplugin/protocol.py +79 -0
- package/plugins/pyplugin/source.py +285 -0
- package/plugins/verify-codeowners/describe.go +19 -0
- package/plugins/verify-codeowners/describe_test.go +11 -0
- package/plugins/verify-codeowners/main.go +75 -0
- package/plugins/verify-codeowners/match.go +85 -0
- package/plugins/verify-codeowners/match_test.go +47 -0
- package/plugins/verify-codeowners/options.schema.json +15 -0
- package/plugins/verify-codeowners/owners.go +164 -0
- package/plugins/verify-codeowners/owners_test.go +225 -0
- package/plugins/verify-codeowners/parse.go +90 -0
- package/plugins/verify-codeowners/parse_test.go +62 -0
- package/plugins/verify-otel/describe.go +19 -0
- package/plugins/verify-otel/describe_test.go +11 -0
- package/plugins/verify-otel/main.go +53 -0
- package/plugins/verify-otel/match.go +336 -0
- package/plugins/verify-otel/options.schema.json +30 -0
- package/plugins/verify-otel/otlp.go +200 -0
- package/plugins/verify-otel/verify.go +734 -0
- package/plugins/verify-otel/verify_test.go +460 -0
- package/portolan.json +1073 -0
- package/public/favicon.svg +12 -0
- package/schema/portolan.schema.json +2558 -0
- package/scripts/README.md +53 -0
- package/scripts/build-report.mjs +60 -0
- package/scripts/build-report.test.mjs +44 -0
- package/scripts/builtin-plugins.mjs +49 -0
- package/scripts/catalog-sources.mjs +67 -0
- package/scripts/diff.mjs +315 -0
- package/scripts/diff.test.mjs +151 -0
- package/scripts/forge-comment.mjs +144 -0
- package/scripts/forge-comment.test.mjs +151 -0
- package/scripts/forge-release.mjs +138 -0
- package/scripts/forge-release.test.mjs +106 -0
- package/scripts/forge.mjs +61 -0
- package/scripts/gen-likec4.mjs +854 -0
- package/scripts/gen-likec4.test.mjs +258 -0
- package/scripts/gen.mjs +603 -0
- package/scripts/generated-docs.test.mjs +66 -0
- package/scripts/local-api.mjs +1192 -0
- package/scripts/local-api.test.mjs +387 -0
- package/scripts/manifest.mjs +214 -0
- package/scripts/manifest.test.mjs +148 -0
- package/scripts/output-path.mjs +79 -0
- package/scripts/output-path.test.mjs +45 -0
- package/scripts/package-smoke.mjs +37 -0
- package/scripts/plugin-host.mjs +419 -0
- package/scripts/plugin-host.test.mjs +109 -0
- package/scripts/plugin-wasm-worker.mjs +34 -0
- package/scripts/run-builtin.mjs +81 -0
- package/scripts/schema.mjs +357 -0
- package/scripts/site-docs.mjs +295 -0
- package/scripts/site-docs.test.mjs +181 -0
- package/scripts/vendor-lock.mjs +58 -0
- package/scripts/vendor-lock.test.mjs +69 -0
- package/src/README.md +38 -0
- package/src/app/App.tsx +448 -0
- package/src/app/BranchPicker.test.ts +46 -0
- package/src/app/BranchPicker.tsx +153 -0
- package/src/app/Breadcrumbs.test.ts +38 -0
- package/src/app/Breadcrumbs.tsx +223 -0
- package/src/app/BuildStamp.tsx +146 -0
- package/src/app/CommandPalette.tsx +292 -0
- package/src/app/HashScroll.tsx +45 -0
- package/src/app/Sidebar.tsx +1742 -0
- package/src/app/TopBar.tsx +393 -0
- package/src/app/density.tsx +64 -0
- package/src/app/forge-access.test.ts +27 -0
- package/src/app/forge-access.tsx +80 -0
- package/src/app/panels.tsx +141 -0
- package/src/app/pins.tsx +242 -0
- package/src/app/query-client.ts +24 -0
- package/src/app/responsive.ts +55 -0
- package/src/app/shortcuts.test.ts +39 -0
- package/src/app/shortcuts.tsx +282 -0
- package/src/app/theme.tsx +51 -0
- package/src/app/title.test.ts +14 -0
- package/src/app/title.ts +30 -0
- package/src/app/toast.tsx +59 -0
- package/src/app/tree-keys.test.ts +80 -0
- package/src/app/tree-keys.ts +87 -0
- package/src/app/ui-store.ts +52 -0
- package/src/catalog-error.test.ts +105 -0
- package/src/catalog-profile.test.ts +59 -0
- package/src/catalog-profile.ts +101 -0
- package/src/catalog-stores.test.ts +431 -0
- package/src/catalog.test.ts +1215 -0
- package/src/catalog.ts +3125 -0
- package/src/chat/Cards.tsx +408 -0
- package/src/chat/ChatPanel.tsx +79 -0
- package/src/chat/ChatSettings.tsx +219 -0
- package/src/chat/Composer.tsx +99 -0
- package/src/chat/Conversation.tsx +281 -0
- package/src/chat/Header.tsx +46 -0
- package/src/chat/Message.tsx +49 -0
- package/src/chat/Notice.tsx +195 -0
- package/src/chat/Starter.tsx +67 -0
- package/src/chat/Steps.tsx +45 -0
- package/src/chat/Waiting.tsx +87 -0
- package/src/chat/flags.test.ts +59 -0
- package/src/chat/flags.ts +155 -0
- package/src/chat/linkify.test.ts +30 -0
- package/src/chat/linkify.ts +45 -0
- package/src/chat/prefs.ts +56 -0
- package/src/chat/prompt.test.ts +43 -0
- package/src/chat/prompt.ts +157 -0
- package/src/chat/store.ts +35 -0
- package/src/chat/tools.ts +58 -0
- package/src/chat/transport.ts +70 -0
- package/src/components/AdrRow.tsx +29 -0
- package/src/components/AnchorLink.tsx +67 -0
- package/src/components/ApiReference.tsx +134 -0
- package/src/components/AsyncApiReference.tsx +120 -0
- package/src/components/CatalogStamp.tsx +155 -0
- package/src/components/ChannelRows.tsx +132 -0
- package/src/components/CommandRows.tsx +139 -0
- package/src/components/DiagramSkeleton.tsx +71 -0
- package/src/components/EditorLink.tsx +85 -0
- package/src/components/FieldTree.tsx +305 -0
- package/src/components/FlowTrigger.tsx +34 -0
- package/src/components/Ident.tsx +89 -0
- package/src/components/Integrations.tsx +243 -0
- package/src/components/LifecycleDiagram.tsx +110 -0
- package/src/components/MachineDocs.tsx +42 -0
- package/src/components/Markdown.tsx +173 -0
- package/src/components/MarkdownTable.tsx +206 -0
- package/src/components/Mermaid.tsx +211 -0
- package/src/components/MethodRows.tsx +309 -0
- package/src/components/Overlay.tsx +125 -0
- package/src/components/PageHeader.tsx +189 -0
- package/src/components/RowActions.tsx +97 -0
- package/src/components/SchemaDocument.tsx +89 -0
- package/src/components/Select.tsx +126 -0
- package/src/components/ShapeRows.tsx +118 -0
- package/src/components/SourceDoc.tsx +82 -0
- package/src/components/SourcePreview.tsx +441 -0
- package/src/components/TabRow.tsx +182 -0
- package/src/components/TechIcon.tsx +34 -0
- package/src/components/Toc.tsx +132 -0
- package/src/components/WhatLinksHere.tsx +273 -0
- package/src/components/WsdlReference.tsx +84 -0
- package/src/components/ddd-icons.tsx +107 -0
- package/src/components/kind.tsx +157 -0
- package/src/components/logo.tsx +61 -0
- package/src/components/primitives.tsx +264 -0
- package/src/data.test.ts +18 -0
- package/src/data.ts +202 -0
- package/src/enrich.test.ts +1150 -0
- package/src/enrich.ts +672 -0
- package/src/er/ErCanvas.tsx +533 -0
- package/src/er/RedisSchema.tsx +74 -0
- package/src/er/RelationCard.tsx +267 -0
- package/src/er/StoreHeader.tsx +155 -0
- package/src/er/TableNode.tsx +84 -0
- package/src/er/ViewNode.tsx +83 -0
- package/src/er/layout.test.ts +115 -0
- package/src/er/layout.ts +77 -0
- package/src/er/lineage.test.ts +140 -0
- package/src/er/lineage.ts +107 -0
- package/src/er/markers.tsx +79 -0
- package/src/er/spec.test.ts +344 -0
- package/src/er/spec.ts +426 -0
- package/src/flow/ChainList.tsx +162 -0
- package/src/flow/FlowTable.tsx +123 -0
- package/src/flow/FlowToolbar.tsx +372 -0
- package/src/flow/StepDetail.tsx +380 -0
- package/src/flow/StepRail.tsx +428 -0
- package/src/flow/answers.test.ts +50 -0
- package/src/flow/answers.ts +58 -0
- package/src/flow/chain.test.ts +383 -0
- package/src/flow/chain.ts +353 -0
- package/src/flow/chapters.test.ts +185 -0
- package/src/flow/chapters.ts +225 -0
- package/src/flow/continues.test.ts +102 -0
- package/src/flow/continues.ts +65 -0
- package/src/flow/cross-context.test.ts +97 -0
- package/src/flow/cross-context.ts +35 -0
- package/src/flow/mermaid.test.ts +22 -0
- package/src/flow/mermaid.ts +141 -0
- package/src/flow/outline.test.ts +212 -0
- package/src/flow/outline.ts +170 -0
- package/src/flow/paths.test.ts +129 -0
- package/src/flow/paths.ts +147 -0
- package/src/flow/prefs.test.ts +80 -0
- package/src/flow/prefs.ts +104 -0
- package/src/graph/DependencyGraph.tsx +283 -0
- package/src/graph/DependencyNodes.tsx +162 -0
- package/src/graph/EdgeLabel.tsx +61 -0
- package/src/graph/FocusedEventGraph.tsx +211 -0
- package/src/graph/GraphToolbar.tsx +187 -0
- package/src/graph/RoutedEdge.tsx +80 -0
- package/src/graph/dependency-layout.test.ts +360 -0
- package/src/graph/dependency-layout.ts +444 -0
- package/src/graph/elk.test.ts +70 -0
- package/src/graph/elk.ts +287 -0
- package/src/graph/nodes.tsx +97 -0
- package/src/graph/theme.ts +81 -0
- package/src/graph/useElkFlow.ts +125 -0
- package/src/index.css +1547 -0
- package/src/language/InLanguage.tsx +40 -0
- package/src/language/TermCard.tsx +132 -0
- package/src/language/cards.test.ts +114 -0
- package/src/language/cards.ts +111 -0
- package/src/lib/admonition.test.ts +49 -0
- package/src/lib/admonition.ts +48 -0
- package/src/lib/adr.test.ts +131 -0
- package/src/lib/adr.ts +68 -0
- package/src/lib/api.test.ts +122 -0
- package/src/lib/api.ts +135 -0
- package/src/lib/backlinks.test.ts +330 -0
- package/src/lib/backlinks.ts +567 -0
- package/src/lib/branch-compare.ts +16 -0
- package/src/lib/build-info.test.ts +76 -0
- package/src/lib/build-info.ts +80 -0
- package/src/lib/catalog-diff.test.ts +275 -0
- package/src/lib/catalog-diff.ts +655 -0
- package/src/lib/clipboard.ts +43 -0
- package/src/lib/comparison-memory.test.ts +40 -0
- package/src/lib/comparison-memory.ts +39 -0
- package/src/lib/context-color.ts +27 -0
- package/src/lib/context-map.test.ts +148 -0
- package/src/lib/context-map.ts +399 -0
- package/src/lib/data-model.test.ts +245 -0
- package/src/lib/data-model.ts +399 -0
- package/src/lib/data-problems.test.ts +178 -0
- package/src/lib/data-problems.ts +314 -0
- package/src/lib/derive.test.ts +251 -0
- package/src/lib/derive.ts +515 -0
- package/src/lib/editor-link.test.ts +125 -0
- package/src/lib/editor-link.ts +96 -0
- package/src/lib/editor-prefs.ts +27 -0
- package/src/lib/event-graph.test.ts +237 -0
- package/src/lib/event-graph.ts +327 -0
- package/src/lib/export-canvas.ts +37 -0
- package/src/lib/export-file.test.ts +30 -0
- package/src/lib/export-file.ts +20 -0
- package/src/lib/flow-tree.test.ts +177 -0
- package/src/lib/flow-tree.ts +160 -0
- package/src/lib/forge-refs.test.ts +50 -0
- package/src/lib/forge-refs.ts +60 -0
- package/src/lib/format.test.ts +35 -0
- package/src/lib/format.ts +64 -0
- package/src/lib/github-catalog.test.ts +301 -0
- package/src/lib/github-catalog.ts +610 -0
- package/src/lib/integrations.test.ts +119 -0
- package/src/lib/integrations.ts +133 -0
- package/src/lib/kinds.test.ts +91 -0
- package/src/lib/kinds.ts +204 -0
- package/src/lib/lifecycle.test.ts +93 -0
- package/src/lib/lifecycle.ts +165 -0
- package/src/lib/local-api.ts +194 -0
- package/src/lib/motion-features.ts +11 -0
- package/src/lib/motion.test.ts +120 -0
- package/src/lib/motion.tsx +269 -0
- package/src/lib/palette-recent.test.ts +80 -0
- package/src/lib/palette-recent.ts +90 -0
- package/src/lib/palette.test.ts +365 -0
- package/src/lib/palette.ts +593 -0
- package/src/lib/pins.test.ts +139 -0
- package/src/lib/pins.ts +130 -0
- package/src/lib/problems.test.ts +112 -0
- package/src/lib/project-preview.ts +5 -0
- package/src/lib/proto-problems.test.ts +121 -0
- package/src/lib/proto-problems.ts +65 -0
- package/src/lib/queries.test.ts +113 -0
- package/src/lib/queries.ts +131 -0
- package/src/lib/registry.test.ts +219 -0
- package/src/lib/registry.ts +208 -0
- package/src/lib/scenarios.ts +547 -0
- package/src/lib/setup-info.test.ts +199 -0
- package/src/lib/setup-info.ts +415 -0
- package/src/lib/shape.test.ts +289 -0
- package/src/lib/shape.ts +454 -0
- package/src/lib/sidebar-prefs.test.ts +32 -0
- package/src/lib/sidebar-prefs.ts +75 -0
- package/src/lib/source-code.test.ts +159 -0
- package/src/lib/source-code.ts +286 -0
- package/src/lib/source-doc.test.ts +131 -0
- package/src/lib/source-doc.ts +103 -0
- package/src/lib/source-highlight.test.ts +29 -0
- package/src/lib/source-highlight.ts +24 -0
- package/src/lib/source-link.test.ts +153 -0
- package/src/lib/source-link.ts +201 -0
- package/src/lib/spec-files.test.ts +24 -0
- package/src/lib/spec-files.ts +23 -0
- package/src/lib/table-directive.test.ts +139 -0
- package/src/lib/table-directive.ts +106 -0
- package/src/lib/tech.test.ts +120 -0
- package/src/lib/tech.ts +421 -0
- package/src/lib/terms.test.ts +67 -0
- package/src/lib/terms.ts +179 -0
- package/src/lib/wire-problems.test.ts +263 -0
- package/src/lib/wire-problems.ts +281 -0
- package/src/likec4/C4View.tsx +55 -0
- package/src/likec4/CanvasBridge.tsx +187 -0
- package/src/likec4/FlowView.tsx +235 -0
- package/src/likec4/InteractiveView.tsx +144 -0
- package/src/likec4/canvas-viewport.test.ts +149 -0
- package/src/likec4/canvas-viewport.ts +135 -0
- package/src/likec4/flow-edges.test.ts +51 -0
- package/src/likec4/flow-edges.ts +89 -0
- package/src/likec4/frame-theme.test.ts +76 -0
- package/src/likec4/frame-theme.ts +101 -0
- package/src/likec4/highlight-css.test.ts +49 -0
- package/src/likec4/highlight-css.ts +52 -0
- package/src/likec4/ids.test.ts +151 -0
- package/src/likec4/ids.ts +115 -0
- package/src/likec4/levels.tsx +70 -0
- package/src/likec4/mapping.test.ts +66 -0
- package/src/likec4/mapping.ts +54 -0
- package/src/likec4/reserved.json +141 -0
- package/src/likec4/view-index.ts +78 -0
- package/src/main.tsx +13 -0
- package/src/map/ContextMapGraph.tsx +274 -0
- package/src/map/ContextNode.tsx +70 -0
- package/src/map/FloatingEdge.tsx +86 -0
- package/src/merge.test.ts +1038 -0
- package/src/merge.ts +1004 -0
- package/src/pages/AdrDetail.tsx +242 -0
- package/src/pages/AdrIndex.tsx +114 -0
- package/src/pages/AggregatePage.tsx +727 -0
- package/src/pages/BlockPage.tsx +478 -0
- package/src/pages/CatalogFailure.tsx +111 -0
- package/src/pages/Changes.tsx +313 -0
- package/src/pages/ContextMap.tsx +534 -0
- package/src/pages/ContextPage.tsx +346 -0
- package/src/pages/EnumPage.tsx +282 -0
- package/src/pages/EventPage.tsx +686 -0
- package/src/pages/ExternalPage.tsx +204 -0
- package/src/pages/FlowDetail.tsx +663 -0
- package/src/pages/FlowIndex.tsx +303 -0
- package/src/pages/GraphPage.tsx +217 -0
- package/src/pages/Language.tsx +257 -0
- package/src/pages/ModulePage.tsx +366 -0
- package/src/pages/NotFound.tsx +54 -0
- package/src/pages/Overview.tsx +344 -0
- package/src/pages/Problems.tsx +360 -0
- package/src/pages/RegistryIndex.tsx +118 -0
- package/src/pages/ServicePage.tsx +746 -0
- package/src/pages/Settings.tsx +975 -0
- package/src/pages/StorePage.tsx +114 -0
- package/src/peek/PeekCard.test.ts +35 -0
- package/src/peek/PeekCard.tsx +118 -0
- package/src/peek/PeekLayer.tsx +147 -0
- package/src/peek/model.test.ts +156 -0
- package/src/peek/model.ts +253 -0
- package/src/routes.test.ts +222 -0
- package/src/routes.ts +449 -0
- package/src/selection/DetailPanel.tsx +1407 -0
- package/src/selection/hash.test.ts +73 -0
- package/src/selection/hash.ts +58 -0
- package/src/selection/model.test.ts +199 -0
- package/src/selection/model.ts +473 -0
- package/src/selection/pages.test.ts +115 -0
- package/src/selection/pages.ts +268 -0
- package/src/selection/store.test.ts +62 -0
- package/src/selection/store.ts +46 -0
- package/src/selection/sync.tsx +99 -0
- package/src/table/ColumnsMenu.tsx +72 -0
- package/src/table/DataTable.tsx +780 -0
- package/src/table/Facets.tsx +54 -0
- package/src/table/GroupMenu.tsx +69 -0
- package/src/table/Toolbar.tsx +190 -0
- package/src/table/cells.tsx +117 -0
- package/src/table/compare.test.ts +117 -0
- package/src/table/compare.ts +196 -0
- package/src/table/export.test.ts +88 -0
- package/src/table/export.ts +66 -0
- package/src/table/facet-groups.test.ts +74 -0
- package/src/table/facet-groups.ts +56 -0
- package/src/table/grouping.test.ts +84 -0
- package/src/table/grouping.ts +82 -0
- package/src/table/infer.test.ts +57 -0
- package/src/table/infer.ts +50 -0
- package/src/table/persist.test.ts +76 -0
- package/src/table/persist.ts +112 -0
- package/src/table/sort-url.test.ts +75 -0
- package/src/table/sort-url.ts +52 -0
- package/src/table/types.ts +80 -0
- package/src/table/useTableState.ts +220 -0
- package/src/test-catalog.ts +7 -0
- package/src/testing/estate/data__catalog.json +2847 -0
- package/src/testing/estate/data__flows.json +1724 -0
- package/src/testing/estate/examples__auth__portolan__api.json +212 -0
- package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
- package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
- package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
- package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
- package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
- package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
- package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
- package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
- package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
- package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
- package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
- package/src/testing/estate.ts +49 -0
- package/src/trail/Trail.tsx +81 -0
- package/src/trail/model.test.ts +157 -0
- package/src/trail/model.ts +189 -0
- package/src/trail/record.tsx +32 -0
- package/src/trail/store.test.ts +66 -0
- package/src/trail/store.ts +102 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +171 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// The keyboard.
|
|
2
|
+
//
|
|
3
|
+
// One handler, one table. The table below is what the "?" sheet prints and what
|
|
4
|
+
// the handler implements, so the sheet cannot drift from the app: adding a
|
|
5
|
+
// shortcut without listing it is not possible without editing both halves of
|
|
6
|
+
// the same object.
|
|
7
|
+
//
|
|
8
|
+
// Two rules hold everywhere. Nothing fires while a text field has focus - a
|
|
9
|
+
// reader typing "flows" into a table's filter must not be teleported four
|
|
10
|
+
// times - and ⌘K keeps working regardless, because opening the palette is the
|
|
11
|
+
// one thing a reader does *from* a field.
|
|
12
|
+
|
|
13
|
+
import { useEffect, useRef } from "react";
|
|
14
|
+
import { useNavigate } from "react-router";
|
|
15
|
+
import { X } from "lucide-react";
|
|
16
|
+
import { Modal } from "../components/Overlay";
|
|
17
|
+
import { OVERVIEW_ANCHOR, paths } from "../routes";
|
|
18
|
+
|
|
19
|
+
export interface Shortcut {
|
|
20
|
+
/** Rendered as separate keycaps; a chord is two entries. */
|
|
21
|
+
keys: string[];
|
|
22
|
+
what: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The "g" chord, once. The handler navigates from this table and the sheet
|
|
27
|
+
* prints it, so a destination cannot exist in one and not the other.
|
|
28
|
+
*/
|
|
29
|
+
export const GO_TO: { key: string; to: string; what: string }[] = [
|
|
30
|
+
{ key: "f", to: paths.flows(), what: "flows" },
|
|
31
|
+
{
|
|
32
|
+
key: "d",
|
|
33
|
+
to: `${paths.overview()}#${OVERVIEW_ANCHOR.contexts}`,
|
|
34
|
+
what: "the contexts, on the overview",
|
|
35
|
+
},
|
|
36
|
+
{ key: "m", to: paths.map(), what: "the context map" },
|
|
37
|
+
{ key: "p", to: paths.problems(), what: "problems" },
|
|
38
|
+
{ key: "o", to: paths.overview(), what: "overview" },
|
|
39
|
+
{ key: "a", to: paths.adrs(), what: "decisions" },
|
|
40
|
+
{ key: "g", to: paths.graph(), what: "the dependency graph" },
|
|
41
|
+
{ key: "l", to: paths.language(), what: "language" },
|
|
42
|
+
{ key: "r", to: paths.registry(), what: "registry" },
|
|
43
|
+
{ key: "c", to: paths.changes(), what: "changes" },
|
|
44
|
+
{ key: "s", to: paths.settings(), what: "settings" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export const SHORTCUT_GROUPS: { group: string; items: Shortcut[] }[] = [
|
|
48
|
+
{
|
|
49
|
+
group: "Go to",
|
|
50
|
+
items: GO_TO.map((g) => ({ keys: ["g", g.key], what: g.what })),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
group: "Layout",
|
|
54
|
+
items: [
|
|
55
|
+
{ keys: ["["], what: "show or hide the catalog tree" },
|
|
56
|
+
{ keys: ["]"], what: "show or hide the detail panel" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
group: "Lists, tables and the steps of a flow",
|
|
61
|
+
items: [
|
|
62
|
+
{ keys: ["j"], what: "next row or step" },
|
|
63
|
+
{ keys: ["k"], what: "previous row or step" },
|
|
64
|
+
{ keys: ["⏎"], what: "open the row, or select the step" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
group: "The catalog tree",
|
|
69
|
+
items: [
|
|
70
|
+
{ keys: ["↓"], what: "next row" },
|
|
71
|
+
{ keys: ["↑"], what: "previous row" },
|
|
72
|
+
{ keys: ["→"], what: "open the branch, or step into it" },
|
|
73
|
+
{ keys: ["←"], what: "close the branch, or go up to its parent" },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
group: "Everywhere",
|
|
78
|
+
items: [
|
|
79
|
+
{ keys: ["⌘", "K"], what: "search the whole catalog" },
|
|
80
|
+
{ keys: ["?"], what: "this sheet" },
|
|
81
|
+
{ keys: ["Esc"], what: "clear the selection, or close what is open" },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
/** True while the keystroke belongs to something the reader is typing into. */
|
|
87
|
+
function inField(target: EventTarget | null): boolean {
|
|
88
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
89
|
+
return (
|
|
90
|
+
target.tagName === "INPUT" ||
|
|
91
|
+
target.tagName === "TEXTAREA" ||
|
|
92
|
+
target.tagName === "SELECT" ||
|
|
93
|
+
target.isContentEditable
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* j / k walk whatever list the reader is standing in, and the first one on the
|
|
99
|
+
* page when they are standing in none. Movement is DOM focus rather than a
|
|
100
|
+
* highlight of our own, so Enter is the browser's Enter and the focus ring is
|
|
101
|
+
* the one every other control uses.
|
|
102
|
+
*/
|
|
103
|
+
function moveListFocus(delta: number): boolean {
|
|
104
|
+
const active = document.activeElement as HTMLElement | null;
|
|
105
|
+
// Standing in a list walks that list. Standing nowhere walks the page's own
|
|
106
|
+
// list before the catalog tree's - the tree is always on screen, and always
|
|
107
|
+
// first in the DOM, so preferring it would mean j never touched the thing
|
|
108
|
+
// the reader is actually looking at.
|
|
109
|
+
const list =
|
|
110
|
+
active?.closest<HTMLElement>("[data-nav-list]") ??
|
|
111
|
+
document.querySelector<HTMLElement>("main [data-nav-list]") ??
|
|
112
|
+
document.querySelector<HTMLElement>("[data-nav-list]");
|
|
113
|
+
if (!list) return false;
|
|
114
|
+
const items = [...list.querySelectorAll<HTMLElement>("[data-nav-item]")];
|
|
115
|
+
if (items.length === 0) return false;
|
|
116
|
+
const at = active ? items.indexOf(active) : -1;
|
|
117
|
+
const next =
|
|
118
|
+
at < 0
|
|
119
|
+
? delta > 0
|
|
120
|
+
? 0
|
|
121
|
+
: items.length - 1
|
|
122
|
+
: Math.min(items.length - 1, Math.max(0, at + delta));
|
|
123
|
+
items[next]?.focus();
|
|
124
|
+
items[next]?.scrollIntoView({ block: "nearest" });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ShortcutActions {
|
|
129
|
+
openPalette: () => void;
|
|
130
|
+
openHelp: () => void;
|
|
131
|
+
toggleSidebar: () => void;
|
|
132
|
+
toggleDetail: () => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** How long a "g" waits for its second key before it goes back to being a "g". */
|
|
136
|
+
const CHORD_MS = 1200;
|
|
137
|
+
|
|
138
|
+
export function useShortcuts(actions: ShortcutActions, enabled: boolean): void {
|
|
139
|
+
const navigate = useNavigate();
|
|
140
|
+
// Refs so the listener is installed once: rebinding it on every render would
|
|
141
|
+
// drop a chord half-typed.
|
|
142
|
+
const act = useRef(actions);
|
|
143
|
+
act.current = actions;
|
|
144
|
+
const on = useRef(enabled);
|
|
145
|
+
on.current = enabled;
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
let chord = false;
|
|
149
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
150
|
+
|
|
151
|
+
const endChord = () => {
|
|
152
|
+
chord = false;
|
|
153
|
+
if (timer) clearTimeout(timer);
|
|
154
|
+
timer = null;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const onKey = (e: KeyboardEvent) => {
|
|
158
|
+
// ⌘K first, and from anywhere at all - including a field.
|
|
159
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
act.current.openPalette();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!on.current) return;
|
|
165
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
166
|
+
if (inField(e.target)) return;
|
|
167
|
+
if (e.defaultPrevented) return;
|
|
168
|
+
|
|
169
|
+
const key = e.key;
|
|
170
|
+
|
|
171
|
+
if (chord) {
|
|
172
|
+
endChord();
|
|
173
|
+
const target = GO_TO.find((g) => g.key === key);
|
|
174
|
+
if (target) {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
navigate(target.to);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (key === "g") {
|
|
182
|
+
chord = true;
|
|
183
|
+
timer = setTimeout(endChord, CHORD_MS);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// "?" is Shift+/ on most layouts and a key of its own on some.
|
|
188
|
+
if (key === "?") {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
act.current.openHelp();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (key === "[") {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
act.current.toggleSidebar();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (key === "]") {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
act.current.toggleDetail();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (key === "j" || key === "k") {
|
|
204
|
+
if (moveListFocus(key === "j" ? 1 : -1)) e.preventDefault();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
window.addEventListener("keydown", onKey);
|
|
210
|
+
return () => {
|
|
211
|
+
window.removeEventListener("keydown", onKey);
|
|
212
|
+
if (timer) clearTimeout(timer);
|
|
213
|
+
};
|
|
214
|
+
}, [navigate]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Cap({ children }: { children: React.ReactNode }) {
|
|
218
|
+
return (
|
|
219
|
+
<kbd className="mono inline-flex min-w-5 items-center justify-center rounded-[4px] border px-1 py-px border-line-strong text-ink">
|
|
220
|
+
{children}
|
|
221
|
+
</kbd>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function ShortcutsSheet({
|
|
226
|
+
open,
|
|
227
|
+
onClose,
|
|
228
|
+
}: {
|
|
229
|
+
open: boolean;
|
|
230
|
+
onClose: () => void;
|
|
231
|
+
}) {
|
|
232
|
+
return (
|
|
233
|
+
<Modal
|
|
234
|
+
open={open}
|
|
235
|
+
onClose={onClose}
|
|
236
|
+
label="Keyboard shortcuts"
|
|
237
|
+
width="min(560px,92vw)"
|
|
238
|
+
>
|
|
239
|
+
<>
|
|
240
|
+
<div className="flex shrink-0 items-center gap-2 border-b px-4 py-2.5 border-line">
|
|
241
|
+
<span className="label">keyboard</span>
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
onClick={onClose}
|
|
245
|
+
aria-label="Close (Esc)"
|
|
246
|
+
title="Esc"
|
|
247
|
+
className="ml-auto rounded-control p-1 text-muted t-micro transition-colors hover:bg-surface hover:text-ink"
|
|
248
|
+
>
|
|
249
|
+
<X size={16} aria-hidden />
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div className="flex-1 overflow-y-auto px-4 py-3">
|
|
254
|
+
{SHORTCUT_GROUPS.map((group) => (
|
|
255
|
+
<section key={group.group} className="mb-4 last:mb-0">
|
|
256
|
+
<div className="label mb-1.5">{group.group}</div>
|
|
257
|
+
<ul>
|
|
258
|
+
{group.items.map((item) => (
|
|
259
|
+
<li
|
|
260
|
+
key={item.keys.join("+")}
|
|
261
|
+
className="flex items-baseline gap-3 py-1"
|
|
262
|
+
>
|
|
263
|
+
<span className="flex shrink-0 items-center gap-1">
|
|
264
|
+
{item.keys.map((k, i) => (
|
|
265
|
+
<Cap key={`${k}-${i}`}>{k}</Cap>
|
|
266
|
+
))}
|
|
267
|
+
</span>
|
|
268
|
+
<span className="text-muted">{item.what}</span>
|
|
269
|
+
</li>
|
|
270
|
+
))}
|
|
271
|
+
</ul>
|
|
272
|
+
</section>
|
|
273
|
+
))}
|
|
274
|
+
<p className="meta border-t pt-3 border-line">
|
|
275
|
+
none of these fire while a text field has focus — ⌘K is the one
|
|
276
|
+
exception, because that is what a field is for
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
</>
|
|
280
|
+
</Modal>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
|
|
11
|
+
type Theme = "dark" | "light";
|
|
12
|
+
|
|
13
|
+
interface ThemeCtx {
|
|
14
|
+
theme: Theme;
|
|
15
|
+
toggle: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Ctx = createContext<ThemeCtx>({ theme: "dark", toggle: () => {} });
|
|
19
|
+
|
|
20
|
+
const KEY = "portolan.theme";
|
|
21
|
+
|
|
22
|
+
function initial(): Theme {
|
|
23
|
+
const stored =
|
|
24
|
+
typeof localStorage === "undefined" ? null : localStorage.getItem(KEY);
|
|
25
|
+
if (stored === "light" || stored === "dark") return stored;
|
|
26
|
+
return "dark";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
30
|
+
const [theme, setTheme] = useState<Theme>(initial);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
34
|
+
try {
|
|
35
|
+
localStorage.setItem(KEY, theme);
|
|
36
|
+
} catch {
|
|
37
|
+
/* private mode: the toggle still works for this session */
|
|
38
|
+
}
|
|
39
|
+
}, [theme]);
|
|
40
|
+
|
|
41
|
+
const toggle = useCallback(
|
|
42
|
+
() => setTheme((t) => (t === "dark" ? "light" : "dark")),
|
|
43
|
+
[],
|
|
44
|
+
);
|
|
45
|
+
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
|
|
46
|
+
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useTheme(): ThemeCtx {
|
|
50
|
+
return useContext(Ctx);
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { pageTitle } from "./title";
|
|
3
|
+
|
|
4
|
+
describe("pageTitle", () => {
|
|
5
|
+
it("puts the page's name before the app's", () => {
|
|
6
|
+
expect(pageTitle("Orders")).toBe("Orders · portolan");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("falls back to the app's name when the page has none", () => {
|
|
10
|
+
expect(pageTitle(undefined)).toBe("portolan");
|
|
11
|
+
expect(pageTitle(null)).toBe("portolan");
|
|
12
|
+
expect(pageTitle(" ")).toBe("portolan");
|
|
13
|
+
});
|
|
14
|
+
});
|
package/src/app/title.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The tab's name.
|
|
2
|
+
//
|
|
3
|
+
// Every page used to be called "portolan": twelve tabs open on twelve pages
|
|
4
|
+
// read as twelve copies of one, history was a list of the same word, and a
|
|
5
|
+
// screen reader announced nothing on a route change. The page that knows its
|
|
6
|
+
// own name says it here, and the app's name follows so the tab is still
|
|
7
|
+
// findable among a reader's others.
|
|
8
|
+
|
|
9
|
+
import { useEffect } from "react";
|
|
10
|
+
|
|
11
|
+
const APP = "portolan";
|
|
12
|
+
|
|
13
|
+
/** "Orders · portolan", or the bare app name when a page has no name yet. */
|
|
14
|
+
export function pageTitle(name: string | null | undefined): string {
|
|
15
|
+
const trimmed = name?.trim();
|
|
16
|
+
return trimmed ? `${trimmed} · ${APP}` : APP;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sets the document title while the caller is mounted, and hands it back on
|
|
21
|
+
* the way out so a page that sets none is not called by the last one that did.
|
|
22
|
+
*/
|
|
23
|
+
export function useDocumentTitle(name: string | null | undefined): void {
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
document.title = pageTitle(name);
|
|
26
|
+
return () => {
|
|
27
|
+
document.title = APP;
|
|
28
|
+
};
|
|
29
|
+
}, [name]);
|
|
30
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// One line, bottom left, gone in four seconds.
|
|
2
|
+
//
|
|
3
|
+
// The app has exactly one thing to say this way: that a pin pushed another pin
|
|
4
|
+
// out. It is not an error and it is not a page, but it must not be silent
|
|
5
|
+
// either - a reader who watches their oldest pin vanish with no word learns
|
|
6
|
+
// that the pin list loses things at random.
|
|
7
|
+
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
import { create } from "zustand";
|
|
10
|
+
import { AnimatePresence, m, rise } from "../lib/motion";
|
|
11
|
+
|
|
12
|
+
interface ToastState {
|
|
13
|
+
message: string | null;
|
|
14
|
+
/** Bumped per call, so the same message twice restarts the timer. */
|
|
15
|
+
nonce: number;
|
|
16
|
+
say: (message: string) => void;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const useToastStore = create<ToastState>()((set) => ({
|
|
21
|
+
message: null,
|
|
22
|
+
nonce: 0,
|
|
23
|
+
say: (message) => set((s) => ({ message, nonce: s.nonce + 1 })),
|
|
24
|
+
clear: () => set({ message: null }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const SHOWN_MS = 4000;
|
|
28
|
+
|
|
29
|
+
export function Toaster() {
|
|
30
|
+
const message = useToastStore((s) => s.message);
|
|
31
|
+
const nonce = useToastStore((s) => s.nonce);
|
|
32
|
+
const clear = useToastStore((s) => s.clear);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!message) return;
|
|
36
|
+
const timer = setTimeout(clear, SHOWN_MS);
|
|
37
|
+
return () => clearTimeout(timer);
|
|
38
|
+
}, [message, nonce, clear]);
|
|
39
|
+
|
|
40
|
+
// It rises in and sinks out. A second message while one is up swaps the
|
|
41
|
+
// text in place: the box is the same box, only its line changed.
|
|
42
|
+
return (
|
|
43
|
+
<AnimatePresence>
|
|
44
|
+
{message && (
|
|
45
|
+
<m.div
|
|
46
|
+
{...rise}
|
|
47
|
+
role="status"
|
|
48
|
+
aria-live="polite"
|
|
49
|
+
/* Bottom RIGHT, not left: the foot of the sidebar is where Problems
|
|
50
|
+
and the build line are pinned, and a message that covers them for
|
|
51
|
+
four seconds hides the one row that promised never to leave. */
|
|
52
|
+
className="mono pointer-events-none fixed right-4 bottom-4 z-50 max-w-80 rounded-control border px-3 py-2 shadow-md border-line bg-canvas text-ink"
|
|
53
|
+
>
|
|
54
|
+
{message}
|
|
55
|
+
</m.div>
|
|
56
|
+
)}
|
|
57
|
+
</AnimatePresence>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parentOf, treeKey } from "./tree-keys";
|
|
3
|
+
import type { TreeRow } from "./tree-keys";
|
|
4
|
+
|
|
5
|
+
// shop (open)
|
|
6
|
+
// oms (open)
|
|
7
|
+
// Order (closed)
|
|
8
|
+
// Cart (leaf)
|
|
9
|
+
// billing (closed)
|
|
10
|
+
// auth (closed)
|
|
11
|
+
const rows: TreeRow[] = [
|
|
12
|
+
{ depth: 0, open: true },
|
|
13
|
+
{ depth: 1, open: true },
|
|
14
|
+
{ depth: 2, open: false },
|
|
15
|
+
{ depth: 2, open: null },
|
|
16
|
+
{ depth: 1, open: false },
|
|
17
|
+
{ depth: 0, open: false },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe("parentOf", () => {
|
|
21
|
+
it("is the nearest shallower row above", () => {
|
|
22
|
+
expect(parentOf(rows, 3)).toBe(1);
|
|
23
|
+
expect(parentOf(rows, 4)).toBe(0);
|
|
24
|
+
expect(parentOf(rows, 1)).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("is -1 at the top level", () => {
|
|
28
|
+
expect(parentOf(rows, 0)).toBe(-1);
|
|
29
|
+
expect(parentOf(rows, 5)).toBe(-1);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("treeKey", () => {
|
|
34
|
+
it("walks down and up, and stops at the ends", () => {
|
|
35
|
+
expect(treeKey(rows, 0, "ArrowDown")).toEqual({ type: "focus", index: 1 });
|
|
36
|
+
expect(treeKey(rows, 5, "ArrowDown")).toEqual({ type: "focus", index: 5 });
|
|
37
|
+
expect(treeKey(rows, 3, "ArrowUp")).toEqual({ type: "focus", index: 2 });
|
|
38
|
+
expect(treeKey(rows, 0, "ArrowUp")).toEqual({ type: "focus", index: 0 });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("enters the tree at either end when nothing is focused", () => {
|
|
42
|
+
expect(treeKey(rows, -1, "ArrowDown")).toEqual({ type: "focus", index: 0 });
|
|
43
|
+
expect(treeKey(rows, -1, "ArrowUp")).toEqual({ type: "focus", index: 5 });
|
|
44
|
+
expect(treeKey(rows, -1, "ArrowRight")).toBeNull();
|
|
45
|
+
expect(treeKey(rows, -1, "ArrowLeft")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("jumps to the first and last row", () => {
|
|
49
|
+
expect(treeKey(rows, 3, "Home")).toEqual({ type: "focus", index: 0 });
|
|
50
|
+
expect(treeKey(rows, 3, "End")).toEqual({ type: "focus", index: 5 });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("opens a closed branch, then steps into it", () => {
|
|
54
|
+
expect(treeKey(rows, 2, "ArrowRight")).toEqual({ type: "toggle", index: 2 });
|
|
55
|
+
expect(treeKey(rows, 1, "ArrowRight")).toEqual({ type: "focus", index: 2 });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("does nothing on a leaf, or on an open branch with no children", () => {
|
|
59
|
+
expect(treeKey(rows, 3, "ArrowRight")).toBeNull();
|
|
60
|
+
const childless: TreeRow[] = [{ depth: 0, open: true }, { depth: 0, open: null }];
|
|
61
|
+
expect(treeKey(childless, 0, "ArrowRight")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("closes an open branch, and goes up from anything else", () => {
|
|
65
|
+
expect(treeKey(rows, 1, "ArrowLeft")).toEqual({ type: "toggle", index: 1 });
|
|
66
|
+
expect(treeKey(rows, 3, "ArrowLeft")).toEqual({ type: "focus", index: 1 });
|
|
67
|
+
expect(treeKey(rows, 2, "ArrowLeft")).toEqual({ type: "focus", index: 1 });
|
|
68
|
+
expect(treeKey(rows, 4, "ArrowLeft")).toEqual({ type: "focus", index: 0 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does nothing going up from the top level, or on an empty tree", () => {
|
|
72
|
+
expect(treeKey(rows, 5, "ArrowLeft")).toBeNull();
|
|
73
|
+
expect(treeKey([], -1, "ArrowDown")).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("ignores keys it does not own", () => {
|
|
77
|
+
expect(treeKey(rows, 1, "Enter")).toBeNull();
|
|
78
|
+
expect(treeKey(rows, 1, "j")).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The arrow keys in the catalog tree, as a function of the rows on screen.
|
|
2
|
+
//
|
|
3
|
+
// j and k already walk the tree the way they walk every list, but a tree is
|
|
4
|
+
// more than a list: a branch opens and closes, and a reader on a leaf wants
|
|
5
|
+
// the branch it hangs from. This is the tree's own grammar - the one every
|
|
6
|
+
// file browser and outliner taught - and it is decided here, on a description
|
|
7
|
+
// of the rows, so the Sidebar has only to describe its rows and carry out
|
|
8
|
+
// the answer.
|
|
9
|
+
//
|
|
10
|
+
// ↓ ↑ the next or previous row, whatever depth it is at
|
|
11
|
+
// → open a closed branch; on an open one, step into its first child
|
|
12
|
+
// ← close an open branch; on anything else, go up to the parent
|
|
13
|
+
// Home the first row
|
|
14
|
+
// End the last row
|
|
15
|
+
|
|
16
|
+
export interface TreeRow {
|
|
17
|
+
depth: number;
|
|
18
|
+
/** true or false for a branch; null for a leaf, which has nothing to open. */
|
|
19
|
+
open: boolean | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TreeMove =
|
|
23
|
+
| { type: "focus"; index: number }
|
|
24
|
+
| { type: "toggle"; index: number };
|
|
25
|
+
|
|
26
|
+
export const TREE_KEYS = new Set([
|
|
27
|
+
"ArrowDown",
|
|
28
|
+
"ArrowUp",
|
|
29
|
+
"ArrowRight",
|
|
30
|
+
"ArrowLeft",
|
|
31
|
+
"Home",
|
|
32
|
+
"End",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/** The nearest row above `at` that is shallower than it: the parent. */
|
|
36
|
+
export function parentOf(rows: readonly TreeRow[], at: number): number {
|
|
37
|
+
const depth = rows[at]?.depth ?? 0;
|
|
38
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
39
|
+
if (rows[i]!.depth < depth) return i;
|
|
40
|
+
}
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What `key` does when the reader is on row `at` (or on none, when `at` is
|
|
46
|
+
* -1) of `rows`, or null when the key does nothing there - so the caller can
|
|
47
|
+
* leave the event alone and let the page scroll.
|
|
48
|
+
*/
|
|
49
|
+
export function treeKey(
|
|
50
|
+
rows: readonly TreeRow[],
|
|
51
|
+
at: number,
|
|
52
|
+
key: string,
|
|
53
|
+
): TreeMove | null {
|
|
54
|
+
const last = rows.length - 1;
|
|
55
|
+
if (last < 0) return null;
|
|
56
|
+
|
|
57
|
+
switch (key) {
|
|
58
|
+
case "ArrowDown":
|
|
59
|
+
return { type: "focus", index: at < 0 ? 0 : Math.min(last, at + 1) };
|
|
60
|
+
case "ArrowUp":
|
|
61
|
+
return { type: "focus", index: at < 0 ? last : Math.max(0, at - 1) };
|
|
62
|
+
case "Home":
|
|
63
|
+
return { type: "focus", index: 0 };
|
|
64
|
+
case "End":
|
|
65
|
+
return { type: "focus", index: last };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const row = rows[at];
|
|
69
|
+
if (!row) return null;
|
|
70
|
+
|
|
71
|
+
if (key === "ArrowRight") {
|
|
72
|
+
if (row.open === false) return { type: "toggle", index: at };
|
|
73
|
+
const next = rows[at + 1];
|
|
74
|
+
if (row.open === true && next && next.depth > row.depth) {
|
|
75
|
+
return { type: "focus", index: at + 1 };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (key === "ArrowLeft") {
|
|
81
|
+
if (row.open === true) return { type: "toggle", index: at };
|
|
82
|
+
const parent = parentOf(rows, at);
|
|
83
|
+
return parent >= 0 ? { type: "focus", index: parent } : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Shell state that more than one place has to agree about.
|
|
2
|
+
//
|
|
3
|
+
// Two flags, and both exist because a keystroke fired at the window has to
|
|
4
|
+
// reach a panel rendered several routes deep. `detailHidden` is the reader
|
|
5
|
+
// saying "not now" to the detail rail without giving up their selection;
|
|
6
|
+
// `drawer` is the catalog tree, below the narrow breakpoint, where it is an
|
|
7
|
+
// overlay rather than a pane.
|
|
8
|
+
|
|
9
|
+
import { create } from "zustand";
|
|
10
|
+
import { readZebra, writeZebra } from "../table/persist";
|
|
11
|
+
|
|
12
|
+
interface UiState {
|
|
13
|
+
/** True while "]" has folded the detail rail away. Selection is untouched. */
|
|
14
|
+
detailHidden: boolean;
|
|
15
|
+
toggleDetail: () => void;
|
|
16
|
+
setDetailHidden: (hidden: boolean) => void;
|
|
17
|
+
/** Narrow layouts only: the catalog tree, open over the page. */
|
|
18
|
+
drawer: boolean;
|
|
19
|
+
setDrawer: (open: boolean) => void;
|
|
20
|
+
toggleDrawer: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Bumped by "reveal in tree". The tree follows the selection on its own; what
|
|
23
|
+
* it cannot do is give itself back the width it was collapsed out of, so the
|
|
24
|
+
* shell watches this counter and opens the pane - or the drawer - around it.
|
|
25
|
+
*/
|
|
26
|
+
revealNonce: number;
|
|
27
|
+
requestReveal: () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Zebra striping, for every table at once. It is a statement about how the
|
|
30
|
+
* reader wants rows separated, not about the table they happen to be
|
|
31
|
+
* looking at, so it is one flag and it outlives the session.
|
|
32
|
+
*/
|
|
33
|
+
zebra: boolean;
|
|
34
|
+
toggleZebra: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const useUiStore = create<UiState>()((set) => ({
|
|
38
|
+
detailHidden: false,
|
|
39
|
+
toggleDetail: () => set((s) => ({ detailHidden: !s.detailHidden })),
|
|
40
|
+
setDetailHidden: (hidden) => set({ detailHidden: hidden }),
|
|
41
|
+
drawer: false,
|
|
42
|
+
setDrawer: (open) => set({ drawer: open }),
|
|
43
|
+
toggleDrawer: () => set((s) => ({ drawer: !s.drawer })),
|
|
44
|
+
revealNonce: 0,
|
|
45
|
+
requestReveal: () => set((s) => ({ revealNonce: s.revealNonce + 1 })),
|
|
46
|
+
zebra: readZebra(),
|
|
47
|
+
toggleZebra: () =>
|
|
48
|
+
set((s) => {
|
|
49
|
+
writeZebra(!s.zebra);
|
|
50
|
+
return { zebra: !s.zebra };
|
|
51
|
+
}),
|
|
52
|
+
}));
|