Pure cell-placement math for grid dashboards.
A grid widget occupies an explicit rectangle of cells — x (column, 0-based),
y (row, 0-based), spanning w×h — on a breakpoint's column grid. Widgets
can sit anywhere (gaps are allowed, that's the point of the grid type) but
never overlap; these helpers are the single source of truth for what fits
where, shared by every placement mutation and by the render-time resolution
of layouts that predate explicit coordinates:
collides?/5— would a rectangle overlap any placed widget?first_free/4— the first free rectangle in reading order (row-major).compact/2— pack a list of spans into explicit cells in list order (legacy/derived layouts: "reflow + compact").fit_size/8— clamp a requested resize so it grows until blocked by a neighbour or the grid edge, never onto another widget.
Everything is integer cell math on plain string-keyed placement maps
(%{"x" => _, "y" => _, "w" => _, "h" => _}) — no DOM, no DB.
Summary
Functions
The first row below every placed widget — the non-overlapping fallback spot
when a packing scan comes up empty (a grid solid through max_rows/0).
Whether a w×h rectangle at (x, y) overlaps any of the others (their
placement maps; entries without both x and y are ignored — they have no
cells yet).
Pack placements (their w/h spans, in list order) into explicit cells on
a cols-wide grid: each gets the first free rectangle in reading order, spans
clamped to the column count. Returns the placements with "x"/"y" set.
The first {x, y} (reading order: row by row, left to right) where a w×h
rectangle fits without overlapping others. Spans wider than the grid are
handled by the caller (clamp first); nil only if the grid is packed solid
through max_rows/0 (practically unreachable).
Clamp a requested req_w×req_h resize of the widget anchored at (x, y) so
it stays within the grid and grows until blocked — the nearest neighbour
(any of others) or the grid edge stops it, instead of overlapping or
rejecting the whole resize. Width is fitted first (at the current height
orig_h), then height at the fitted width; the result never collides because
the original placement doesn't.
The maximum row index + span extent a placement may reach.
Functions
@spec below_all([map()]) :: non_neg_integer()
The first row below every placed widget — the non-overlapping fallback spot
when a packing scan comes up empty (a grid solid through max_rows/0).
@spec collides?(integer(), integer(), pos_integer(), pos_integer(), [map()]) :: boolean()
Whether a w×h rectangle at (x, y) overlaps any of the others (their
placement maps; entries without both x and y are ignored — they have no
cells yet).
@spec compact([map()], pos_integer()) :: [map()]
Pack placements (their w/h spans, in list order) into explicit cells on
a cols-wide grid: each gets the first free rectangle in reading order, spans
clamped to the column count. Returns the placements with "x"/"y" set.
This is the "reflow + compact" primitive: it materializes legacy order-only layouts and derives un-customized breakpoints from a designed tier (sorted to reading order first by the caller).
@spec first_free([map()], pos_integer(), pos_integer(), pos_integer()) :: {non_neg_integer(), non_neg_integer()} | nil
The first {x, y} (reading order: row by row, left to right) where a w×h
rectangle fits without overlapping others. Spans wider than the grid are
handled by the caller (clamp first); nil only if the grid is packed solid
through max_rows/0 (practically unreachable).
@spec fit_size( integer(), integer(), integer(), integer(), pos_integer(), [map()], pos_integer(), {%{w: pos_integer(), h: pos_integer()}, %{w: pos_integer(), h: pos_integer()}} ) :: {pos_integer(), pos_integer()}
Clamp a requested req_w×req_h resize of the widget anchored at (x, y) so
it stays within the grid and grows until blocked — the nearest neighbour
(any of others) or the grid edge stops it, instead of overlapping or
rejecting the whole resize. Width is fitted first (at the current height
orig_h), then height at the fitted width; the result never collides because
the original placement doesn't.
bounds are the widget type's {min, max} size maps.
@spec max_rows() :: pos_integer()
The maximum row index + span extent a placement may reach.