feat: list long file pages as filtered tables

MAME's card pages, GroovyMAME and yaps2 read 13,000 to 16,700 words of
cards. Each entry is now a table row (name, members, status, short
hash) that opens to its card in a details block, with the site's filter
box and row counter. Pages from 150 entries and every split part use it.
Visible words: mame-files-1 16,680 to 1,612, groovymame 14,396 to 1,876.

A row costs about five elements, so a part holds 180 cards: MAME goes
from 8 parts to 9, the largest at 567 KB and 8,989 elements.

Each wanted list (TOSEC, No-Intro, Redump) opens on its counts, then one
table folded under a filter box that opens it, in place of a block per
DAT. Only the names are searched: indexed descriptions and hashes put
the PlayStation page ahead of the PlayStation 2 one.

A filtered row stayed visible on a phone: a stacked row's display:block
outweighed the hidden attribute on every stacked table.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 21:14:04 +01:00
1 parent 7ec4d00375
commit 1409a8db6a
8 files changed
+658 -28

No files matched your search

+30 -2
View File
@@ -87,9 +87,19 @@ const setScrollable = (table: HTMLTableElement, overflows: boolean) => {
// The filter box above a long table is written when the page is built
// (src/plugins/rehype-stack-tables.mjs), so the table never moves down once
// drawn. Here it only filters.
const filterTable = (table: HTMLTableElement) => {
// A table folded in a closed block can have its box above the block
// (the provenance lists): typing opens the block on the rows it keeps.
const filterControls = (table: HTMLTableElement) => {
const controls = table.previousElementSibling;
if (!controls?.matches('.rb-table-filter')) return;
if (controls?.matches('.rb-table-filter')) return controls;
const block = table.closest('details');
return block?.previousElementSibling?.matches('.rb-table-filter') ? block.previousElementSibling : null;
};
const filterTable = (table: HTMLTableElement) => {
const controls = filterControls(table);
if (!controls) return;
const block = controls.nextElementSibling instanceof HTMLDetailsElement ? controls.nextElementSibling : null;
const input = controls.querySelector('input');
const count = controls.querySelector('.rb-table-filter-count');
if (!input || !count || input.dataset.rbWired) return;
@@ -100,6 +110,7 @@ const filterTable = (table: HTMLTableElement) => {
input.addEventListener('input', () => {
searchable ??= rows.map((row) => ({ row, text: row.textContent!.normalize('NFKD').toLocaleLowerCase() }));
const query = input.value.trim().normalize('NFKD').toLocaleLowerCase();
if (block && query) block.open = true;
let visible = 0;
for (const entry of searchable) {
entry.row.hidden = !!query && !entry.text.includes(query);
@@ -171,6 +182,9 @@ const enhanceTables = () => {
});
if (document.readyState === 'complete') start();
else addEventListener('load', start, { once: true });
// A box above a closed block is typed in before its table is ever near
// the screen: it is wired now.
for (const table of tables) if (table.closest('details:not([open])') && filterControls(table)) filterTable(table);
let timer = 0;
addEventListener('resize', () => {
clearTimeout(timer);
@@ -743,6 +757,18 @@ const followMovedCard = () => {
if (match && !target) location.replace(new URL(`files/?sha1=${match[1]}`, siteRoot()));
};
// A link to a card folded in a table row (emulators with many entries)
// opens its row. Chrome does it on its own. Where the browser left the
// target hidden, the same address is followed again once the row is open,
// so the browser brings the card into view as for any link.
const openTargetBlock = () => {
const id = decodeURIComponent(location.hash.slice(1));
const block = id ? document.getElementById(id)?.closest('details') : null;
if (!block || block.open) return;
block.open = true;
location.replace(location.hash);
};
// The site search holds no hashes. A hash typed in it gets "Look up this
// hash" as its first result, and Enter opens the lookup. The pages Pagefind
// finds for the same characters (a file name that holds them, a word that
@@ -948,6 +974,8 @@ const notFound = () => {
resolveFileAddress();
followMovedCard();
openTargetBlock();
addEventListener('hashchange', openTargetBlock);
enhanceTables();
breakPaths();
hashSearch();
+38
View File
@@ -530,6 +530,44 @@ body:has(.rb-sys-file, .rb-file-entry) { --sl-content-width: var(--rb-grid-width
}
:is(.rb-sys-file, .rb-file-entry):has(> .rb-file-title:target) { background: var(--rb-surface); }
/* Entries as table rows (scripts/siteentries.py): the name takes the room
and its card opens under it. On a phone the row stacks like any table
with a hash column, the name on the full width. */
body:has(table.rb-entries) { --sl-content-width: var(--rb-grid-width); }
/* A row the filter leaves out stays out on a phone, where a stacked row is
a block: `display: block` outweighed the hidden attribute, and every
stacked table showed all its rows under "11 of 1,037 rows". */
.sl-markdown-content table[data-rb-stack] tr[hidden] { display: none; }
@media (min-width: 50rem) {
.sl-markdown-content table.rb-wanted td.rb-hash { min-width: 10rem; }
}
.sl-markdown-content .rb-entries-wrap { container-type: inline-size; }
.sl-markdown-content table.rb-entries { width: 100%; }
@media (min-width: 50rem) {
.sl-markdown-content table.rb-entries { display: table; }
.sl-markdown-content table.rb-entries td:first-child { width: 100%; }
/* An open card takes the table's width, not its column's: the other
cells of its row keep to one line, so the room under them is free. */
.sl-markdown-content table.rb-entries tr:has(> td > details[open]) > td:not(:first-child) { white-space: nowrap; }
.sl-markdown-content details.rb-entry[open] { contain: inline-size; }
.sl-markdown-content details.rb-entry[open] > :not(summary) { width: 100cqi; }
}
.sl-markdown-content details.rb-entry { border: 0; padding: 0; }
.sl-markdown-content details.rb-entry > summary { min-height: 24px; font-weight: 400; }
.sl-markdown-content details.rb-entry > summary > .rb-file-title { display: inline; margin: 0; }
.sl-markdown-content details.rb-entry[open] > summary { margin-bottom: 0.4rem; }
.sl-markdown-content details.rb-entry ul { margin: 0.3rem 0 0; padding-left: 1.2rem; font-size: 0.85rem; }
.sl-markdown-content details.rb-entry li { margin: 0.1rem 0; }
.sl-markdown-content details.rb-entry:has(.rb-file-title:target) { background: var(--rb-surface); }
@media (max-width: 50rem) {
.sl-markdown-content table.rb-entries td:first-child { padding-left: 0.6rem; font-size: 1rem; }
.sl-markdown-content table.rb-entries td:first-child::before { content: none; }
/* "required for Data East DECO Cassette System" ran 12 px past a phone
beside its label: a long badge wraps in its cell. */
.sl-markdown-content table.rb-entries .rb-badge { white-space: normal; }
.sl-markdown-content details.rb-entry ul { font-size: 0.75rem; }
}
/* Collapsible blocks are quiet: a closed block must not outweigh the
command it explains. Colour is kept for caution and danger. */
.sl-markdown-content details.rb-details {