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();
+61
View File
@@ -551,6 +551,67 @@ body:has(.rb-sys-file, .rb-file-entry) { --sl-content-width: var(--rb-grid-width
.sl-markdown-content details.rb-details > summary::before { background-color: var(--rb-text-secondary); }
.sl-markdown-content details.rb-details[open] > summary { margin-bottom: 0.6rem; }
/* A platform file row: a short hash on a button that copies it whole
(site.ts), and the row's evidence in a details cell. */
.sl-markdown-content button.rb-copy {
display: inline-flex;
align-items: center;
gap: 0.3em;
min-height: 24px;
padding: 0 0.35em;
border: 1px solid var(--rb-border-strong);
border-radius: 3px;
background: var(--rb-surface);
color: var(--sl-color-white);
font: 0.8125rem/1.4 var(--sl-font-mono);
cursor: pointer;
white-space: nowrap;
}
.sl-markdown-content button.rb-copy::after {
content: '';
width: 0.9em;
height: 0.9em;
background: var(--rb-text-secondary);
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M5 1h8a2 2 0 0 1 2 2v8h-2V3H5zM3 5h7a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zm0 2v7h7V7z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.sl-markdown-content button.rb-copy[data-copied]::after {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 11.2 2.8 8 1.4 9.4 6 14l8.6-8.6-1.4-1.4z'/%3E%3C/svg%3E");
background: var(--rb-ok-fg);
}
.sl-markdown-content button.rb-copy:hover { border-color: var(--rb-link); }
/* The file name and its destination, written as plain text to spare an
element per cell. */
.sl-markdown-content table:has(:is(button.rb-copy, details.rb-row-details)) td:is(:nth-child(1), :nth-child(2)) {
font-family: var(--sl-font-mono);
font-size: 0.8125rem;
overflow-wrap: anywhere;
}
.sl-markdown-content details.rb-row-details {
margin: 0;
padding: 0;
border: 0;
}
.sl-markdown-content details.rb-row-details > summary {
min-height: 24px;
color: var(--rb-link);
font-size: 0.875rem;
cursor: pointer;
}
.sl-markdown-content details.rb-row-details[open] {
font-size: 0.8125rem;
line-height: 1.5;
overflow-wrap: anywhere;
}
/* Open beside four narrow columns, the evidence keeps a readable width:
the other cells give up their side padding for it. */
@media (min-width: 50rem) {
.sl-markdown-content table:has(details.rb-row-details) :is(th, td) { padding-inline: 0.5rem; }
/* A long name or path wraps before the table outgrows the column. */
.sl-markdown-content table:has(details.rb-row-details) td:first-child { min-width: 9rem; }
.sl-markdown-content table:has(details.rb-row-details) td:nth-child(2) { min-width: 4rem; }
.sl-markdown-content details.rb-row-details[open] { min-width: 15rem; }
}
/* Asides: a neutral box for notes and tips, no side stripe, no icon.
Caution and danger keep their colour, as a 1 px border all round. */
.sl-markdown-content .starlight-aside {