diff --git a/examples/stackblitz/src/main.ts b/examples/stackblitz/src/main.ts
index 07169fe..bcc0d9a 100644
--- a/examples/stackblitz/src/main.ts
+++ b/examples/stackblitz/src/main.ts
@@ -22,11 +22,36 @@ interface Photo {
/** Deterministic sizes, so the grid looks the same every time it boots. */
const HEIGHTS = [300, 520, 260, 440, 310, 580, 350, 420, 280, 500, 330, 460];
+/**
+ * A generated image, as a data URI.
+ *
+ * Deliberately not a photo service. An external image host makes the example
+ * depend on someone else's uptime, rate limits and CORS policy — and when it is
+ * slow, the very first thing a visitor sees is an empty grid. These render
+ * instantly, work offline, and still exercise the decode path the grid waits on.
+ */
+function artwork(id: number, width: number, height: number): string {
+ const hue = (id * 47) % 360;
+ const svg =
+ ``;
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
+}
+
function makePhoto(id: number): Photo {
const height = HEIGHTS[id % HEIGHTS.length];
return {
id,
- url: `https://picsum.photos/seed/masonry-${id}/400/${height}`,
+ url: artwork(id, 400, height),
title: `Photo ${id}`,
width: 400,
height,
diff --git a/projects/demo/src/app/app.css b/projects/demo/src/app/app.css
index 53dfbef..080607e 100644
--- a/projects/demo/src/app/app.css
+++ b/projects/demo/src/app/app.css
@@ -1,19 +1,12 @@
-:host {
- display: block;
- max-width: 1600px;
- margin: 0 auto;
- padding: 32px 24px 96px;
-}
-
.masthead {
display: flex;
flex-wrap: wrap;
gap: 20px;
- align-items: flex-end;
+ align-items: center;
justify-content: space-between;
- padding-bottom: 18px;
margin-bottom: 26px;
border-bottom: 1px solid var(--line);
+ padding: 10px 10px;
}
.masthead h1 {
@@ -52,3 +45,7 @@ nav a.active {
color: var(--accent);
font-weight: 600;
}
+
+main {
+ padding: 20px 10px;
+}
diff --git a/projects/demo/src/app/example-frame.ts b/projects/demo/src/app/example-frame.ts
new file mode 100644
index 0000000..662b83a
--- /dev/null
+++ b/projects/demo/src/app/example-frame.ts
@@ -0,0 +1,149 @@
+import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
+
+type Panel = 'none' | 'config' | 'code';
+
+/**
+ * The shell every example sits in: a description, a toggle for the controls, a
+ * toggle for the source, and a copy button.
+ *
+ * The preview is never hidden. Tabs that swap the grid out would take it to
+ * `display: none`, where it measures zero and has to lay out again on the way
+ * back — a flicker caused entirely by the demo, in the one place a visitor is
+ * judging whether the layout is steady. So the panels open *above* a preview
+ * that stays mounted, which also lets you read the code and watch the result at
+ * the same time.
+ */
+@Component({
+ selector: 'demo-example',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
- 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.
-
+
+
+ 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 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.
+
- Every button below mutates the source array and nothing else. Item order is derived from the
- DOM at layout time, so a prepend really lands first — the ordering bug that makes other
- masonry wrappers expose a manual reloadItems() cannot occur here.
-
+
+
+ Every button below mutates the source array and nothing else. Item order is derived from the
+ DOM at layout time, so a prepend really lands first — the ordering bug that makes other
+ masonry wrappers expose a manual reloadItems() cannot occur here.
+
- Column count follows the container width through a breakpoint map —
- { xs: 1, sm: 2, md: 3, xl: 4, '2xl': 5 }, named against the default
- scale, and matched against this panel rather than the viewport. Cards with images are held
- back until decode() resolves, so they never land at the wrong height and shove
- their neighbours around.
-
+
+
+ Column count follows the container width through a breakpoint map —
+ { xs: 1, sm: 2, md: 3, xl: 4, '2xl': 5 }, named against the default
+ scale, and matched against this panel rather than the viewport. Cards with images are held
+ back until decode() resolves, so they never land at the wrong height and shove
+ their neighbours around.
+
- Solve time is measured across the whole pass — reading measurements, running the solver and
- writing every transform. Sizes arrive pre-computed from one shared
- ResizeObserver, so a pass performs no forced reflow, and repeated passes with
- identical input return before touching the DOM. Turn on contentVisibility to let
- the browser skip rendering off-screen tiles.
-
+
+
+ Solve time is measured across the whole pass — reading measurements, running the solver and
+ writing every transform. Sizes arrive pre-computed from one shared
+ ResizeObserver, so a pass performs no forced reflow, and repeated passes with
+ identical input return before touching the DOM. Turn on contentVisibility to
+ let the browser skip rendering off-screen tiles.
+
+ }
+
+
`,
})
export class PerformanceExample {
+ readonly code = `
+
+ @for (card of cards(); track card.id) {
+ {{ card.title }}
+ }
+`;
+
readonly count = signal(600);
readonly contentVisibility = signal(true);
readonly transitions = signal(true);
diff --git a/projects/demo/src/app/examples/spans.ts b/projects/demo/src/app/examples/spans.ts
index 8b0df33..c4d5386 100644
--- a/projects/demo/src/app/examples/spans.ts
+++ b/projects/demo/src/app/examples/spans.ts
@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
+import { DemoExample } from '../example-frame';
import { NG_MASONRY_GRID, type MasonryGridOptions } from 'masonry-angular';
import { makeCards, type DemoCard } from './cards';
@@ -9,84 +10,99 @@ import { makeCards, type DemoCard } from './cards';
*/
@Component({
selector: 'spans-example',
- imports: [NG_MASONRY_GRID],
+ imports: [NG_MASONRY_GRID, DemoExample],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
-
- masonryColSpan widens an item across whole columns; the solver drops it into the
- group of columns with the lowest shared top edge. A masonryGridStamp element is
- positioned by you and treated as an obstacle. horizontalOrder switches from
- shortest-column packing to strict row order.
-
+
+
+ masonryColSpan widens an item across whole columns; the solver drops it into
+ the group of columns with the lowest shared top edge. A
+ masonryGridStamp element is positioned by you and treated as an obstacle.
+ horizontalOrder switches from shortest-column packing to strict row order.
+