feat: stack tables at build, add section tabs

This commit is contained in:
Abdessamad Derraz committed 2026-10-23 02:27:09 +02:00
1 parent 72e7507ff9
commit 2c71f0d3da
3 files changed
+193 -2

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
---
// Starlight's header bar, and under it the five sections of the site as
// tabs: the funnel (Start here to Contribute) is the site's structure, so it
// sits where every docs site puts its sections. On a phone the tabs stay the
// first row of the menu, where the sidebar lists them.
import Default from '@astrojs/starlight/components/Header.astro';
const topics = Astro.locals.starlightSidebarTopics?.topics ?? [];
---
<div class="rb-header-bar"><Default /></div>
{
topics.length > 0 && (
<nav class="rb-tabs print:hidden" aria-label="Sections">
<ul>
{topics.map((topic) => (
<li>
<a href={topic.link} aria-current={topic.isCurrent ? 'true' : undefined}>
{topic.label}
</a>
</li>
))}
</ul>
</nav>
)
}
<!-- Global: the bar height and the sidebar's topic list belong to other components. -->
<style is:global>
.rb-header-bar { height: 100%; }
.rb-tabs { display: none; }
/* "Look up this hash", the first result of a hash typed in the search (site.ts). */
.rb-search-hash {
margin: 1rem 0 0;
padding: 0.75rem 1rem;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
overflow-wrap: anywhere;
}
.rb-search-hash a { font-weight: 600; }
/* Pagefind's own display rules would show what site.ts hides. */
#starlight__search [hidden] { display: none !important; }
@media (min-width: 50rem) {
:root {
--rb-nav-bar: 4rem;
--rb-tab-row: 2.75rem;
--sl-nav-height: calc(var(--rb-nav-bar) + var(--rb-tab-row));
}
.rb-header-bar { height: calc(100% - var(--rb-tab-row)); }
.site-title { height: calc(var(--rb-nav-bar) - 2 * var(--sl-nav-pad-y)); }
.rb-tabs {
display: block;
position: absolute;
inset-inline: 0;
bottom: 0;
height: var(--rb-tab-row);
padding-inline: var(--sl-nav-pad-x);
}
.rb-tabs ul {
display: flex;
gap: 1.75rem;
height: 100%;
margin: 0;
padding: 0;
list-style: none;
}
.rb-tabs li { display: flex; }
.rb-tabs a {
display: flex;
align-items: center;
border-bottom: 2px solid transparent;
color: var(--sl-color-gray-2);
font-size: var(--sl-text-sm);
text-decoration: none;
white-space: nowrap;
}
.rb-tabs a:hover { color: var(--sl-color-white); }
.rb-tabs a[aria-current] {
border-bottom-color: currentColor;
color: var(--sl-color-white);
font-weight: 600;
}
/* The header holds the sections: the sidebar lists the pages of one. */
.sidebar-content ul.starlight-sidebar-topics { display: none; }
}
</style>
@@ -0,0 +1,73 @@
// Tables a phone reads one row at a time, marked when the page is built so
// the stacked layout is there on first paint (the CSS is retrobios.css,
// table[data-rb-stack]). The rule reads the table, never the page that
// wrote it:
// - a row that ends in an action (a release download, a button, the link to
// a setup's install command) or a header that names a ZIP column: one
// block per row, each cell under its column name;
// - two columns: a list of label and value, data-rb-stack="kv";
// - on a level 0 or 1 page (`level: start` or `setup`), a table short enough
// to read as records (under 20 rows, the size from which site.ts adds a
// filter): one block per row. A longer one is a list scanned by column and
// keeps its scroll.
// Satteri runs hast plugins (the rehype stage of its pipeline): this one is
// registered in astro.config.mjs through satteri({ hastPlugins }).
const ACTION_HREF = /\/releases\/(download\/|latest\/?$)|#install$/;
const ACTION_CLASS = /\bclass=["'][^"']*\brb-button\b/;
const DOWNLOAD_HEADER = /^(zip|download)$/i;
const READER_LEVELS = new Set(['start', 'setup']);
export const RECORD_ROWS = 20;
const elements = (node, tag, found = []) => {
for (const child of node.children ?? []) {
if (child.type !== 'element') continue;
if (child.tagName === tag) found.push(child);
// A table inside a cell is its own table.
if (child.tagName !== 'table') elements(child, tag, found);
}
return found;
};
const holdsAction = (node) =>
(node.children ?? []).some(
(child) =>
(child.type === 'element' &&
((child.tagName === 'a' && ACTION_HREF.test(String(child.properties?.href ?? ''))) ||
String(child.properties?.className ?? '').split(/[\s,]+/).includes('rb-button') ||
holdsAction(child))) ||
(child.type === 'raw' && ACTION_CLASS.test(child.value ?? '')),
);
export const stackKind = ({ labels, rows, action, level }) => {
if (labels.length === 2) return 'kv';
if (action || DOWNLOAD_HEADER.test(labels.at(-1) ?? '')) return 'true';
if (READER_LEVELS.has(level) && labels.length > 2 && rows < RECORD_ROWS) return 'true';
return null;
};
export default function stackTables() {
return {
name: 'rb-stack-tables',
element: {
filter: ['table'],
visit(table, ctx) {
const head = elements(table, 'thead')[0];
const body = elements(table, 'tbody')[0];
if (!head || !body) return;
const labels = elements(head, 'th').map((cell) => ctx.textContent(cell).trim());
const rows = elements(body, 'tr');
const level = ctx.data.astro?.frontmatter?.level;
const kind = stackKind({ labels, rows: rows.length, action: holdsAction(body), level });
if (!kind) return;
ctx.setProperty(table, 'dataRbStack', kind);
if (kind === 'kv') return;
for (const row of rows) {
elements(row, 'td').forEach((cell, column) => {
if (labels[column] && cell.properties?.dataLabel === undefined) ctx.setProperty(cell, 'dataLabel', labels[column]);
});
}
},
},
};
}