From 617689d576e12e127c15b21e8535be6b10473250 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 10 Aug 2026 11:51:04 +0000 Subject: [PATCH] fix: website bugs I spawned a couple of subagents to find bugs in the website. It fixed the following (smallish) bugs: - Clicking a copy button resized it and shifted its neighbours; the BibTeX one had no confirmation style at all - The last terminal line on the landing page was accent blue for no good reason - The landing page hid the theme switcher and social links on phones (no sidebar drawer to house them) - Broken code fences in the FAQ swallowed the glibc note and the whole "Why ROS and Conda?" section - The custom fonts never reached the docs pages (`--sl-font` was never set) - The sidebar "Packages" entry lost its highlight on 7 of 8 distro pages - The copy button overlapped clipped text in the long Contributing code blocks - Small fixes: conda page title casing, Discord link phrasing ### Package table bugs As expected, it found the most bugs in the TS code for the package table: - Switching the mutex reset the search box, sort select and keyboard focus - A filter could survive a mutex switch with its chip gone, silently showing "No matches" - Scrolling rebuilt every row, killing hover state and text selection - rosdistro repo URLs went into `href` unescaped - The fetch-error fallback linked to a wrong channel URL for foxy and galactic - The table was invisible to screen readers as a matrix: no `scope="col"`, no text on the availability marks, no row count - The `ros--` prefix failed WCAG AA contrast and ellipsized the actual package name on phones It suggested to use Svelte instead of plain TS code. After asking why Svelte instead of plain TS or React, that's what it said: - Plain TS: every control rebuild had to carry state, focus and escaping by hand, which is exactly where the bugs above came from. Declarative rendering removes that bug class instead of patching each instance - React: No VDOM and fine-grained updates suit a scroll-windowed table; keyed rows are patched, not rebuilt - Compiled output with Svelte is ~10-15 KB gzipped vs ~40+ KB for React - Single-file components with scoped styles match how the `.astro` components are already written, and the compiler emits a11y warnings at build time --- .prettierignore | 2 + .prettierrc | 2 +- astro.config.mjs | 3 + package.json | 6 +- pixi.toml | 7 +- pnpm-lock.yaml | 250 +++++ scripts/compare_pkg_completeness.py | 13 +- src/components/PackageTable.astro | 568 +---------- src/components/PackageTable.svelte | 1292 ++++++++++++++++++++++++++ src/components/home/QuickStart.astro | 11 +- src/components/home/RsButton.astro | 10 + src/content/docs/Contributing.md | 6 +- src/content/docs/FAQ.md | 7 +- src/content/docs/conda.mdx | 2 +- src/content/docs/support.md | 2 +- src/routeData.ts | 17 + src/scripts/copy-buttons.ts | 35 +- src/scripts/package-table.ts | 763 --------------- src/styles/custom.css | 23 + src/virtual-starlight.d.ts | 4 + svelte.config.js | 5 + 21 files changed, 1682 insertions(+), 1346 deletions(-) create mode 100644 src/components/PackageTable.svelte create mode 100644 src/routeData.ts delete mode 100644 src/scripts/package-table.ts create mode 100644 src/virtual-starlight.d.ts create mode 100644 svelte.config.js 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(), +};