4.3 KiB
Dashboard tables
How every data table on the dashboard is laid out, and the rules a new table
must follow so it never grows a horizontal scrollbar on a desktop screen.
Everything here is plain CSS in src/web/tailwind.css plus four cell classes;
there is no table library and no JavaScript involved.
The problem this solves
A table used to be min-width: max-content inside a .scroll-x wrapper, and
most cells were white-space: nowrap. Any table whose natural width exceeded
the page column (80rem, ~1355px at the site's 110% scale) became a horizontal
scroller, even on a 2560px monitor, and the columns that mattered were hidden
behind the scrollbar. Wrapping columns were squeezed to their minimum while the
nowrap ones kept everything.
The three rules
-
Tables get the whole viewport, everything else keeps the page column.
.dashboardis a CSS grid with two named column spans:content(at most 80rem minus the page gutters, exactly what the header uses) andwide(the viewport minus the same gutters). Every direct child sits incontent; a direct-child.scroll-xsits inwide. The table inside a wide wrapper iswidth: auto(shrink-to-fit) withmin-widthequal to the content column andmax-width: 100%, centred. A table that fits the page column therefore looks exactly as before; a table that needs more grows, centred, up to the viewport edge; the page itself never scrolls sideways. -
Columns wrap before the table scrolls. The
min-width: max-contentrule is gone. A table shrinks by wrapping its text columns down to their floors, and only when the floors alone no longer fit (phones, mostly) does the.scroll-xwrapper scroll. Headers always wrap. -
A cell declares what it holds, and nothing else decides its width. There are exactly four cell classes:
class use it for behaviour (none) short prose, names, tokens, badges wraps at spaces numnumbers, money, counts right-aligned tabular figures; td.numnever wraps,th.nummaycell-tighttimestamps, dates, ids, a single short token that must not split white-space: nowrap. Nothing longer than ~20 characterscell-wraptitles, URLs, notes, messages, badge lists, anything free-form floor 10rem, ceiling 32rem, overflow-wrap: anywhereAnything long that must not wrap gets an inner block with a Tailwind clamp (
<div class="line-clamp-2" title="…">) inside acell-wrapcell; the cell ceiling bounds the column, the clamp bounds the row height. Do not puttruncate/line-clamp-*on atditself (a table cell cannot be a-webkit-box), and do not usetable-fixedon a page-level table (fixed layout needs a definite width and is silently ignored withwidth: auto).
Checklist for a new table
- Wrap it in
<div class="scroll-x">(addtallfor a 70vh vertical cap) and make that wrapper a direct child of<section class="dashboard">if the table has more than a handful of columns. Tables inside.card,.cardsordetailsstay inside their box and just wrap/scroll there. - Give every
tdone of the four classes above according to its content. When in doubt, leave it unclassed: wrapping is the safe default. - Never
cell-tighta name, a title, a reason sentence or a free-text field. - Check the page at 390, 1280 and 2560px. At 1280 and above the wrapper's
scrollWidthmust equal itsclientWidth; at 390 a wide table is allowed to scroll.
Measuring
docs/plans/briefs/dashboard-tables/measure.mjs logs in as the dev-seed admin,
visits every dashboard page at several widths and prints, per page, whether the
document overflows and which table wrappers scroll. Run it against a seeded
dev server (cargo run --example seed_dev_db -- ./dev) before and after a CSS
change; the "scrolling" count for widths ≥ 1280 should be zero.