feat: list platform files as table rows

Each file of a platform list is one row: name, folder, status chip, a short hash on a button that copies it whole, and a details cell. The details hold every declared hash, the size, the served copy and the readers. A reference's role is one word (glue, port, upstream, build) defined once in a legend instead of a phrase after every reference.

Visible words: Batocera 2,433 to 2,332, AmberELEC 1,301 to 1,299, RomM 1,607 to 1,596, RetroArch 2,207 to 2,088, RetroDECK arcade part 2 19,127 to 2,104. Rows weigh more Markdown, so 38 more systems move to pages of their own under the existing budgets.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 20:44:05 +01:00
1 parent 74b49e8c04
commit 20dc488e41
7 files changed
+535 -85

No files matched your search

+32 -2
View File
@@ -1,8 +1,8 @@
// Progressive enhancement of the generated pages: table captions, filters
// and keyboard scrolling, the hash lookup on files/ (a pasted hash or a file
// hashed on this device), moved cards, a hash typed in the search, glossary
// terms on a tap, the OS tab of the visitor's system, the current page in
// the sidebar, and the 404 page's hints.
// terms on a tap, hash copy buttons, the OS tab of the visitor's system, the
// current page in the sidebar, and the 404 page's hints.
// Which tables stack on a phone is decided when the page is built
// (src/plugins/rehype-stack-tables.mjs), so the layout is right on first paint.
@@ -931,6 +931,35 @@ const glossaryTerms = () => {
document.addEventListener('click', close);
};
// A hash cut short on a button (button.rb-copy): a click copies the whole
// value from data-copy, says so to a screen reader, and marks the button
// for two seconds. The full value is also in the row's details, so a
// browser without the clipboard API loses nothing.
const copyButtons = () => {
if (!document.querySelector('button.rb-copy')) return;
let status: HTMLElement | null = null;
document.addEventListener('click', async (event) => {
const button = (event.target as Element | null)?.closest<HTMLButtonElement>('button.rb-copy');
const value = button?.dataset.copy;
if (!button || !value || !navigator.clipboard) return;
try {
await navigator.clipboard.writeText(value);
} catch {
// Clipboard refused (no permission, insecure context): nothing copied.
return;
}
if (!status) {
status = document.createElement('p');
status.className = 'rb-visually-hidden';
status.setAttribute('role', 'status');
document.body.append(status);
}
status.textContent = `Copied ${value}`;
button.dataset.copied = '';
setTimeout(() => delete button.dataset.copied, 2000);
});
};
// The 404 page: a search button, and the list a moved page belongs to.
const notFound = () => {
document.querySelector('[data-rb-open-search]')?.addEventListener('click', () => {
@@ -952,6 +981,7 @@ enhanceTables();
breakPaths();
hashSearch();
glossaryTerms();
copyButtons();
osTabs();
showCurrentPage();
notFound();