Problem
ui_table is currently mobile-aware via overflow-x-auto only. That works for short, dense tables (e.g. proc tables on the Application detail page), but it's a weak experience for tables where each row represents a distinct entity people actually read — SSH keys, available package updates, audit events. Users have to swipe to see most of the data, and row-to-row comparison breaks because columns slide off-screen independently.
Different tables want different mobile behavior. Rather than picking one, give ui_table a mobile variant attr so each callsite chooses what suits its data.
Proposed API
attr :mobile, :string, default: "scroll", values: ~w(scroll stack priority)
Three named modes, all on the same component, all opt-in per callsite. Same desktop render across all of them.
mobile="scroll" (default — current behavior)
overflow-x-auto wrapper, full table preserved. Best for short, dense, numeric tables where row-to-row comparison across columns is the actual job (proc tables, anything with tabular-nums).
mobile="stack"
Each <tr> collapses into a card below md. Column headers become field labels inside the card. Best for tables where each row is a distinct entity people read (SSH keys, audit events, updates).
Shape:
bg-card block, radius-box, padding 16
- Vertical layout, ~12px gap between fields
- Each field is label left (
text-tertiary, text-xs uppercase), value right (text-primary, font-mono where appropriate)
- Unlabeled action column docks bottom-right
mobile="priority"
Only the first 1–2 cols + the action col render on mobile; the rest hide behind an expand chevron. Best for wide tables where there's a clear "primary identity" column and the rest is detail (think a list of servers with status badge + name visible, everything else collapsed).
We can ship scroll (default — already works) and stack first, then add priority later if a callsite actually needs it. The values: whitelist makes adding a fourth variant trivial.
Callsite plan (after we ship stack)
| Table |
Variant |
Why |
| Settings → SSH keys |
stack |
One row per key, mostly identifiers, reads naturally as cards |
| Updates tab → packages |
stack |
Each row is a distinct package decision (Apply/skip), card per row clearer than horizontal scroll |
| Application → top by memory/msgq |
scroll (default) |
Dense numeric, comparison matters more than per-row identity |
| Future: server list, audit log |
stack likely |
|
Design check (per CLAUDE.md)
This adds a new layout primitive per the issue-26 protocol — a card-stack mobile row pattern that doesn't exist in components.pen yet. Block on a pen update before implementing.
Suggested pen addition: a Table/Row/Mobile/Stacked reusable in the existing Table Components frame (ff0zV), capturing the shape above. If we ship priority later, it gets its own pen entry too (Table/Row/Mobile/Priority or similar).
Open design/engineering questions
- Action column placement in
stack mode. Unlabeled right-aligned cols (delete, Apply) need a defined slot in the mobile card. Options: top-right corner, bottom of card, or an opt-in mobile_action: true flag on the :col slot to control placement.
- Breakpoint.
md is the natural default but worth confirming with how the rest of the layout shifts (sidebar collapse, page padding).
- Implementation strategy. Pure-CSS via
display: table-row toggling vs. two parallel renders (<table class="hidden md:table"> + <div class="md:hidden">). Two renders is more DOM but keeps semantics clean and is easier to style; CSS-only is leaner but fights <table> defaults. Leaning two-render.
- Future sorting. Out of scope now, but worth flagging: column sorting affordances live in
<th> and don't translate cleanly to label-value cards. A sortable column attr would need a mobile presentation too.
- Migration plan. Default stays
scroll so nothing changes on merge. Per-table opt-in to stack happens in a follow-up PR per the callsite table above.
Acceptance
- Pen file has a stacked-mobile-row reusable in
ff0zV that callers/reviewers can point at
ui_table accepts mobile="scroll" | "stack" (priority deferred unless a callsite needs it)
- Default
mobile="scroll" preserves current behavior — no existing callsite changes
- A second PR flips
stack on for the SSH keys table and the updates tab and visually verifies at 375 / 768 / 1024
mix precommit clean
Problem
ui_tableis currently mobile-aware viaoverflow-x-autoonly. That works for short, dense tables (e.g. proc tables on the Application detail page), but it's a weak experience for tables where each row represents a distinct entity people actually read — SSH keys, available package updates, audit events. Users have to swipe to see most of the data, and row-to-row comparison breaks because columns slide off-screen independently.Different tables want different mobile behavior. Rather than picking one, give
ui_tableamobilevariant attr so each callsite chooses what suits its data.Proposed API
Three named modes, all on the same component, all opt-in per callsite. Same desktop render across all of them.
mobile="scroll"(default — current behavior)overflow-x-autowrapper, full table preserved. Best for short, dense, numeric tables where row-to-row comparison across columns is the actual job (proc tables, anything with tabular-nums).mobile="stack"Each
<tr>collapses into a card belowmd. Column headers become field labels inside the card. Best for tables where each row is a distinct entity people read (SSH keys, audit events, updates).Shape:
bg-cardblock,radius-box, padding 16text-tertiary,text-xs uppercase), value right (text-primary,font-monowhere appropriate)mobile="priority"Only the first 1–2 cols + the action col render on mobile; the rest hide behind an expand chevron. Best for wide tables where there's a clear "primary identity" column and the rest is detail (think a list of servers with status badge + name visible, everything else collapsed).
We can ship
scroll(default — already works) andstackfirst, then addprioritylater if a callsite actually needs it. Thevalues:whitelist makes adding a fourth variant trivial.Callsite plan (after we ship
stack)stackstackscroll(default)stacklikelyDesign check (per CLAUDE.md)
This adds a new layout primitive per the issue-26 protocol — a card-stack mobile row pattern that doesn't exist in
components.penyet. Block on a pen update before implementing.Suggested pen addition: a
Table/Row/Mobile/Stackedreusable in the existing Table Components frame (ff0zV), capturing the shape above. If we shipprioritylater, it gets its own pen entry too (Table/Row/Mobile/Priorityor similar).Open design/engineering questions
stackmode. Unlabeled right-aligned cols (delete, Apply) need a defined slot in the mobile card. Options: top-right corner, bottom of card, or an opt-inmobile_action: trueflag on the:colslot to control placement.mdis the natural default but worth confirming with how the rest of the layout shifts (sidebar collapse, page padding).display: table-rowtoggling vs. two parallel renders (<table class="hidden md:table">+<div class="md:hidden">). Two renders is more DOM but keeps semantics clean and is easier to style; CSS-only is leaner but fights<table>defaults. Leaning two-render.<th>and don't translate cleanly to label-value cards. Asortablecolumn attr would need a mobile presentation too.scrollso nothing changes on merge. Per-table opt-in tostackhappens in a follow-up PR per the callsite table above.Acceptance
ff0zVthat callers/reviewers can point atui_tableacceptsmobile="scroll" | "stack"(priority deferred unless a callsite needs it)mobile="scroll"preserves current behavior — no existing callsite changesstackon for the SSH keys table and the updates tab and visually verifies at 375 / 768 / 1024mix precommitclean