Back to DevLog

Two-column CSS grid for desk-mode HUD panels

4 min read

Desk mode was stacking every panel full-width in a vertical column. A 5-row list occupied 1920px.

Each monitor became a 2-slot grid

Each .mon is now a 2-slot CSS grid. Panels are placed by a first-fit packer that never reorders the list. The size field in the panel registry already existed as a wall column span. Desk was ignoring it. Now it drives desk width too.

The field accepts a number or {wall: n, desk: n}. Mode-indexing only happens where the two grids genuinely disagree. Today that is git alone. Making git span 2 everywhere would move the wall from a clean 3x2 to a 3x3 with two dead cells.

Packing is first-fit in list order, never reordering. I rejected grid-auto-flow: dense because appending a panel to the end of the list could silently move an earlier one. Holes are real, visible, and mean those two panels did not pair.

The bezel guarantee is structural, not arithmetic. A panel is a child of exactly one .mon. It cannot cross a monitor edge. The 2-slot subdivision happens strictly inside a .mon. No pixel math to get wrong.

Renderers were fixed to match

Widening the cells exposed that every renderer was written when a panel was about as wide as its content. justify-content: space-between and percentage table columns both mean spread to the edges. That was invisible at the old width and was two feet of glass at the new one.

Content is now sized to itself. Leftover width becomes right-edge whitespace. I put width: max-content on .rows, the container, not on each row. On the container, every row stretches to the widest row's natural content and space-between still right-aligns all the values into one clean column. Per-row would give a ragged staircase.

Reminders deliberately did not get max-content. Its text is the widest thing and already ellipsises. Sizing to it would push every other timestamp right. Full width plus a real gap keeps timestamps in a fixed column. This is the panel where the general rule was the wrong answer.

Clock was untouched. It has no label/value pairs at all.

Tables cannot shrink below min-content

A table cannot lay out narrower than the sum of its columns' min-content widths. max-width: 100% cannot rescue that. With td.repo { max-width: 22em }, a 134-char repo name produced a 912px table in a 911px body. The cell caps are what the minimum is made of. I tightened to 17em and added td.branch { 9em }.

Verification

Every panel was measured at both 941px and 1894px. Nothing truncated, nothing overflowing. Every panel's worst pair gap is identical at both widths. The 953px difference lands entirely in right slack. That is the direct proof that content stopped tracking the panel.

Sites' worst pair gap went from 1679px to 97px. Git's table went from 1865px of 1865 available to 677px. Worst inter-column gap went from 541 to 221.

The wall baseline compared 6 panel boxes with worst edge drift 0.000px and 78 inner elements compared edge-for-edge. 13 were skipped because their text changed. All 8 existing suites passed.

The value column's position now depends on the data. It can twitch about 13px when a number gains a digit. I weighed that against a 1679px defect and against the alternative of nine hand-tuned per-panel max-width values that each go stale silently.

Testing details

Element-box measurement reads 0 for a stranded label because a flex: 1 1 auto box grows to touch the thing it was pushed away from. I switched to document.createRange().selectNodeContents() glyph extents, which exposed the real 1679px separation.

A Range reports where text would end if nothing clipped it, so on an ellipsised element it runs past the box. I bounded by the element's own right edge.

The wall baseline was comparing panel boxes only. It would have passed even if I had wrecked the wall git table's internals. I fixed that before making any turn-2 edit. The baseline now compares inner element geometry, text-matched so a moved element is distinguishable from one that merely says something new.

The baseline stores wall geometry only. A stored desk snapshot would either be edited every run, asserting nothing, or go stale, asserting the wrong thing. The desk is checked against invariants instead.

scripts/panel_width_test.py and scripts/desk_grid_test.py both require the control panel running on 127.0.0.1:3334. They drive the live server so panels hold real data. An empty git table is the one shape that cannot spread, so stubbed data would have hidden the defect.

What this does not do

The ordered list still cannot express position, only sequence. A panel cannot be pinned to monitor 2, bottom-right. It lands where the packer puts it given everything before it. That is the known migration cost when drag-to-rearrange arrives.

Share this post