diff --git a/.prettierignore b/.prettierignore index 29fb0830..9a6f80e2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,8 @@ public/data/*.json # Prettier's MDX v1 parser corrupts code fences nested in JSX components *.mdx +# Markdown stays hand-formatted +*.md # Lockfiles pixi.lock pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc index 47357bcf..43b148b1 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,3 @@ { - "plugins": ["prettier-plugin-astro"] + "plugins": ["prettier-plugin-astro", "prettier-plugin-svelte"] } diff --git a/astro.config.mjs b/astro.config.mjs index 40ef85a3..457aacfa 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,6 +1,7 @@ // @ts-check import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; +import svelte from "@astrojs/svelte"; export default defineConfig({ site: "https://robostack.github.io", @@ -29,6 +30,7 @@ export default defineConfig({ "https://github.com/RoboStack/robostack.github.io/edit/master/", }, customCss: ["./src/styles/custom.css"], + routeMiddleware: "./src/routeData.ts", components: { SiteTitle: "./src/components/SiteTitle.astro", PageTitle: "./src/components/PageTitle.astro", @@ -51,5 +53,6 @@ export default defineConfig({ { label: "FAQ", slug: "FAQ" }, ], }), + svelte(), ], }); diff --git a/package.json b/package.json index 5bf43efc..7965aaa4 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,16 @@ }, "dependencies": { "@astrojs/starlight": "^0.41.7", - "astro": "^7.2.0" + "@astrojs/svelte": "^9.0.1", + "astro": "^7.2.0", + "svelte": "^5.56.8" }, "devDependencies": { "@astrojs/check": "^0.9.10", "prettier": "^3.9.6", "prettier-plugin-astro": "^0.14.1", + "prettier-plugin-svelte": "^4.1.1", + "svelte-check": "^4.7.5", "typescript": "^6.0.3" } } diff --git a/pixi.toml b/pixi.toml index cc0a0cb6..203c5588 100644 --- a/pixi.toml +++ b/pixi.toml @@ -42,6 +42,11 @@ astro-check = { depends-on = ["pnpm-install"], description = "Type-check the site", } +svelte-check = { + cmd = "pnpm exec svelte-check --tsconfig ./tsconfig.json --fail-on-warnings", + depends-on = ["pnpm-install"], + description = "Type-check the Svelte components", +} [feature.lint.dependencies] ruff = ">=0.16,<0.17" @@ -71,7 +76,7 @@ ty-check = { description = "Type-check the scripts in scripts/", } lint = { - depends-on = ["ruff-check", "format-check", "typos", "zizmor", "ty-check", "prettier-check", "astro-check"], + depends-on = ["ruff-check", "format-check", "typos", "zizmor", "ty-check", "prettier-check", "astro-check", "svelte-check"], description = "Run all lint checks", } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e7c34c8..f8363475 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,15 @@ importers: '@astrojs/starlight': specifier: ^0.41.7 version: 0.41.7(@astrojs/markdown-remark@7.2.2)(astro@7.2.0(@astrojs/markdown-remark@7.2.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(yaml@2.9.0))(typescript@6.0.3) + '@astrojs/svelte': + specifier: ^9.0.1 + version: 9.0.1(@types/node@24.13.3)(astro@7.2.0(@astrojs/markdown-remark@7.2.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(yaml@2.9.0))(esbuild@0.28.1)(svelte@5.56.8)(typescript@6.0.3)(yaml@2.9.0) astro: specifier: ^7.2.0 version: 7.2.0(@astrojs/markdown-remark@7.2.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(yaml@2.9.0) + svelte: + specifier: ^5.56.8 + version: 5.56.8 devDependencies: '@astrojs/check': specifier: ^0.9.10 @@ -24,6 +30,12 @@ importers: prettier-plugin-astro: specifier: ^0.14.1 version: 0.14.1 + prettier-plugin-svelte: + specifier: ^4.1.1 + version: 4.1.1(prettier@3.9.6)(svelte@5.56.8) + svelte-check: + specifier: ^4.7.5 + version: 4.7.5(picomatch@4.0.5)(svelte@5.56.8)(typescript@6.0.3) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -151,6 +163,14 @@ packages: '@astrojs/markdown-remark': optional: true + '@astrojs/svelte@9.0.1': + resolution: {integrity: sha512-n5FzIR9Eqs4Kz8O0BIyGfV+wJuAbXRhZazJbq+sdU64Cho83my9/yFUiQyUeGFPnu3nytCg7Dj24LMDsCh31Jg==} + engines: {node: '>=22.12.0'} + peerDependencies: + astro: ^7.0.0 + svelte: ^5.43.6 + typescript: ^5.3.3 || ^6.0.0 + '@astrojs/telemetry@3.3.3': resolution: {integrity: sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} @@ -603,9 +623,22 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -784,6 +817,22 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sveltejs/acorn-typescript@1.0.12': + resolution: {integrity: sha512-J1jNYG23QWd67UfrQSFHtjhV37r9mVi0gdc12A3MWPldOjRK35Xk+um+qACPVjgw3AleiqoyEAhok8Wm3q46NA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/load-config@0.2.2': + resolution: {integrity: sha512-K7dsJDQxBOF+f+epuhMactcjK2VP4MRkLKtwSykNtEI+cKVEyzrmmhQ1pmoxI800m4JKcyJ05L2M4yPwAGiBNw==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/vite-plugin-svelte@7.3.0': + resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -820,6 +869,9 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -918,6 +970,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -1058,6 +1114,13 @@ packages: decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -1139,6 +1202,17 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.3.2: + resolution: {integrity: sha512-40GyiEJevYKXzYTHtZkFqAgTjLOuFcaXMao8TPyOlnWTlkHDlvZ6mPMJaJyOqVwrVCgomEG1WhJd81w0X+IcCw==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + estree-util-attach-comments@3.0.0: resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} @@ -1334,6 +1408,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + js-yaml@4.3.1: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true @@ -1429,6 +1506,9 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -1620,6 +1700,10 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -1734,6 +1818,13 @@ packages: resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==} engines: {node: ^14.15.0 || >=16.0.0} + prettier-plugin-svelte@4.1.1: + resolution: {integrity: sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==} + engines: {node: '>=20'} + peerDependencies: + prettier: ^3.0.0 + svelte: ^5.0.0 + prettier@3.9.6: resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} @@ -1860,6 +1951,10 @@ packages: s.color@0.0.15: resolution: {integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==} + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} @@ -1870,6 +1965,9 @@ packages: resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} engines: {node: '>=11.0.0'} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -1938,6 +2036,24 @@ packages: suf-log@2.5.3: resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} + svelte-check@4.7.5: + resolution: {integrity: sha512-NnkHGCTPH6k4ka1E9IpTuNv40uLArHnX52kLEuaHSGqRlPYTnkbFs529jSWG+y+wDy+v+jA2PQ0soN1umVK+OA==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.0.0 || ^6.0.0 + + svelte2tsx@0.7.59: + resolution: {integrity: sha512-Itj7Wz9WIiGFl/uJa58+rf43ajezaljKK/KTOHq5abUjto56xe+o5SOzLwSoeJ5hma9NZEstpZiazFW2q1nZPQ==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 || ^6.0.0 + + svelte@5.56.8: + resolution: {integrity: sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==} + engines: {node: '>=18'} + svgo@4.0.2: resolution: {integrity: sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==} engines: {node: '>=16'} @@ -2303,6 +2419,9 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -2515,6 +2634,29 @@ snapshots: - supports-color - typescript + '@astrojs/svelte@9.0.1(@types/node@24.13.3)(astro@7.2.0(@astrojs/markdown-remark@7.2.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(yaml@2.9.0))(esbuild@0.28.1)(svelte@5.56.8)(typescript@6.0.3)(yaml@2.9.0)': + dependencies: + '@sveltejs/vite-plugin-svelte': 7.3.0(svelte@5.56.8)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(yaml@2.9.0)) + astro: 7.2.0(@astrojs/markdown-remark@7.2.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(yaml@2.9.0) + svelte: 5.56.8 + svelte2tsx: 0.7.59(svelte@5.56.8)(typescript@6.0.3) + typescript: 6.0.3 + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(yaml@2.9.0)) + transitivePeerDependencies: + - '@types/node' + - '@vitejs/devtools' + - esbuild + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + '@astrojs/telemetry@3.3.3': dependencies: ci-info: 4.4.0 @@ -2842,8 +2984,25 @@ snapshots: '@img/sharp-win32-x64@0.35.3': optional: true + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.9 @@ -2999,6 +3158,21 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sveltejs/acorn-typescript@1.0.12(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/load-config@0.2.2': {} + + '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.8)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(yaml@2.9.0))': + dependencies: + deepmerge: 4.3.1 + magic-string: 1.1.0 + obug: 2.1.4 + svelte: 5.56.8 + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(yaml@2.9.0)) + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -3040,6 +3214,8 @@ snapshots: dependencies: '@types/node': 24.13.3 + '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -3140,6 +3316,8 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.1: {} + aria-query@5.3.2: {} array-iterate@2.0.1: {} @@ -3337,6 +3515,10 @@ snapshots: dependencies: character-entities: 2.0.2 + dedent-js@1.0.1: {} + + deepmerge@4.3.1: {} + defu@6.1.7: {} dequal@2.0.3: {} @@ -3435,6 +3617,12 @@ snapshots: escape-string-regexp@5.0.0: {} + esm-env@1.2.2: {} + + esrap@2.3.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + estree-util-attach-comments@3.0.0: dependencies: '@types/estree': 1.0.9 @@ -3752,6 +3940,10 @@ snapshots: is-plain-obj@4.1.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -3815,6 +4007,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + locate-character@3.0.0: {} + longest-streak@3.1.0: {} lru-cache@11.5.2: {} @@ -4298,6 +4492,8 @@ snapshots: transitivePeerDependencies: - supports-color + mri@1.2.0: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -4418,6 +4614,11 @@ snapshots: prettier: 3.9.6 sass-formatter: 0.7.9 + prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8): + dependencies: + prettier: 3.9.6 + svelte: 5.56.8 + prettier@3.9.6: {} prismjs@1.30.0: {} @@ -4625,6 +4826,10 @@ snapshots: s.color@0.0.15: {} + sade@1.8.1: + dependencies: + mri: 1.2.0 + sass-formatter@0.7.9: dependencies: suf-log: 2.5.3 @@ -4648,6 +4853,8 @@ snapshots: sax@1.6.1: {} + scule@1.3.0: {} + semver@7.8.5: {} sharp@0.35.3(@types/node@24.13.3): @@ -4746,6 +4953,47 @@ snapshots: dependencies: s.color: 0.0.15 + svelte-check@4.7.5(picomatch@4.0.5)(svelte@5.56.8)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.2 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.5) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.56.8 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte2tsx@0.7.59(svelte@5.56.8)(typescript@6.0.3): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 5.56.8 + typescript: 6.0.3 + + svelte@5.56.8: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.12(acorn@8.18.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.0 + esm-env: 1.2.2 + esrap: 2.3.2 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + svgo@4.0.2: dependencies: commander: 11.1.0 @@ -5051,6 +5299,8 @@ snapshots: yocto-queue@1.2.2: {} + zimmerframe@1.1.4: {} + zod@4.4.3: {} zwitch@2.0.4: {} diff --git a/scripts/compare_pkg_completeness.py b/scripts/compare_pkg_completeness.py index e4d7e77e..51066849 100644 --- a/scripts/compare_pkg_completeness.py +++ b/scripts/compare_pkg_completeness.py @@ -1,7 +1,7 @@ """Build the package dataset behind the Available Packages pages. -Writes `public/data/.json`, which `src/scripts/package-table.js` fetches -and renders in the browser. Three sources are combined: +Writes `public/data/.json`, which `src/components/PackageTable.svelte` +fetches and renders in the browser. Three sources are combined: - `rosdistro`'s `distribution.yaml` for the package list, the released version, and the upstream source repository. @@ -21,8 +21,8 @@ would work today, but the specs appear in two forms (`0.9.* humble_*` and `>=0.9.0,<0.10.0a0`) and nothing stops a third from showing up. -The JSON is positional to keep it small; `packages.js` unpacks it by index, so the -order in `PackageRecordJson` is load-bearing. +The JSON is positional to keep it small; `PackageTable.svelte` unpacks it by +index, so the order in `PackageRecordJson` is load-bearing. Usage: python scripts/compare_pkg_completeness.py channel is an anaconda.org channel name or a full base URL. @@ -46,8 +46,9 @@ import yaml from rattler import MatchSpec, PackageRecord -# Keep in sync with PLATFORMS in src/scripts/package-table.js: the bit positions -# here are the bit positions the page reads. +# The bit positions here are the bit positions the page reads. The page takes +# the platform order from the JSON itself; only the icon map in +# src/components/PackageTable.svelte is keyed by platform id. PLATFORMS: list[str] = [ "linux-64", "linux-aarch64", diff --git a/src/components/PackageTable.astro b/src/components/PackageTable.astro index 69dac8b3..420379f9 100644 --- a/src/components/PackageTable.astro +++ b/src/components/PackageTable.astro @@ -1,6 +1,7 @@ --- import type { Distro } from "../data/distros"; import { browseUrl } from "../data/distros"; +import PackageTable from "./PackageTable.svelte"; interface Props { distro: Distro; @@ -12,558 +13,23 @@ const browse = browseUrl(distro); { /* - The `data-eol` attribute suppresses the per-row "Add to channel" button. - Inviting contributions to a distro upstream has stopped supporting would - waste someone's afternoon. + The `eol` prop suppresses the per-row "Add to channel" button. Inviting + contributions to a distro upstream has stopped supporting would waste + someone's afternoon. - `not-content` opts the runtime-rendered table UI out of Starlight's - Markdown sibling spacing, which would otherwise push extra margins between - the filter buttons and controls. + The island renders and updates everything itself; this wrapper only feeds + it the distro facts that are known at build time. */ } -
-
- - - -{ - /* - Global rather than scoped: every element below the mount is created at - runtime by package-table.js, and Astro's scoped selectors only match - server-rendered markup. Everything is namespaced under .rs-packages or an - rs- class it generates. -*/ -} - + channel directly at {browse.replace("https://", "")}. +

+ diff --git a/src/components/PackageTable.svelte b/src/components/PackageTable.svelte new file mode 100644 index 00000000..d6583548 --- /dev/null +++ b/src/components/PackageTable.svelte @@ -0,0 +1,1292 @@ + + + +{#snippet iconSpan(name: string, size: number)} + + +{/snippet} + +{#snippet extLink(href: string, title: string, name: string)} + + {@render iconSpan(name, 13)} + {title} + +{/snippet} + +{#snippet upgradeChip(row: MutexRow)} + {#if row.upgrade} + + ↑ {row.upgrade.version} on {row.upgrade.mutex} + {/if} +{/snippet} + +
+ {#if error} +
+

Could not load the package list

+

+ The request failed with {error}. Browse the channel directly at + {browse.replace("https://", "")}. +

+
+ {:else if !doc} +

Loading packages…

+ {:else} +
+

+ {percent}%of the packages on the index available on RoboStack +

+
+ + +
+

+ {counts.full} on every platform + {counts.partial} partial + {counts.missing} not on channel + of those, {counts.behind} behind the index +

+ {#if mutexes.length} +

+ + {#if counts.upgrade} + {counts.upgrade} newer on a newer mutex + {/if} + {#if counts.older} + {counts.older} built only for an older mutex + {/if} +

+ {/if} +
+ +
+ + +
+
+ {#each chips as f (f.id)} + + {/each} +
+ +
+
+ +

+ Showing {rows.length.toLocaleString()} of {all.length.toLocaleString()} + index packages. + {#if hiddenPlatforms.length} + {hiddenPlatforms.join(", ")} hidden: nothing built for this mutex. + {/if} +

+ +
+ + + + {#each active as p (p.id)} + + {/each} + + + + + {#each active as p (p.id)} + {@const meta = PLATFORMS[p.id] ?? { icon: "linux", arch: "" }} + + {/each} + + + + + {#if first > 0} + + + + {/if} + {#each slice as row, i (row.name)} + {@const conda = "ros-" + distro + "-" + row.name} + + {@const rosName = row.name.replace(/-/g, "_")} + + + {#each active as p (p.id)} + {@const on = ((row.mask >> p.bit) & 1) === 1} + + {/each} + + {/each} + {#if padBottom > 0} + + + + {/if} + +
Package + + {@render iconSpan(meta.icon, 14)}{meta.arch} + + {p.id} +
+ 0 && row.built === row.total} + class:rs-dot--partial={row.built > 0 && row.built < row.total} + > + + + ros-{distro}-{row.name} + + + {#if row.version} + {row.version} + {@render upgradeChip(row)} + + {#if row.behind && !row.upgrade} + {baseVersion(row.indexVersion)} + {/if} + {:else if row.upgrade} + {@render upgradeChip(row)} + {:else if row.older.length} + + mutex {row.older[0]}{row.older.length > 1 + ? " +" + (row.older.length - 1) + : ""} + {:else if row.indexVersion} + {baseVersion(row.indexVersion)} + {/if} + {#if row.version} + {@render extLink( + "https://prefix.dev/channels/" + + encodeURIComponent(channel) + + "/packages/" + + encodeURIComponent(conda), + conda + " on " + channel, + "channel", + )} + {/if} + {@render extLink( + "https://index.ros.org/p/" + + encodeURIComponent(rosName) + + "/#" + + distro, + rosName + " on the ROS index", + "docs", + )} + {#if row.repo} + {@render extLink( + row.repo, + row.repo.replace(/^https?:\/\//, ""), + "github", + )} + {/if} + + {#if row.never && !eol} + + {@render iconSpan("github", 12)} + Add to channel + + {/if} + + {row.desc || "-"} + + + {on ? "✓" : "·"}{p.id}: {on ? "available" : "not on channel"} + +
+
+ + {#if rows.length === 0} +
+

No matches

+

No packages match that search or filter.

+
+ {/if} + {/if} +
+ + diff --git a/src/components/home/QuickStart.astro b/src/components/home/QuickStart.astro index 76da8ffd..15f22c04 100644 --- a/src/components/home/QuickStart.astro +++ b/src/components/home/QuickStart.astro @@ -36,14 +36,10 @@ const commands = [
{ - commands.map((command, i) => ( + commands.map((command) => (
$ - {i === commands.length - 1 ? ( - {command} - ) : ( - command - )} + {command}
)) } @@ -138,7 +134,4 @@ const commands = [ color: #8ea4ff; user-select: none; } - .accent { - color: #8ea4ff; - } diff --git a/src/components/home/RsButton.astro b/src/components/home/RsButton.astro index 681be1b2..847a1598 100644 --- a/src/components/home/RsButton.astro +++ b/src/components/home/RsButton.astro @@ -54,4 +54,14 @@ const Tag = href ? "a" : "button"; font-size: 18px; padding: 6px 16px 7px; } + /* Confirmation state for copy buttons (see copy-buttons.ts). Feedback has + to be immediate, so no transition: a 250ms fade out of the hover-inverted + state reads as a glitch rather than a confirmation. */ + .btn.copied, + .btn.copied:hover { + background: transparent; + border-color: var(--rs-good); + color: var(--rs-good); + transition: none; + } diff --git a/src/content/docs/Contributing.md b/src/content/docs/Contributing.md index c7de27ec..b754f704 100644 --- a/src/content/docs/Contributing.md +++ b/src/content/docs/Contributing.md @@ -7,7 +7,7 @@ Many thanks for taking the time to read this and for contributing to RoboStack! This project is in early stages and we are looking for contributors to help it grow. -The developers are on the [`robotics` channel on `prefix.dev`'s discord](https://discord.gg/kKV8ZxyzY4) where we discuss steps forward. +The developers are on the [robotics channel on the prefix.dev Discord](https://discord.gg/kKV8ZxyzY4) where we discuss steps forward. We welcome all kinds of contribution -- code or non-code -- and value them highly. We pledge to treat everyones contribution fairly and with respect and @@ -55,13 +55,13 @@ To make code review easier, please consider manually porting the new hunks into Clone the relevant repo: -```bash +```bash wrap git clone https://github.com/RoboStack/ros-humble.git # or: git clone https://github.com/RoboStack/ros-noetic.git or git clone https://github.com/RoboStack/ros-jazzy.git or git clone https://github.com/RoboStack/ros-kilted.git or git clone https://github.com/RoboStack/ros-lyrical.git or git clone https://github.com/RoboStack/ros-rolling.git ``` Then move in the newly cloned repo, and if necessary do any change to the `vinca_*.yaml` file for your platform: -```bash +```bash wrap cd ros-humble # or: cd ros-noetic or cd ros-jazzy or cd ros-kilted or cd ros-lyrical or cd ros-rolling ``` diff --git a/src/content/docs/FAQ.md b/src/content/docs/FAQ.md index f2b81d3c..766b49cb 100644 --- a/src/content/docs/FAQ.md +++ b/src/content/docs/FAQ.md @@ -100,10 +100,12 @@ platforms = [ ] ``` +Alternatively, you can declare the baseline with a `[system-requirements]` table: + +```toml [system-requirements] libc = { family = "glibc", version = "2.31" } linux = "5.15" - ``` Note: Set the glibc version to match the oldest machine or robot that needs to run your project, not necessarily your personal machine. You can check a machine's version by running `ldd --version`. @@ -116,5 +118,4 @@ Installing other recent packages via conda-forge side-by-side works easily, e.g. As no system libraries are used, you can also easily install ROS Noetic on any recent Linux Distribution - including older versions of Ubuntu. As the packages are pre-built, it saves you from compiling from source, which is especially helpful on macOS and Windows. No root access is required, all packages live in your home directory. -We have recently written up a paper and blog post with more information. -``` +We have written up a [paper](https://arxiv.org/abs/2104.12910) and [blog post](https://medium.com/robostack/cross-platform-conda-packages-for-ros-fa1974fd1de3) with more information. diff --git a/src/content/docs/conda.mdx b/src/content/docs/conda.mdx index e9d8f311..c52d36f3 100644 --- a/src/content/docs/conda.mdx +++ b/src/content/docs/conda.mdx @@ -1,5 +1,5 @@ --- -title: Install robostack packages with conda +title: Install RoboStack packages with Conda slug: conda --- diff --git a/src/content/docs/support.md b/src/content/docs/support.md index dbc9cfdd..ccf0dc76 100644 --- a/src/content/docs/support.md +++ b/src/content/docs/support.md @@ -8,7 +8,7 @@ We strive to make our documentation as clear as possible, but sometimes things c How to help improve our documentation: - For typos, grammar, or other errors, we'd appreciate your support! Simply [fork](https://github.com/RoboStack/robostack.github.io/fork) our repo, make the necessary changes, and submit a pull request. -- If you have questions or need help, feel free to create an [issue](https://github.com/RoboStack/robostack.github.io/issues) or join the community in the [`robotics` channel on `prefix.dev`'s discord](https://discord.gg/kKV8ZxyzY4). +- If you have questions or need help, feel free to create an [issue](https://github.com/RoboStack/robostack.github.io/issues) or join the community in the [robotics channel on the prefix.dev Discord](https://discord.gg/kKV8ZxyzY4). We're always eager to improve, and your input is valuable to us. Thank you for being part of the RoboStack community! diff --git a/src/routeData.ts b/src/routeData.ts new file mode 100644 index 00000000..a76efbf6 --- /dev/null +++ b/src/routeData.ts @@ -0,0 +1,17 @@ +/* Starlight highlights a sidebar link only on an exact URL match, so the + * "Packages" entry (which points at one distro page) would lose its active + * state on every other distro page. Mark it current on all of them. */ + +import { defineRouteMiddleware } from "@astrojs/starlight/route-data"; +import { DISTROS } from "./data/distros"; + +const DISTRO_PATHS = new Set(DISTROS.map((distro) => `/${distro.name}.html`)); + +export const onRequest = defineRouteMiddleware((context) => { + if (!DISTRO_PATHS.has(context.url.pathname)) return; + for (const entry of context.locals.starlightRoute.sidebar) { + if (entry.type === "link" && DISTRO_PATHS.has(entry.href)) { + entry.isCurrent = true; + } + } +}); diff --git a/src/scripts/copy-buttons.ts b/src/scripts/copy-buttons.ts index a20b0b63..6916d039 100644 --- a/src/scripts/copy-buttons.ts +++ b/src/scripts/copy-buttons.ts @@ -1,9 +1,31 @@ /* Binds every button carrying `data-copy`: writes the text to the clipboard - * and briefly flips the button's `.copy-label` to confirm. */ + * and briefly flips the button's `.copy-label` to confirm. + * + * Both labels are stacked in a 1x1 inline grid, with the inactive one kept + * invisible: the button is always as wide as its wider label, so confirming + * cannot resize it or shift its neighbours. */ + +function stackLabels(label: Element): [HTMLSpanElement, HTMLSpanElement] { + const idle = document.createElement("span"); + idle.textContent = label.textContent; + const done = document.createElement("span"); + done.textContent = "Copied"; + done.style.visibility = "hidden"; + for (const span of [idle, done]) { + span.style.gridArea = "1 / 1"; + span.style.justifySelf = "center"; + } + label.replaceChildren(idle, done); + (label as HTMLElement).style.display = "inline-grid"; + return [idle, done]; +} for (const button of document.querySelectorAll( "button[data-copy]", )) { + const label = button.querySelector(".copy-label"); + const spans = label ? stackLabels(label) : null; + button.addEventListener("click", async () => { const text = button.dataset.copy ?? ""; try { @@ -19,14 +41,15 @@ for (const button of document.querySelectorAll( document.execCommand("copy"); area.remove(); } - const label = button.querySelector(".copy-label"); - if (!label || button.classList.contains("copied")) return; - const original = label.textContent; + if (!spans || button.classList.contains("copied")) return; + const [idle, done] = spans; button.classList.add("copied"); - label.textContent = "Copied"; + idle.style.visibility = "hidden"; + done.style.visibility = "visible"; setTimeout(() => { button.classList.remove("copied"); - label.textContent = original; + idle.style.visibility = "visible"; + done.style.visibility = "hidden"; }, 1400); }); } diff --git a/src/scripts/package-table.ts b/src/scripts/package-table.ts deleted file mode 100644 index dbd2cadd..00000000 --- a/src/scripts/package-table.ts +++ /dev/null @@ -1,763 +0,0 @@ -/* Renders the Available Packages table from /data/.json. - * - * The distro pages used to ship the whole table as static Markdown — around - * 2,300 rows and 13,000 remote emoji images per page. This fetches the same - * data as JSON instead and renders only the rows currently on screen. - * - * Everything on a channel is built against one version of the ROS distro - * mutex, and builds for different mutex versions cannot be installed together. - * So availability is always relative to a mutex: the dataset carries, per - * package and per mutex, which platforms have a build and what version is - * there, and picking a mutex recomputes the marks, the versions, the coverage - * figure and every filter count. The newest mutex is the default, because that - * is what a fresh install resolves to. - * - * Windowing uses a tall empty row above and below the visible slice rather - * than absolute positioning, which a real table would not allow. The matching - * styles live in PackageTable.astro. - */ - -interface PlatformMeta { - icon: string; - arch: string; -} - -const PLATFORMS: Record = { - "linux-64": { icon: "linux", arch: "x64" }, - "linux-aarch64": { icon: "linux", arch: "arm" }, - "osx-64": { icon: "apple", arch: "x64" }, - "osx-arm64": { icon: "apple", arch: "arm" }, - "win-64": { icon: "windows", arch: "x64" }, - // Only one wasm target, so the icon alone is unambiguous. - "emscripten-wasm32": { icon: "wasm", arch: "" }, -}; - -const FILTERS = [ - { id: "all", label: "All" }, - { id: "full", label: "Complete" }, - { id: "partial", label: "Partial" }, - { id: "missing", label: "Missing" }, - { id: "behind", label: "Behind index" }, -]; - -const OVERSCAN = 6; -const CONTRIBUTING = "/Contributing.html#adding-new-packages-via-pull-requests"; - -/* One entry per mutex, aligned with doc.mutexes: 0 when nothing is built for - * that mutex, otherwise a platform bitmask plus the version built. */ -type BuildSlot = 0 | [number, string]; - -/* name, description, license, index version, last-built timestamp, index into - * doc.repos (-1 for none), build slots. */ -type PackageEntry = [ - string, - string, - string, - string, - number, - number, - BuildSlot[], -]; - -interface Doc { - distro: string; - channel: string; - platforms: string[]; - mutexPackage: string; - mutexes: string[]; - repos: string[]; - packages: PackageEntry[]; -} - -interface Upgrade { - version: string; - mutex: string; -} - -interface Row { - name: string; - desc: string; - license: string; - indexVersion: string; - updated: number; - repo: string; - builds: BuildSlot[]; - haystack: string; - // Derived per mutex by applyMutex(). - mask: number; - version: string; - built: number; - total: number; - behind: boolean; - older: string[]; - upgrade: Upgrade | null; - never: boolean; -} - -interface ActivePlatform { - id: string; - bit: number; -} - -const ESCAPES: Record = { - "&": "&", - "<": "<", - ">": ">", - '"': """, -}; - -function escapeHtml(value: unknown): string { - return String(value).replace(/[&<>"]/g, (c) => ESCAPES[c]); -} - -// rosdistro versions carry a release increment ("2.0.2-1"); drop it to -// compare against the plain version conda publishes. -function baseVersion(value: string): string { - return String(value || "").split("-")[0]; -} - -function versionParts(value: string): number[] { - return baseVersion(value) - .split(".") - .map((part) => (/^\d+$/.test(part) ? parseInt(part, 10) : -1)); -} - -function compareVersions(a: string, b: string): number { - const x = versionParts(a); - const y = versionParts(b); - for (let i = 0; i < Math.max(x.length, y.length); i++) { - const delta = - (x[i] === undefined ? -1 : x[i]) - (y[i] === undefined ? -1 : y[i]); - if (delta) return delta < 0 ? -1 : 1; - } - return 0; -} - -/* The artwork lives in /images/icons/ and is applied as a CSS mask, not an - * : a mask takes its colour from the surrounding text, so the same file - * works for a muted column header and for a link that inverts on hover. Size - * is per use, since headers, row links and the add button all differ. */ -function icon(name: string, size: number): string { - return ``; -} - -function Table(mount: HTMLElement, doc: Doc): void { - const all: Row[] = doc.packages.map((pkg) => ({ - name: pkg[0], - desc: pkg[1], - license: pkg[2], - indexVersion: pkg[3], - updated: pkg[4], - repo: pkg[5] >= 0 ? doc.repos[pkg[5]] : "", - builds: pkg[6], // aligned with doc.mutexes - haystack: (pkg[0] + " " + pkg[1]).toLowerCase(), - mask: 0, - version: "", - built: 0, - total: 0, - behind: false, - older: [], - upgrade: null, - never: false, - })); - - const state = { - query: "", - filter: "all", - sort: "name", - mutex: 0, - rows: [] as Row[], - }; - let active: ActivePlatform[] = []; - let counts: Record = {}; - let columns = 0; - let thead: HTMLTableSectionElement; - let tbody: HTMLTableSectionElement; - let rowHeight = 68; - - /* A platform column with nothing built for the selected mutex is thousands - * of identical empty cells, so it is dropped and reported instead. This is - * recomputed per mutex: humble builds nothing for wasm on 0.1, so that - * column genuinely does not exist there. */ - function activePlatforms(): ActivePlatform[] { - return doc.platforms - .map((id, bit) => ({ id, bit })) - .filter((p) => - all.some((row) => { - const slot = row.builds[state.mutex]; - return slot ? (slot[0] & (1 << p.bit)) !== 0 : false; - }), - ); - } - - /* Derives every per-mutex value onto the rows. Called whenever the mutex - * changes; O(n) over 2,300 rows, which is far cheaper than re-fetching. */ - function applyMutex(): void { - active = activePlatforms(); - columns = active.length + 1; - const bits = active.map((p) => p.bit); - - all.forEach((row) => { - const slot = row.builds[state.mutex]; - row.mask = slot ? slot[0] : 0; - row.version = slot ? slot[1] : ""; - row.built = bits.reduce((n, bit) => n + ((row.mask >> bit) & 1), 0); - row.total = bits.length; - row.behind = - !!row.version && - !!row.indexVersion && - compareVersions(row.version, row.indexVersion) < 0; - // Which other mutexes do have it, so a gap reads as "built, but not - // for this mutex" rather than "never built". Newer and older are kept - // apart because only one of them is actionable: a package waiting on a - // newer mutex arrives if you move up, one that exists only on an older - // mutex has been dropped and will not come back. - // doc.mutexes is newest first, so a lower index means newer. - row.older = []; - row.upgrade = null; - // Never built for any mutex. Distinct from "not built for the selected - // mutex": that one is answered by changing the mutex, this one only by - // someone adding the package. - row.never = !row.builds.some((slot) => Boolean(slot)); - for (let i = 0; i < doc.mutexes.length; i++) { - const other = row.builds[i]; - if (i === state.mutex || !other) continue; - if (i > state.mutex) { - if (!row.mask) row.older.push(doc.mutexes[i]); - continue; - } - // A newer mutex: worth reporting when it offers this package at all, - // or offers a newer version of it than the selected mutex does. - if ( - !row.upgrade || - compareVersions(other[1], row.upgrade.version) > 0 - ) { - row.upgrade = { version: other[1], mutex: doc.mutexes[i] }; - } - } - if ( - row.upgrade && - row.version && - compareVersions(row.upgrade.version, row.version) <= 0 - ) { - row.upgrade = null; - } - }); - - counts = { - all: all.length, - full: all.filter((r) => r.total && r.built === r.total).length, - partial: all.filter((r) => r.built > 0 && r.built < r.total).length, - missing: all.filter((r) => r.built === 0).length, - behind: all.filter((r) => r.behind).length, - upgrade: all.filter((r) => r.upgrade).length, - older: all.filter((r) => !r.mask && r.older.length).length, - }; - } - - /* The upgrade filter only exists while an older mutex is selected — on the - * newest there is nothing newer to move to. */ - function filters(): { id: string; label: string }[] { - return counts.upgrade - ? FILTERS.concat([{ id: "upgrade", label: "Newer on a newer mutex" }]) - : FILTERS; - } - - function matchesFilter(row: Row, filter: string): boolean { - switch (filter) { - case "full": - return row.total > 0 && row.built === row.total; - case "partial": - return row.built > 0 && row.built < row.total; - case "missing": - return row.built === 0; - case "behind": - return row.behind; - case "upgrade": - return !!row.upgrade; - default: - return true; - } - } - - function apply(): void { - const query = state.query.trim().toLowerCase(); - const rows = all.filter( - (row) => - matchesFilter(row, state.filter) && - (!query || row.haystack.indexOf(query) !== -1), - ); - const sorters: Record number> = { - name: (a, b) => a.name.localeCompare(b.name), - coverage: (a, b) => b.built - a.built || a.name.localeCompare(b.name), - gaps: (a, b) => a.built - b.built || a.name.localeCompare(b.name), - recent: (a, b) => b.updated - a.updated || a.name.localeCompare(b.name), - }; - state.rows = rows.sort(sorters[state.sort] || sorters.name); - } - - function chrome(): void { - const available = counts.full + counts.partial; - const percent = all.length ? Math.round((available / all.length) * 100) : 0; - // Behind-index packages are a subset of the available ones — a package - // needs a version on the channel before it can be compared — so the bar - // splits the filled portion rather than adding to it. Unrounded widths, - // so the two segments cannot drift apart from the total. - const availablePct = all.length ? (available / all.length) * 100 : 0; - const behindPct = all.length ? (counts.behind / all.length) * 100 : 0; - const currentPct = Math.max(0, availablePct - behindPct); - const hidden = doc.platforms.filter( - (_id, bit) => !active.some((p) => p.bit === bit), - ); - - mount.innerHTML = - '
' + - '

' + - percent + - "%" + - 'of the packages on the index ' + - "available on RoboStack

" + - '
' + - '' + - '' + - "
" + - '

' + - '' + - counts.full + - " on every platform" + - '' + - counts.partial + - " partial" + - '' + - counts.missing + - " not on channel" + - 'of those, ' + - counts.behind + - " behind the index" + - "

" + - mutexPicker() + - "
" + - '
' + - '' + - // Filters and sort travel together, so the sort control stays beside - // them and only drops to its own line when they genuinely overflow. - '
' + - '
' + - filters() - .map((f) => { - const on = f.id === state.filter; - return ( - '" - ); - }) - .join("") + - "
" + - '" + - "
" + - "
" + - '

' + - (hidden.length - ? " " + hidden.join(", ") + " hidden — nothing built for this mutex." - : "") + - "

" + - '
' + - "" + - active.map(() => '').join("") + - "" + - active - .map((p) => { - const meta = PLATFORMS[p.id] || { icon: "linux", arch: "" }; - return ( - '" - ); - }) - .join("") + - "" + - "
Package' + - icon(meta.icon, 14) + - meta.arch + - "
" + - '"; - - thead = mount.querySelector("thead")!; - tbody = mount.querySelector("tbody")!; - // One source of truth for the row height, so the windowing maths cannot - // drift from the stylesheet. - rowHeight = - parseInt( - window.getComputedStyle(mount).getPropertyValue("--rs-row-h"), - 10, - ) || 68; - } - - function mutexPicker(): string { - if (!doc.mutexes.length) return ""; - return ( - '

" + - (counts.upgrade - ? '' + - counts.upgrade + - " newer on a newer mutex" - : "") + - (counts.older - ? '' + - counts.older + - " built only for an older mutex" - : "") + - "

" - ); - } - - function link(href: string, title: string, name: string): string { - return ( - '' + - icon(name, 13) + - "" - ); - } - - function rowHtml(row: Row): string { - const conda = "ros-" + doc.distro + "-" + row.name; - // The ROS index spells package names with underscores; conda uses hyphens. - const rosName = row.name.replace(/-/g, "_"); - - // A newer mutex offering this package, or a newer version of it, is the - // one gap the reader can act on — so it is called out the same way - // whether the selected mutex has nothing or merely has something older. - const upgrade = row.upgrade - ? '↑ ' + - escapeHtml(row.upgrade.version) + - " on " + - escapeHtml(row.upgrade.mutex) + - "" - : ""; - - // On this mutex: the version built for it, plus an amber pill carrying - // the newer version when the index has moved past it. Not on this mutex: - // the upgrade chip, a grey pill naming an older mutex that does have it, - // or the index version — in that order of usefulness. - const version = row.version - ? '' + - escapeHtml(row.version) + - "" + - upgrade + - // Suppressed alongside the upgrade chip: three versions on one line - // squeezes the package name out, and "a newer mutex has more" is the - // more actionable of the two ways to be behind. - (row.behind && !upgrade - ? '' + - escapeHtml(baseVersion(row.indexVersion)) + - "" - : "") - : upgrade || - // Labelled "mutex", because a bare version here sits where the - // package version normally does and would be read as one. - (row.older.length - ? 'mutex ' + - escapeHtml(row.older[0]) + - (row.older.length > 1 ? " +" + (row.older.length - 1) : "") + - "" - : row.indexVersion - ? '' + - escapeHtml(baseVersion(row.indexVersion)) + - "" - : ""); - - return ( - "" + - '' + - '' + - '' + - 'ros-' + - escapeHtml(doc.distro) + - "-" + - escapeHtml(row.name) + - "" + - version + - (row.version - ? link( - "https://prefix.dev/channels/" + - encodeURIComponent(doc.channel) + - "/packages/" + - encodeURIComponent(conda), - conda + " on " + doc.channel, - "channel", - ) - : "") + - link( - "https://index.ros.org/p/" + - encodeURIComponent(rosName) + - "/#" + - doc.distro, - rosName + " on the ROS index", - "docs", - ) + - (row.repo - ? link(row.repo, row.repo.replace(/^https?:\/\//, ""), "github") - : "") + - // Pushed to the far right of the package cell, so it uses the slack - // in that column instead of squeezing the name. - (row.never && !mount.dataset.eol - ? '' + - icon("github", 12) + - 'Add to channel' - : "") + - "" + - "" + - escapeHtml(row.desc || "—") + - "" + - "" + - active - .map((p) => { - const on = (row.mask >> p.bit) & 1; - return ( - '' + - (on ? "✓" : "·") + - "" - ); - }) - .join("") + - "" - ); - } - - // A single empty row standing in for `count` rows that are not rendered. - function padding(count: number): string { - return count > 0 - ? '' - : ""; - } - - function renderRows(): void { - const rows = state.rows; - // keeps its place regardless of how tall the padding rows are, - // so its bottom edge is a stable origin for the visible window. - const above = Math.max(0, -thead.getBoundingClientRect().bottom); - const visible = Math.ceil(window.innerHeight / rowHeight) + OVERSCAN * 2; - // Clamping matters: an unbounded first index would make the leading pad - // taller than the table itself, growing the page and letting the reader - // scroll further, which grows it again. - const first = Math.min( - Math.max(0, rows.length - visible), - Math.max(0, Math.floor(above / rowHeight) - OVERSCAN), - ); - const last = Math.min(rows.length, first + visible); - - tbody.innerHTML = - padding(first) + - rows.slice(first, last).map(rowHtml).join("") + - padding(rows.length - last); - } - - /* The stylesheet declares the row height, but td padding can outweigh it, - * and zoom or a font change shifts it again. Measuring a real row keeps the - * padding rows honest whatever the CSS ends up doing. */ - function syncRowHeight(): boolean { - const row = tbody.querySelector("tr:not(.rs-pad)"); - if (!row) return false; - const measured = Math.round(row.getBoundingClientRect().height); - if (!measured || measured === rowHeight) return false; - rowHeight = measured; - return true; - } - - function refresh(): void { - apply(); - mount.querySelector(".rs-count__showing")!.textContent = - "Showing " + - state.rows.length.toLocaleString() + - " of " + - all.length.toLocaleString() + - " index packages."; - mount.querySelector(".rs-empty")!.hidden = - state.rows.length > 0; - // Filtering to a shorter list can strand the reader below the new end of - // the table; pull back to its top only when that has actually happened. - const rect = thead.getBoundingClientRect(); - if (rect.bottom + state.rows.length * rowHeight < 0) { - window.scrollTo(0, window.scrollY + rect.top); - } - renderRows(); - } - - function bind(): void { - const search = mount.querySelector("input")!; - search.addEventListener("input", () => { - state.query = search.value; - refresh(); - }); - const sort = mount.querySelector(".rs-tools select")!; - sort.addEventListener("change", () => { - state.sort = sort.value; - refresh(); - }); - mount.querySelector(".rs-filters")!.addEventListener("click", (e) => { - const button = (e.target as Element).closest( - "[data-filter]", - ); - if (!button) return; - state.filter = button.dataset.filter ?? "all"; - mount.querySelectorAll("[data-filter]").forEach((b) => { - const on = b.dataset.filter === state.filter; - b.setAttribute("aria-pressed", String(on)); - b.classList.toggle("rs-filter--on", on); - }); - refresh(); - }); - const picker = mount.querySelector("[data-mutex]"); - if (picker) { - picker.addEventListener("change", () => { - state.mutex = parseInt(picker.value, 10) || 0; - // Columns and every count change with the mutex, so the whole chrome - // is rebuilt rather than patched. - applyMutex(); - rebuild(); - }); - } - } - - function rebuild(): void { - chrome(); - bind(); - refresh(); - if (syncRowHeight()) renderRows(); - } - - applyMutex(); - rebuild(); - - window.addEventListener("scroll", () => requestAnimationFrame(renderRows), { - passive: true, - }); - window.addEventListener("resize", renderRows); - document.addEventListener("keydown", (e) => { - const search = mount.querySelector("input"); - if (!search) return; - if (e.key === "/" && document.activeElement !== search) { - e.preventDefault(); - search.focus(); - } else if (e.key === "Escape" && document.activeElement === search) { - search.value = ""; - state.query = ""; - refresh(); - } - }); -} - -const mount = document.querySelector(".rs-packages[data-distro]"); -if (mount) { - const distro = mount.dataset.distro ?? ""; - mount.innerHTML = "

Loading packages…

"; - - fetch("/data/" + distro + ".json") - .then((response) => { - if (!response.ok) throw new Error("HTTP " + response.status); - return response.json() as Promise; - }) - .then((payload) => { - Table(mount, payload); - }) - .catch((error: Error) => { - mount.innerHTML = - '
' + - '

Could not load the package list

' + - "

The request failed with " + - escapeHtml(error.message) + - ". Browse the channel directly at " + - 'prefix.dev.

'; - }); -} diff --git a/src/styles/custom.css b/src/styles/custom.css index 58bbb86a..740b79d0 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -18,6 +18,29 @@ Starlight's cool blue-gray defaults so the docs share the frontpage's paper character. Text steps keep a slight ink-navy cast; surface steps (5-7, black) are warm cream / charcoal. */ +/* Hand the custom families to Starlight, which otherwise falls back to the + system stack on every docs page. Starlight appends its own fallbacks. */ +:root { + --sl-font: "Inter"; + --sl-font-mono: "JetBrains Mono"; +} + +/* Docs headings share the frontpage's serif display face. */ +.sl-markdown-content :is(h1, h2, h3, h4, h5, h6), +.content-panel h1 { + font-family: var(--font-display); +} + +/* Starlight hides the header's social links and theme switcher below 50rem + and offers them in the sidebar drawer instead. Pages without a sidebar + (the splash landing page) have no drawer, so keep the controls in the + header there; they fit because those pages have no menu button either. */ +@media (max-width: 49.999rem) { + :root:not([data-has-sidebar]) .header .right-group { + display: flex; + } +} + :root { --sl-color-accent-low: #dee5ff; --sl-color-accent: #2e46d8; diff --git a/src/virtual-starlight.d.ts b/src/virtual-starlight.d.ts new file mode 100644 index 00000000..519e01a0 --- /dev/null +++ b/src/virtual-starlight.d.ts @@ -0,0 +1,4 @@ +/* Starlight's internals import `virtual:starlight/*` modules that only exist + * inside Astro's runtime. svelte-check follows the import chain from + * routeData.ts into them and cannot resolve the specifiers, so declare them. */ +declare module "virtual:starlight/*"; diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 00000000..cf44f387 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from "@astrojs/svelte"; + +export default { + preprocess: vitePreprocess(), +};