Dashboard
Fixed-size tiles with per-grid breakpoints, each body loaded only when it scrolls into view.
Twenty tiles, each a static size: height in pixels, width in whole columns via masonryColSpan. No body is rendered up front — every one sits behind @defer (on viewport) and loads as its placeholder scrolls into view. Because the tile already owns its height, the swap costs no relayout: the grid positioned the board from the skeletons, and the real content lands in exactly the same box.
Tiles are wider than article cards, so this board also redefines what the breakpoint names mean — breakpoints: { sm: 620, md: 980, … } — while columns still reads as { xs: 1, sm: 2, md: 3, … }. The override merges over the defaults and is scoped to this grid.
<!-- Tiles declare their own size: height in px, width in whole
columns. Because the size is known before the content exists,
each body can sit behind @defer without moving anything. -->
<masonry-grid [columns]="{ xs: 1, sm: 2, md: 3 }"
[options]="{ breakpoints: { sm: 620, md: 980 } }"
gutter="16">
@for (w of widgets(); track w.id) {
<article masonryGridItem [masonryColSpan]="w.span" [style.height.px]="w.height">
<h3>{{ w.title }}</h3>
@defer (on viewport) {
<dashboard-widget [widget]="w" />
} @placeholder {
<div class="skeleton"></div>
}
</article>
}
</masonry-grid>