-
-
-
-{
- /*
- 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://", "")}.
+
+ {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}
+
+
+
+
Package
+ {#each active as p (p.id)}
+ {@const meta = PLATFORMS[p.id] ?? { icon: "linux", arch: "" }}
+
))
}
@@ -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
' +
- '' +
- // Filters and sort travel together, so the sort control stays beside
- // them and only drops to its own line when they genuinely overflow.
- '
";
-
- 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 || "—") +
- "" +
- "
"
- );
- }
-
- // 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 = "