diff --git a/README.md b/README.md index 505ee5d..705589c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Lightweight, composable utilities for documentation sites. - **compass** — Headless tree navigation state machine (pure, portable to any runtime) - **teleport** — Keyboard bindings + DOM integration for compass - **lantern** — Theme toggle with flash-free hydration -- **atlas** — Magic links with build-time resolution and broken link detection +- **atlas** — Remark plugin for Wikipedia-style `[[magic links]]` - **lighthouse** — 404 recovery with fuzzy matching Core packages are framework-agnostic. Astro wrappers available for teleport, lantern, and lighthouse. diff --git a/package-lock.json b/package-lock.json index 2634e16..a07b8be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,10 @@ "lru-cache": "^10.4.3" } }, + "node_modules/@bearing-dev/atlas": { + "resolved": "packages/atlas", + "link": true + }, "node_modules/@bearing-dev/compass": { "resolved": "packages/compass", "link": true @@ -973,6 +977,38 @@ "win32" ] }, + "node_modules/@sailkit/kbd-hints": { + "resolved": "packages/kbd-hints", + "link": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -980,18 +1016,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@vitest/expect": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", @@ -1285,6 +1342,17 @@ "dev": true, "license": "MIT" }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1468,6 +1536,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -1726,6 +1805,20 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1756,6 +1849,30 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1937,6 +2054,13 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2230,6 +2354,19 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-port-reachable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", @@ -2293,6 +2430,13 @@ "node": ">=10" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsdom": { "version": "25.0.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", @@ -2371,6 +2515,17 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -2405,148 +2560,687 @@ "node": ">= 0.4" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dev": true, "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mlly/node_modules/pathe": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "dev": true, - "license": "MIT" + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "dev": true, "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" } ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/nwsapi": { @@ -2888,6 +3582,39 @@ "node": ">=0.10.0" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3198,6 +3925,19 @@ "node": ">=0.10.0" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -3381,6 +4121,17 @@ "tree-kill": "cli.js" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -3999,25 +4750,99 @@ "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, - "engines": { - "node": ">=14.17" + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "license": "MIT", - "optional": true, - "peer": true + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, "node_modules/update-check": { "version": "1.5.4", @@ -4050,6 +4875,36 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -4365,10 +5220,20 @@ "dev": true, "license": "MIT" }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "packages/atlas": { - "name": "@sailkit/atlas", + "name": "@bearing-dev/atlas", "version": "0.1.0", - "extraneous": true, "license": "MIT", "dependencies": { "unist-util-visit": "^5.0.0" @@ -4400,6 +5265,308 @@ "vitest": "^2.0.0" } }, + "packages/kbd-hints": { + "name": "@sailkit/kbd-hints", + "version": "0.1.0", + "devDependencies": { + "jsdom": "^26.0.0", + "typescript": "^5.0.0", + "vitest": "^3.0.0" + }, + "peerDependencies": { + "astro": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "astro": { + "optional": true + } + } + }, + "packages/kbd-hints/node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/kbd-hints/node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "packages/kbd-hints/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/kbd-hints/node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/kbd-hints/node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/kbd-hints/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/kbd-hints/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/kbd-hints/node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "packages/kbd-hints/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "packages/kbd-hints/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "packages/kbd-hints/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "packages/kbd-hints/node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "packages/kbd-hints/node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/kbd-hints/node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "packages/lantern": { "name": "@bearing-dev/lantern", "version": "0.1.0", @@ -4443,7 +5610,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@bearing-dev/compass": "*" + "@bearing-dev/compass": ">=0.0.1" }, "devDependencies": { "@playwright/test": "^1.57.0", diff --git a/package.json b/package.json index 4d1a6ab..bccdbdb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "bearing-dev", + "name": "sailkit", "version": "0.0.1", "private": true, "type": "module", diff --git a/packages/atlas/README.md b/packages/atlas/README.md index b1c658b..a437bba 100644 --- a/packages/atlas/README.md +++ b/packages/atlas/README.md @@ -1,90 +1,84 @@ # atlas -Wikipedia-style magic links with build-time resolution and broken link detection. +Remark plugin for Wikipedia-style magic links. -## What Ships +## Syntax -``` -atlas/ -├── remark-magic-links.mjs # Transforms :id and [[id]] syntax to URLs -├── link-resolver.ts # Core resolution logic -├── link-checker.ts # Broken link detection + reporting -└── index.ts # Unified API +```markdown + +Check out [[context-collapse]] for more. +[[context-collapse|Learn about context]] with custom display text. + + +Check out [:context-collapse] for more. +[:context-collapse|Learn about context] with custom display text. ``` -## Link Syntax +## Usage -```markdown - -Check out [:context] for more info. -See [:context|:ctx|:context-management] for fallback resolution. +```javascript +import { remarkMagicLinks } from '@sailkit/atlas'; - -Check out [[context]] for more info. -[[context|Learn about context]] with custom display text. +export default { + markdown: { + remarkPlugins: [ + [remarkMagicLinks, { + urlBuilder: (id) => `/wiki/${id}/`, + }], + ], + }, +}; ``` -## API +### Multi-collection routing -```typescript -interface LinkTarget { - id: string; - slug: string; - url: string; - aliases?: string[]; - placeholder?: boolean; -} - -type ResolveResult = - | { status: 'resolved'; target: LinkTarget } - | { status: 'placeholder'; target: LinkTarget } - | { status: 'unresolved'; id: string }; +The `urlBuilder` receives the raw ID, so you control all resolution logic. A site with different content types can route each to different paths: -function createLinkResolver(targets: LinkTarget[]): { - resolve(id: string): ResolveResult; - resolveFirst(ids: string[]): ResolveResult; +```javascript +// Imaginary pet store with fish, birds, and reptiles +const catalog = { + 'one-fish': { type: 'fish', slug: 'one-fish' }, + 'two-fish': { type: 'fish', slug: 'two-fish' }, + 'red-parrot': { type: 'bird', slug: 'red-parrot' }, + 'blue-gecko': { type: 'reptile', slug: 'blue-gecko' }, }; -function remarkMagicLinks(config: { - targets: LinkTarget[]; - syntax?: 'colon' | 'wiki' | 'both'; - unresolvedBehavior?: 'text' | 'warn' | 'error'; - placeholderClass?: string; -}): RemarkPlugin; - -function checkLinks(config: { - contentDir: string; - targets: LinkTarget[]; -}): { - broken: { file: string; line: number; id: string }[]; - placeholders: { file: string; line: number; id: string }[]; -}; +urlBuilder: (id) => { + const item = catalog[id]; + if (!item) { + console.warn(`Broken magic link: [[${id}]]`); + return `/404/`; + } + return `/${item.type}/${item.slug}/`; +} ``` -## Resolution Priority +Now `[[one-fish]]` → `/fish/one-fish/` and `[[blue-gecko]]` → `/reptile/blue-gecko/`. -1. Exact `id` match -2. Match in `aliases` array -3. Slug fallback -4. Unresolved → plain text (graceful degradation) +See [`remark-magic-links.test.ts`](./src/remark-magic-links.test.ts) for more examples. -## Usage +## Editor Support -```javascript -// astro.config.mjs -import { remarkMagicLinks } from 'atlas'; +The `[[wiki-link]]` syntax is a widely-adopted convention. Many editors can follow these links natively: -const targets = concepts.map(c => ({ - id: c.data.id || c.slug, - slug: c.slug, - url: `/concepts/${c.slug}/`, - aliases: c.data.aliases, - placeholder: c.data.placeholder, -})); +- **Neovim**: `Ctrl+]` jumps to the linked file +- **VS Code**: Extensions like "Markdown Links" add support -export default { - markdown: { - remarkPlugins: [[remarkMagicLinks, { targets }]], - }, -}; -``` +Your content stays navigable in your editor without any build step. + +## Prior Art + +The `[[wiki-link]]` syntax appears in many tools: + +- **MediaWiki** — Wikipedia's engine uses `[[page]]` syntax +- **Org mode** — Emacs system with `[[link][desc]]` and bidirectional links +- **Roam/Obsidian** — Note-taking apps that use wiki-links + +This plugin brings the same convention to static site generators. + +## How It Works + +1. You write `[[some-page]]` in markdown +2. Plugin finds the syntax in text nodes (not code blocks) +3. Calls your `urlBuilder(id)` to get the URL +4. Transforms to a standard markdown link diff --git a/packages/atlas/package.json b/packages/atlas/package.json new file mode 100644 index 0000000..4cb4db3 --- /dev/null +++ b/packages/atlas/package.json @@ -0,0 +1,56 @@ +{ + "name": "@sailkit/atlas", + "version": "0.1.0", + "description": "Wikipedia-style magic links with build-time resolution", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./remark": { + "import": "./dist/remark-magic-links.js", + "types": "./dist/remark-magic-links.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "unist-util-visit": "^5.0.0" + }, + "devDependencies": { + "@types/mdast": "^4.0.0", + "@types/node": "^20.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "typescript": "^5.3.0", + "unified": "^11.0.0", + "vitest": "^2.0.0" + }, + "peerDependencies": { + "unified": ">=10.0.0" + }, + "peerDependenciesMeta": { + "unified": { + "optional": true + } + }, + "keywords": [ + "remark", + "mdast", + "magic-links", + "wiki-links", + "markdown", + "astro" + ], + "license": "MIT" +} diff --git a/packages/atlas/src/index.ts b/packages/atlas/src/index.ts new file mode 100644 index 0000000..2065a43 --- /dev/null +++ b/packages/atlas/src/index.ts @@ -0,0 +1,2 @@ +export type { LinkSyntax, RemarkMagicLinksConfig } from './remark-magic-links.js'; +export { remarkMagicLinks, default as remarkMagicLinksDefault } from './remark-magic-links.js'; diff --git a/packages/atlas/src/remark-magic-links.test.ts b/packages/atlas/src/remark-magic-links.test.ts new file mode 100644 index 0000000..fcf6005 --- /dev/null +++ b/packages/atlas/src/remark-magic-links.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect } from 'vitest'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkStringify from 'remark-stringify'; +import { remarkMagicLinks } from './remark-magic-links.js'; +import { nestCode } from './test-utils/nest-code.js'; + +const urlBuilder = (id: string) => `/concepts/${id}/`; + +async function process(markdown: string, config: Partial[0]> = {}) { + const result = await unified() + .use(remarkParse) + .use(remarkMagicLinks, { urlBuilder, ...config }) + .use(remarkStringify) + .process(markdown); + return String(result); +} + +describe('remarkMagicLinks', () => { + describe('wiki syntax (default)', () => { + it('transforms [[id]] to links', async () => { + const result = await process('Check out [[context-collapse]] for more info.'); + expect(result).toContain('[context-collapse](/concepts/context-collapse/)'); + }); + + it('supports custom display text [[id|text]]', async () => { + const result = await process('Learn about [[hallucination|AI hallucinations]].'); + expect(result).toContain('[AI hallucinations](/concepts/hallucination/)'); + }); + + it('handles multiple links in same paragraph', async () => { + const result = await process('See [[foo]] and [[bar]] for details.'); + expect(result).toContain('[foo](/concepts/foo/)'); + expect(result).toContain('[bar](/concepts/bar/)'); + }); + }); + + describe('colon syntax', () => { + it('transforms [:id] to links', async () => { + const result = await process('Check out [:context-collapse] for more info.', { syntax: 'colon' }); + expect(result).toContain('[context-collapse](/concepts/context-collapse/)'); + }); + + it('supports custom display text [:id|text]', async () => { + const result = await process('Learn about [:hallucination|AI hallucinations].', { syntax: 'colon' }); + expect(result).toContain('[AI hallucinations](/concepts/hallucination/)'); + }); + }); + + describe('both syntax', () => { + it('handles mixed syntax in same document', async () => { + const result = await process( + 'First [:foo] and then [[bar]].', + { syntax: 'both' } + ); + expect(result).toContain('[foo](/concepts/foo/)'); + expect(result).toContain('[bar](/concepts/bar/)'); + }); + }); + + describe('edge cases', () => { + it('handles link at start of text', async () => { + const result = await process('[[foo]] is important.'); + expect(result).toContain('[foo](/concepts/foo/)'); + }); + + it('handles link at end of text', async () => { + const result = await process('Learn about [[foo]]'); + expect(result).toContain('[foo](/concepts/foo/)'); + }); + + it('preserves surrounding text', async () => { + const result = await process('Before [[foo]] after.'); + expect(result).toContain('Before'); + expect(result).toContain('after'); + }); + + it('handles text with no magic links', async () => { + const result = await process('Just regular text here.'); + expect(result).toContain('Just regular text here.'); + }); + + it('does not match inside code blocks', async () => { + const result = await process('```\n[[not-a-link]]\n```'); + expect(result).toContain('[[not-a-link]]'); + expect(result).not.toContain('/concepts/not-a-link/'); + }); + + it('does not match inside inline code', async () => { + const result = await process('Use `[[syntax]]` for links.'); + expect(result).toContain('`[[syntax]]`'); + }); + + it.each([1, 2, 3, 4, 5])('code block at %i nesting levels', async (levels) => { + const input = nestCode(levels, false); + const result = await process(input); + expect(result).toContain('[[not-a-link]]'); + expect(result).not.toContain('/concepts/not-a-link/'); + }); + + it.each([1, 2, 3, 4, 5])('inline code at %i nesting levels', async (levels) => { + const input = nestCode(levels, true); + const result = await process(input); + expect(result).toContain('[[not-a-link]]'); + expect(result).not.toContain('/concepts/not-a-link/'); + }); + }); + + describe('urlBuilder callback', () => { + it('uses custom urlBuilder', async () => { + const customBuilder = (id: string) => `/custom/${id}.html`; + const result = await unified() + .use(remarkParse) + .use(remarkMagicLinks, { urlBuilder: customBuilder }) + .use(remarkStringify) + .process('See [[my-page]] here.'); + expect(String(result)).toContain('[my-page](/custom/my-page.html)'); + }); + }); +}); diff --git a/packages/atlas/src/remark-magic-links.ts b/packages/atlas/src/remark-magic-links.ts new file mode 100644 index 0000000..c914046 --- /dev/null +++ b/packages/atlas/src/remark-magic-links.ts @@ -0,0 +1,116 @@ +import type { Root, Text, Link, Parent } from 'mdast'; +import { visit } from 'unist-util-visit'; + +export type LinkSyntax = 'colon' | 'wiki' | 'both'; + +export interface RemarkMagicLinksConfig { + /** Build URL from link ID */ + urlBuilder: (id: string) => string; + /** Syntax style to parse (default: 'wiki') */ + syntax?: LinkSyntax; +} + +/** Minimal plugin type compatible with unified */ +type RemarkPlugin = (config: RemarkMagicLinksConfig) => (tree: Root) => void; + +// Regex patterns for magic link syntax +// Colon syntax: [:id] or [:id|Display Text] +const COLON_LINK_PATTERN = /\[:([^\]|]+)(?:\|([^\]]+))?\]/g; +// Wiki syntax: [[id]] or [[id|Display Text]] +const WIKI_LINK_PATTERN = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g; + +interface ParsedLink { + fullMatch: string; + id: string; + displayText?: string; + startIndex: number; +} + +function parseMagicLinks(text: string, syntax: LinkSyntax): ParsedLink[] { + const links: ParsedLink[] = []; + + if (syntax === 'colon' || syntax === 'both') { + let match: RegExpExecArray | null; + COLON_LINK_PATTERN.lastIndex = 0; + while ((match = COLON_LINK_PATTERN.exec(text)) !== null) { + links.push({ + fullMatch: match[0], + id: match[1].trim(), + displayText: match[2]?.trim(), + startIndex: match.index, + }); + } + } + + if (syntax === 'wiki' || syntax === 'both') { + let match: RegExpExecArray | null; + WIKI_LINK_PATTERN.lastIndex = 0; + while ((match = WIKI_LINK_PATTERN.exec(text)) !== null) { + links.push({ + fullMatch: match[0], + id: match[1].trim(), + displayText: match[2]?.trim(), + startIndex: match.index, + }); + } + } + + return links.sort((a, b) => a.startIndex - b.startIndex); +} + +/** + * Remark plugin that transforms magic link syntax into actual links. + * + * Supports two syntax styles: + * - Colon: `[:id]` or `[:id|Display Text]` + * - Wiki: `[[id]]` or `[[id|Display Text]]` + */ +export const remarkMagicLinks: RemarkPlugin = (config) => { + const { urlBuilder, syntax = 'wiki' } = config; + + return (tree: Root) => { + visit(tree, 'text', (node: Text, index: number | undefined, parent: Parent | undefined) => { + if (!parent || index === undefined) return; + + const magicLinks = parseMagicLinks(node.value, syntax); + if (magicLinks.length === 0) return; + + const newNodes: (Text | Link)[] = []; + let lastIndex = 0; + + for (const link of magicLinks) { + // Text before this link + if (link.startIndex > lastIndex) { + newNodes.push({ + type: 'text', + value: node.value.slice(lastIndex, link.startIndex), + }); + } + + // Create link node + const displayText = link.displayText || link.id; + const url = urlBuilder(link.id); + + newNodes.push({ + type: 'link', + url, + children: [{ type: 'text', value: displayText }], + }); + + lastIndex = link.startIndex + link.fullMatch.length; + } + + // Remaining text after last link + if (lastIndex < node.value.length) { + newNodes.push({ + type: 'text', + value: node.value.slice(lastIndex), + }); + } + + (parent.children as (Text | Link)[]).splice(index, 1, ...newNodes); + }); + }; +}; + +export default remarkMagicLinks; diff --git a/packages/atlas/src/test-utils/nest-code.test.ts b/packages/atlas/src/test-utils/nest-code.test.ts new file mode 100644 index 0000000..9533c0a --- /dev/null +++ b/packages/atlas/src/test-utils/nest-code.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { nestCode } from './nest-code.js'; + +describe('nestCode', () => { + describe('inline code', () => { + it('level 0 returns bare inline code', () => { + expect(nestCode(0, true)).toBe('`[[not-a-link]]`'); + }); + + it('level 1 wraps in list', () => { + expect(nestCode(1, true)).toBe('- `[[not-a-link]]`'); + }); + + it('level 2 wraps in list then blockquote', () => { + expect(nestCode(2, true)).toBe('> - `[[not-a-link]]`'); + }); + + it('level 3 alternates list/blockquote/list', () => { + expect(nestCode(3, true)).toBe('- > - `[[not-a-link]]`'); + }); + }); + + describe('code block', () => { + it('level 0 returns bare code block', () => { + expect(nestCode(0, false)).toBe('```\n[[not-a-link]]\n```'); + }); + + it('level 1 wraps in list with indentation', () => { + expect(nestCode(1, false)).toBe('- ```\n [[not-a-link]]\n ```'); + }); + + it('level 2 wraps in list then blockquote', () => { + expect(nestCode(2, false)).toBe('> - ```\n> [[not-a-link]]\n> ```'); + }); + }); + + describe('nesting pattern', () => { + it('even levels start with list', () => { + // Level 0: bare, Level 2: blockquote > list, Level 4: blockquote > list > blockquote > list + const result = nestCode(4, true); + expect(result.startsWith('> - > - ')).toBe(true); + }); + + it('odd levels start with list (outermost)', () => { + const result = nestCode(3, true); + expect(result.startsWith('- > - ')).toBe(true); + }); + }); +}); diff --git a/packages/atlas/src/test-utils/nest-code.ts b/packages/atlas/src/test-utils/nest-code.ts new file mode 100644 index 0000000..8283ba8 --- /dev/null +++ b/packages/atlas/src/test-utils/nest-code.ts @@ -0,0 +1,19 @@ +/** + * Generates markdown with N levels of nesting around code content. + * Alternates between list items and blockquotes for variety. + */ +export function nestCode(levels: number, useInlineCode: boolean): string { + const codeContent = useInlineCode + ? '`[[not-a-link]]`' + : '```\n[[not-a-link]]\n```'; + + let result = codeContent; + for (let i = 0; i < levels; i++) { + if (i % 2 === 0) { + result = `- ${result.split('\n').join('\n ')}`; // list + } else { + result = result.split('\n').map(line => `> ${line}`).join('\n'); // blockquote + } + } + return result; +} diff --git a/packages/atlas/tsconfig.json b/packages/atlas/tsconfig.json new file mode 100644 index 0000000..bf8aaf8 --- /dev/null +++ b/packages/atlas/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/atlas/vitest.config.ts b/packages/atlas/vitest.config.ts new file mode 100644 index 0000000..6ec74ee --- /dev/null +++ b/packages/atlas/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + }, +}); diff --git a/packages/compass/package.json b/packages/compass/package.json index aa3f38c..e7fe2aa 100644 --- a/packages/compass/package.json +++ b/packages/compass/package.json @@ -1,5 +1,5 @@ { - "name": "@bearing-dev/compass", + "name": "@sailkit/compass", "version": "0.1.0", "description": "Headless navigation state for lists and trees", "type": "module", diff --git a/packages/kbd-hints/KbdBadge.astro b/packages/kbd-hints/KbdBadge.astro new file mode 100644 index 0000000..d0f57e2 --- /dev/null +++ b/packages/kbd-hints/KbdBadge.astro @@ -0,0 +1,30 @@ +--- +/** + * KbdBadge - Render keyboard shortcut badges + * + * @example + * + * + */ +import { formatKey } from './src/bindings.js'; + +interface Props { + /** Keys to display, e.g. ['j', 'k'] or ['Ctrl+d'] */ + keys: string[]; + /** Optional label after the keys */ + label?: string; + /** Whether to render (default: true) */ + enabled?: boolean; + /** Additional CSS class */ + class?: string; +} + +const { keys, label, enabled = true, class: className } = Astro.props; + +if (!enabled) return null; +--- + + + {keys.map(key => {formatKey(key)})} + {label && {label}} + diff --git a/packages/kbd-hints/KbdLegend.astro b/packages/kbd-hints/KbdLegend.astro new file mode 100644 index 0000000..68a2a70 --- /dev/null +++ b/packages/kbd-hints/KbdLegend.astro @@ -0,0 +1,49 @@ +--- +/** + * KbdLegend - Render grouped keyboard shortcut legend + * + * @example + * import { DEFAULT_BINDINGS } from '@sailkit/teleport'; + * + */ +import { getBindingGroups, formatKey } from './src/bindings.js'; +import type { TeleportBindings } from './src/types.js'; + +interface Props { + /** Teleport bindings object */ + bindings: TeleportBindings; + /** Whether to render (default: true) */ + enabled?: boolean; + /** Layout: 'compact' for header bar, 'full' for sidebar */ + layout?: 'compact' | 'full'; + /** Additional CSS class */ + class?: string; +} + +const { bindings, enabled = true, layout = 'full', class: className } = Astro.props; + +if (!enabled) return null; + +const groups = getBindingGroups(bindings); +--- + +{layout === 'compact' ? ( +
+ {groups.flatMap(g => g.shortcuts).map(s => ( + + {s.keys.map(k => {formatKey(k)})} + {s.label} + + ))} +
+) : ( +
+
Keyboard
+ {groups.flatMap(g => g.shortcuts).map(s => ( +
+ {s.keys.map(k => {formatKey(k)})} + {s.label} +
+ ))} +
+)} diff --git a/packages/kbd-hints/README.md b/packages/kbd-hints/README.md new file mode 100644 index 0000000..6cb8bf2 --- /dev/null +++ b/packages/kbd-hints/README.md @@ -0,0 +1,78 @@ +# @sailkit/kbd-hints + +Keyboard shortcut visibility components. Renders `` badges and legends from teleport bindings. + +## Usage + +```astro +--- +import { DEFAULT_BINDINGS } from '@sailkit/teleport'; +import KbdLegend from '@sailkit/kbd-hints/KbdLegend.astro'; +import KbdBadge from '@sailkit/kbd-hints/KbdBadge.astro'; +--- + + + + + + + + + +``` + +## Props + +### KbdLegend + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `bindings` | `TeleportBindings` | required | Teleport bindings object | +| `enabled` | `boolean` | `true` | Whether to render | +| `layout` | `'compact' \| 'full'` | `'full'` | Layout style | +| `class` | `string` | - | Additional CSS class | + +### KbdBadge + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `keys` | `string[]` | required | Keys to display | +| `label` | `string` | - | Label after keys | +| `enabled` | `boolean` | `true` | Whether to render | +| `class` | `string` | - | Additional CSS class | + +## Styling + +Components apply CSS classes; sites define styles: + +```css +.kbd-hints-legend { /* container */ } +.kbd-hints-legend--compact { /* header bar variant */ } +.kbd-hints-title { /* "Keyboard" heading */ } +.kbd-hints-row { /* single shortcut row */ } +.kbd-hints-badge { /* inline badge container */ } +.kbd-hints-group { /* compact group */ } +.kbd-hints-label { /* label text */ } +kbd { /* key badge */ } +``` + +## Visibility + +Control visibility with the `enabled` prop: + +```astro + + + + + + + + +``` + +Detection strategy (when `enabled="auto"`): +1. Desktop breakpoint → show by default +2. Mobile breakpoint → hide by default +3. Touch event → hide (unless localStorage flag set) +4. Keydown event → show + persist to localStorage permanently diff --git a/packages/kbd-hints/package.json b/packages/kbd-hints/package.json new file mode 100644 index 0000000..b6de0a8 --- /dev/null +++ b/packages/kbd-hints/package.json @@ -0,0 +1,28 @@ +{ + "name": "@sailkit/kbd-hints", + "version": "0.1.0", + "description": "Keyboard shortcut visibility components", + "type": "module", + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./KbdBadge.astro": "./KbdBadge.astro", + "./KbdLegend.astro": "./KbdLegend.astro" + }, + "files": ["src", "*.astro"], + "scripts": { + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "astro": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "astro": { "optional": true } + }, + "devDependencies": { + "jsdom": "^26.0.0", + "typescript": "^5.0.0", + "vitest": "^3.0.0" + } +} diff --git a/packages/kbd-hints/src/bindings.ts b/packages/kbd-hints/src/bindings.ts new file mode 100644 index 0000000..88fd50d --- /dev/null +++ b/packages/kbd-hints/src/bindings.ts @@ -0,0 +1,95 @@ +import type { TeleportBindings, KbdShortcut, KbdGroup } from './types.js'; + +/** Labels for teleport binding actions */ +const ACTION_LABELS: Record = { + down: 'next', + up: 'prev', + left: 'prev page', + right: 'next page', + scrollDown: 'scroll down', + scrollUp: 'scroll up', + select: 'open', + toggleSidebar: 'sidebar', + openFinder: 'search', + goToTop: 'top', + goToBottom: 'bottom', +}; + +/** Group assignments for teleport bindings */ +const ACTION_GROUPS: Record = { + down: 'Navigation', + up: 'Navigation', + left: 'Navigation', + right: 'Navigation', + scrollDown: 'Scrolling', + scrollUp: 'Scrolling', + select: 'Actions', + toggleSidebar: 'Actions', + openFinder: 'Actions', + goToTop: 'Jump', + goToBottom: 'Jump', +}; + +/** + * Convert teleport bindings to flat shortcut list + */ +export function getShortcuts(bindings: TeleportBindings): KbdShortcut[] { + const shortcuts: KbdShortcut[] = []; + + for (const [action, keys] of Object.entries(bindings)) { + if (keys && keys.length > 0) { + shortcuts.push({ + keys, + label: ACTION_LABELS[action as keyof TeleportBindings] ?? action, + }); + } + } + + return shortcuts; +} + +/** + * Convert teleport bindings to grouped shortcut list + */ +export function getBindingGroups(bindings: TeleportBindings): KbdGroup[] { + const groupMap = new Map(); + + for (const [action, keys] of Object.entries(bindings)) { + if (!keys || keys.length === 0) continue; + + const groupName = ACTION_GROUPS[action as keyof TeleportBindings] ?? 'Other'; + const shortcut: KbdShortcut = { + keys, + label: ACTION_LABELS[action as keyof TeleportBindings] ?? action, + }; + + if (!groupMap.has(groupName)) { + groupMap.set(groupName, []); + } + groupMap.get(groupName)!.push(shortcut); + } + + // Return in consistent order + const groupOrder = ['Navigation', 'Scrolling', 'Jump', 'Actions', 'Other']; + return groupOrder + .filter(name => groupMap.has(name)) + .map(name => ({ name, shortcuts: groupMap.get(name)! })); +} + +/** + * Format a key for display (e.g., 'Ctrl+d' -> 'Ctrl+D', 'gg' -> 'gg') + */ +export function formatKey(key: string): string { + // Sequences like 'gg' stay lowercase + if (key.length > 1 && !key.includes('+')) { + return key; + } + // Modifier combos: capitalize the final key + if (key.includes('+')) { + const parts = key.split('+'); + const lastKey = parts.pop()!; + return [...parts, lastKey.toUpperCase()].join('+'); + } + // Single keys stay as-is + return key; +} diff --git a/packages/kbd-hints/src/detection.test.ts b/packages/kbd-hints/src/detection.test.ts new file mode 100644 index 0000000..c0204ab --- /dev/null +++ b/packages/kbd-hints/src/detection.test.ts @@ -0,0 +1,215 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { createKeyboardDetector, type KeyboardDetector } from './detection.js'; + +describe('KeyboardDetector', () => { + let detector: KeyboardDetector; + let mockMatchMedia: ReturnType; + let mockLocalStorage: Record; + + beforeEach(() => { + // Reset mocks + mockLocalStorage = {}; + vi.stubGlobal('localStorage', { + getItem: (key: string) => mockLocalStorage[key] ?? null, + setItem: (key: string, value: string) => { mockLocalStorage[key] = value; }, + removeItem: (key: string) => { delete mockLocalStorage[key]; }, + }); + + // Default: desktop breakpoint + mockMatchMedia = vi.fn().mockReturnValue({ matches: true }); + vi.stubGlobal('matchMedia', mockMatchMedia); + }); + + describe('breakpoint defaults', () => { + it('shows hints on desktop breakpoint', () => { + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector(); + expect(detector.shouldShow()).toBe(true); + }); + + it('hides hints on mobile breakpoint', () => { + mockMatchMedia.mockReturnValue({ matches: false }); // mobile + detector = createKeyboardDetector(); + expect(detector.shouldShow()).toBe(false); + }); + }); + + describe('touch interaction', () => { + it('hides hints after touchstart on desktop', () => { + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector(); + expect(detector.shouldShow()).toBe(true); + + // Simulate touch event + detector.handleTouch(); + expect(detector.shouldShow()).toBe(false); + }); + + it('does not hide if localStorage flag set', () => { + mockLocalStorage['kbd:hasKeyboard'] = 'true'; + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector(); + + // Touch should not hide when localStorage flag is set + detector.handleTouch(); + expect(detector.shouldShow()).toBe(true); + }); + }); + + describe('keyboard interaction', () => { + it('shows hints after keydown on mobile', () => { + mockMatchMedia.mockReturnValue({ matches: false }); // mobile + detector = createKeyboardDetector(); + expect(detector.shouldShow()).toBe(false); + + // Simulate keydown + detector.handleKeydown(); + expect(detector.shouldShow()).toBe(true); + }); + + it('sets localStorage flag on keydown', () => { + mockMatchMedia.mockReturnValue({ matches: false }); // mobile + detector = createKeyboardDetector(); + + detector.handleKeydown(); + expect(mockLocalStorage['kbd:hasKeyboard']).toBe('true'); + }); + + it('respects localStorage flag on subsequent visits', () => { + mockLocalStorage['kbd:hasKeyboard'] = 'true'; + mockMatchMedia.mockReturnValue({ matches: false }); // mobile + + detector = createKeyboardDetector(); + // Should show despite mobile breakpoint + expect(detector.shouldShow()).toBe(true); + }); + }); + + describe('consumer overrides', () => { + it('always shows when enabled="always"', () => { + mockMatchMedia.mockReturnValue({ matches: false }); // mobile + detector = createKeyboardDetector({ enabled: 'always' }); + + expect(detector.shouldShow()).toBe(true); + + // Even after touch + detector.handleTouch(); + expect(detector.shouldShow()).toBe(true); + }); + + it('always hides when enabled="never"', () => { + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + mockLocalStorage['kbd:hasKeyboard'] = 'true'; + detector = createKeyboardDetector({ enabled: 'never' }); + + expect(detector.shouldShow()).toBe(false); + + // Even after keydown + detector.handleKeydown(); + expect(detector.shouldShow()).toBe(false); + }); + + it('uses detection when enabled="auto"', () => { + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector({ enabled: 'auto' }); + + expect(detector.shouldShow()).toBe(true); + + detector.handleTouch(); + expect(detector.shouldShow()).toBe(false); + }); + + it('treats true as "always"', () => { + mockMatchMedia.mockReturnValue({ matches: false }); // mobile + detector = createKeyboardDetector({ enabled: true }); + expect(detector.shouldShow()).toBe(true); + }); + + it('treats false as "never"', () => { + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector({ enabled: false }); + expect(detector.shouldShow()).toBe(false); + }); + }); + + describe('precedence', () => { + it('localStorage trumps touch events', () => { + mockLocalStorage['kbd:hasKeyboard'] = 'true'; + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector(); + + // localStorage flag should prevent touch from hiding + detector.handleTouch(); + expect(detector.shouldShow()).toBe(true); + }); + + it('keydown sets permanent flag', () => { + mockMatchMedia.mockReturnValue({ matches: false }); // mobile + detector = createKeyboardDetector(); + + detector.handleKeydown(); + expect(mockLocalStorage['kbd:hasKeyboard']).toBe('true'); + + // Create new detector to simulate page reload + detector = createKeyboardDetector(); + expect(detector.shouldShow()).toBe(true); + }); + + it('touch hides unless permanent flag set', () => { + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector(); + + // No localStorage flag, touch should hide + detector.handleTouch(); + expect(detector.shouldShow()).toBe(false); + + // Keydown sets flag + detector.handleKeydown(); + expect(detector.shouldShow()).toBe(true); + + // Touch no longer hides + detector.handleTouch(); + expect(detector.shouldShow()).toBe(true); + }); + }); + + describe('subscription', () => { + it('notifies subscribers when visibility changes', () => { + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector(); + + const callback = vi.fn(); + detector.subscribe(callback); + + detector.handleTouch(); + expect(callback).toHaveBeenCalledWith(false); + + detector.handleKeydown(); + expect(callback).toHaveBeenCalledWith(true); + }); + + it('allows unsubscribing', () => { + mockMatchMedia.mockReturnValue({ matches: true }); // desktop + detector = createKeyboardDetector(); + + const callback = vi.fn(); + const unsubscribe = detector.subscribe(callback); + + detector.handleTouch(); + expect(callback).toHaveBeenCalledTimes(1); + + unsubscribe(); + detector.handleKeydown(); + expect(callback).toHaveBeenCalledTimes(1); // no additional call + }); + }); + + describe('cleanup', () => { + it('provides destroy method to remove event listeners', () => { + detector = createKeyboardDetector(); + expect(typeof detector.destroy).toBe('function'); + // Should not throw + detector.destroy(); + }); + }); +}); diff --git a/packages/kbd-hints/src/detection.ts b/packages/kbd-hints/src/detection.ts new file mode 100644 index 0000000..4ca9bec --- /dev/null +++ b/packages/kbd-hints/src/detection.ts @@ -0,0 +1,130 @@ +/** + * Keyboard detection module + * + * Determines whether to show keyboard hints based on: + * 1. localStorage flag (permanent, set on keydown) + * 2. Consumer override (always/never/auto) + * 3. Touch events (hide unless localStorage flag) + * 4. Breakpoint default (desktop=show, mobile=hide) + */ + +export type EnabledProp = 'auto' | 'always' | 'never' | boolean; + +export interface KeyboardDetectorConfig { + /** Control visibility: 'auto' (detect), 'always', 'never', or boolean */ + enabled?: EnabledProp; + /** Desktop breakpoint media query (default: '(min-width: 769px)') */ + desktopQuery?: string; +} + +export interface KeyboardDetector { + /** Returns whether hints should be shown */ + shouldShow(): boolean; + /** Handle touch event (hides hints unless localStorage flag) */ + handleTouch(): void; + /** Handle keydown event (shows hints + persists to localStorage) */ + handleKeydown(): void; + /** Subscribe to visibility changes */ + subscribe(callback: (visible: boolean) => void): () => void; + /** Cleanup event listeners */ + destroy(): void; +} + +const STORAGE_KEY = 'kbd:hasKeyboard'; + +export function createKeyboardDetector( + config: KeyboardDetectorConfig = {} +): KeyboardDetector { + const { enabled = 'auto', desktopQuery = '(min-width: 769px)' } = config; + + // Normalize enabled prop + const mode = enabled === true ? 'always' : enabled === false ? 'never' : enabled; + + // State + let touchHidden = false; + const subscribers = new Set<(visible: boolean) => void>(); + + // Check localStorage for permanent flag + function hasKeyboardFlag(): boolean { + try { + return localStorage.getItem(STORAGE_KEY) === 'true'; + } catch { + return false; + } + } + + // Set localStorage flag + function setKeyboardFlag(): void { + try { + localStorage.setItem(STORAGE_KEY, 'true'); + } catch { + // Ignore storage errors + } + } + + // Check if desktop breakpoint matches + function isDesktop(): boolean { + return matchMedia(desktopQuery).matches; + } + + // Notify subscribers of visibility change + function notify(visible: boolean): void { + subscribers.forEach(cb => cb(visible)); + } + + function shouldShow(): boolean { + // Consumer overrides + if (mode === 'always') return true; + if (mode === 'never') return false; + + // localStorage flag trumps all detection + if (hasKeyboardFlag()) return true; + + // Touch hidden (unless localStorage flag set) + if (touchHidden) return false; + + // Breakpoint default + return isDesktop(); + } + + function handleTouch(): void { + // If localStorage flag is set, touch doesn't hide + if (hasKeyboardFlag()) return; + + const wasVisible = shouldShow(); + touchHidden = true; + const nowVisible = shouldShow(); + + if (wasVisible !== nowVisible) { + notify(nowVisible); + } + } + + function handleKeydown(): void { + const wasVisible = shouldShow(); + setKeyboardFlag(); + touchHidden = false; // Reset touch hidden state + const nowVisible = shouldShow(); + + if (wasVisible !== nowVisible) { + notify(nowVisible); + } + } + + function subscribe(callback: (visible: boolean) => void): () => void { + subscribers.add(callback); + return () => subscribers.delete(callback); + } + + function destroy(): void { + subscribers.clear(); + } + + return { + shouldShow, + handleTouch, + handleKeydown, + subscribe, + destroy, + }; +} diff --git a/packages/kbd-hints/src/index.ts b/packages/kbd-hints/src/index.ts new file mode 100644 index 0000000..5383002 --- /dev/null +++ b/packages/kbd-hints/src/index.ts @@ -0,0 +1,2 @@ +export type { KbdShortcut, KbdGroup, TeleportBindings } from './types.js'; +export { getShortcuts, getBindingGroups, formatKey } from './bindings.js'; diff --git a/packages/kbd-hints/src/types.ts b/packages/kbd-hints/src/types.ts new file mode 100644 index 0000000..497f8a6 --- /dev/null +++ b/packages/kbd-hints/src/types.ts @@ -0,0 +1,35 @@ +/** + * A single keyboard shortcut for display + */ +export interface KbdShortcut { + /** Display keys, e.g. ['j', 'k'] or ['Ctrl+d'] */ + keys: string[]; + /** Human label, e.g. 'navigate' or 'scroll down' */ + label: string; +} + +/** + * A group of related shortcuts + */ +export interface KbdGroup { + /** Group name, e.g. 'Navigation' or 'Scrolling' */ + name: string; + shortcuts: KbdShortcut[]; +} + +/** + * Teleport's KeyBindings shape (re-declared to avoid hard dep) + */ +export interface TeleportBindings { + down?: string[]; + up?: string[]; + left?: string[]; + right?: string[]; + scrollDown?: string[]; + scrollUp?: string[]; + select?: string[]; + toggleSidebar?: string[]; + openFinder?: string[]; + goToTop?: string[]; + goToBottom?: string[]; +} diff --git a/packages/kbd-hints/tsconfig.json b/packages/kbd-hints/tsconfig.json new file mode 100644 index 0000000..d756e80 --- /dev/null +++ b/packages/kbd-hints/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true, + "esModuleInterop": true, + "outDir": "./dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/kbd-hints/vitest.config.ts b/packages/kbd-hints/vitest.config.ts new file mode 100644 index 0000000..647a9e5 --- /dev/null +++ b/packages/kbd-hints/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + }, +}); diff --git a/packages/lantern/package.json b/packages/lantern/package.json index 1c3152f..a56f75b 100644 --- a/packages/lantern/package.json +++ b/packages/lantern/package.json @@ -1,5 +1,5 @@ { - "name": "@bearing-dev/lantern", + "name": "@sailkit/lantern", "version": "0.1.0", "description": "Theme toggle with flash-free hydration", "type": "module", diff --git a/packages/teleport/package.json b/packages/teleport/package.json index a2fff6c..6cb4105 100644 --- a/packages/teleport/package.json +++ b/packages/teleport/package.json @@ -1,5 +1,5 @@ { - "name": "@bearing-dev/teleport", + "name": "@sailkit/teleport", "version": "0.0.1", "description": "Vim-style keyboard navigation bindings with DOM integration", "type": "module", @@ -25,7 +25,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@bearing-dev/compass": ">=0.0.1" + "@sailkit/compass": ">=0.0.1" }, "devDependencies": { "@playwright/test": "^1.57.0", diff --git a/packages/teleport/tsup.config.ts b/packages/teleport/tsup.config.ts index 345cb0e..fca124b 100644 --- a/packages/teleport/tsup.config.ts +++ b/packages/teleport/tsup.config.ts @@ -5,7 +5,6 @@ export default defineConfig({ format: ['esm'], dts: true, clean: true, - noExternal: ['@bearing-dev/compass'], target: 'es2020', minify: false, sourcemap: true,