svelte-bricks 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Masonry.svelte +189 -21
- package/dist/Masonry.svelte.d.ts +20 -6
- package/package.json +20 -24
- package/readme.md +63 -9
package/dist/Masonry.svelte
CHANGED
|
@@ -1,54 +1,221 @@
|
|
|
1
|
-
<script lang="ts">import { flip } from 'svelte/animate';
|
|
1
|
+
<script lang="ts" generics="Item">import { flip } from 'svelte/animate';
|
|
2
2
|
import { fade } from 'svelte/transition';
|
|
3
|
-
|
|
3
|
+
// On non-primitive types, we need a property to tell masonry items apart. The name of this attribute can be customized with idKey which defaults to 'id'. See https://svelte.dev/docs/svelte/each#Keyed-each-blocks.
|
|
4
|
+
let { animate = true, balance = true, calcCols = (masonryWidth, minColWidth, gap) => {
|
|
4
5
|
return Math.min(items.length, Math.floor((masonryWidth + gap) / (minColWidth + gap)) || 1);
|
|
5
|
-
},
|
|
6
|
+
}, duration = 200, gap = 20, getId = (item) => {
|
|
6
7
|
if (typeof item === `number`)
|
|
7
8
|
return item;
|
|
8
9
|
if (typeof item === `string`)
|
|
9
10
|
return item;
|
|
10
11
|
return item[idKey];
|
|
11
|
-
}, idKey = `id`, items, masonryHeight = $bindable(0), masonryWidth = $bindable(0), maxColWidth = 500, minColWidth = 330,
|
|
12
|
-
|
|
12
|
+
}, idKey = `id`, items, masonryHeight = $bindable(0), masonryWidth = $bindable(0), maxColWidth = 500, minColWidth = 330, columnStyle = ``, columnClass = ``, children, div = $bindable(),
|
|
13
|
+
// Virtualization props
|
|
14
|
+
virtualize = false, getEstimatedHeight = undefined, overscan = 5, height = undefined, ...rest } = $props();
|
|
15
|
+
// Height tracking for column balancing and virtualization
|
|
16
|
+
let item_heights = $state(new Map());
|
|
17
|
+
let measured_count = $state(0); // trigger reactivity on height updates
|
|
18
|
+
let measured_sum = $state(0); // running sum for average calculation
|
|
19
|
+
let avg_measured_height = $derived(measured_count > 0 ? measured_sum / measured_count : null);
|
|
20
|
+
// Clean up stale heights when items change (prevents memory leak)
|
|
21
|
+
$effect(() => {
|
|
22
|
+
const current_ids = new Set(items.map(getId));
|
|
23
|
+
let removed_sum = 0;
|
|
24
|
+
for (const [id, height] of item_heights.entries()) {
|
|
25
|
+
if (!current_ids.has(id)) {
|
|
26
|
+
removed_sum += height;
|
|
27
|
+
item_heights.delete(id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (removed_sum > 0) {
|
|
31
|
+
measured_sum -= removed_sum;
|
|
32
|
+
measured_count = item_heights.size;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// Unified height getter with fallback chain
|
|
36
|
+
const get_height = (item) => {
|
|
37
|
+
const id = getId(item);
|
|
38
|
+
// 1. Actual measured height (most accurate)
|
|
39
|
+
const measured = item_heights.get(id);
|
|
40
|
+
if (measured !== undefined)
|
|
41
|
+
return measured;
|
|
42
|
+
// 2. User-provided estimate (if custom function provided)
|
|
43
|
+
if (getEstimatedHeight)
|
|
44
|
+
return getEstimatedHeight(item);
|
|
45
|
+
// 3. Average of measured items
|
|
46
|
+
if (avg_measured_height)
|
|
47
|
+
return avg_measured_height;
|
|
48
|
+
// 4. Hard fallback
|
|
49
|
+
return 150;
|
|
50
|
+
};
|
|
51
|
+
// Measure item heights via ResizeObserver
|
|
52
|
+
const measure_height = (node, item_id) => {
|
|
53
|
+
if (!balance && !virtualize)
|
|
54
|
+
return {};
|
|
55
|
+
const observer = new ResizeObserver(() => {
|
|
56
|
+
const new_height = node.offsetHeight;
|
|
57
|
+
if (new_height > 0 && item_heights.get(item_id) !== new_height) {
|
|
58
|
+
const old_height = item_heights.get(item_id) ?? 0;
|
|
59
|
+
measured_sum += new_height - old_height;
|
|
60
|
+
item_heights.set(item_id, new_height);
|
|
61
|
+
measured_count = item_heights.size;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
observer.observe(node);
|
|
65
|
+
return { destroy: () => observer.disconnect() };
|
|
66
|
+
};
|
|
67
|
+
// Derive if we have enough measurements
|
|
68
|
+
let can_balance = $derived(balance && measured_count >= items.length);
|
|
69
|
+
// Distribute items to shortest column (uses get_height for estimates when not fully measured)
|
|
70
|
+
function balance_to_cols(num_cols) {
|
|
71
|
+
const cols = Array.from({ length: num_cols }, () => []);
|
|
72
|
+
const heights = Array(num_cols).fill(0);
|
|
73
|
+
for (const [idx, item] of items.entries()) {
|
|
74
|
+
const shortest = heights.indexOf(Math.min(...heights));
|
|
75
|
+
cols[shortest].push([item, idx]);
|
|
76
|
+
heights[shortest] += get_height(item) + gap;
|
|
77
|
+
}
|
|
78
|
+
return cols;
|
|
79
|
+
}
|
|
13
80
|
$effect.pre(() => {
|
|
14
81
|
if (maxColWidth < minColWidth) {
|
|
15
82
|
console.warn(`svelte-bricks: maxColWidth (${maxColWidth}) < minColWidth (${minColWidth}).`);
|
|
16
83
|
}
|
|
17
84
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
85
|
+
// CSS container queries hide excess columns for CLS-free SSR
|
|
86
|
+
// When masonryWidth is 0 (SSR), calculate max cols for 1920px viewport
|
|
87
|
+
let nCols = $derived(calcCols(masonryWidth || 1920, minColWidth, gap));
|
|
88
|
+
// Container query rules: breakpoint(n) = (minColWidth + gap) * n - gap
|
|
89
|
+
let container_query_css = $derived(Array.from({ length: nCols - 1 }, (_, idx) => {
|
|
90
|
+
const col = idx + 1;
|
|
91
|
+
const max_w = (minColWidth + gap) * (col + 1) - gap - 1;
|
|
92
|
+
const min_w = col === 1
|
|
93
|
+
? ``
|
|
94
|
+
: `(min-width: ${(minColWidth + gap) * col - gap}px) and `;
|
|
95
|
+
return `@container ${min_w}(max-width: ${max_w}px) { .masonry > .col:nth-child(n+${col + 1}) { display: none; } }`;
|
|
96
|
+
}).join(`\n`));
|
|
97
|
+
// Balanced distribution when measured, naive round-robin for SSR
|
|
98
|
+
let itemsToCols = $derived.by(() => {
|
|
99
|
+
if (can_balance)
|
|
100
|
+
return balance_to_cols(nCols);
|
|
101
|
+
// SSR/initial: round-robin distribution
|
|
102
|
+
return items.reduce((cols, item, idx) => (cols[idx % nCols].push([item, idx]), cols), Array.from({ length: nCols }, () => []));
|
|
103
|
+
});
|
|
104
|
+
// Virtualization logic
|
|
105
|
+
// Warn if virtualize=true but no height provided (only once)
|
|
106
|
+
let warned_missing_height = false;
|
|
107
|
+
$effect.pre(() => {
|
|
108
|
+
if (virtualize && height === undefined && !warned_missing_height) {
|
|
109
|
+
warned_missing_height = true;
|
|
110
|
+
console.warn(`svelte-bricks: virtualize=true requires a height prop. Falling back to 400px.`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// Binary search: find first index where arr[i] >= target
|
|
114
|
+
function binary_search_ge(arr, target) {
|
|
115
|
+
let lo = 0;
|
|
116
|
+
let hi = arr.length;
|
|
117
|
+
while (lo < hi) {
|
|
118
|
+
const mid = (lo + hi) >>> 1;
|
|
119
|
+
if (arr[mid] < target)
|
|
120
|
+
lo = mid + 1;
|
|
121
|
+
else
|
|
122
|
+
hi = mid;
|
|
123
|
+
}
|
|
124
|
+
return lo;
|
|
125
|
+
}
|
|
126
|
+
// Prefix height arrays per column: prefix_heights[col][i] = cumulative height of items 0..i
|
|
127
|
+
let prefix_heights = $derived(itemsToCols.map((col) => {
|
|
128
|
+
let sum = 0;
|
|
129
|
+
return col.map(([item]) => {
|
|
130
|
+
sum += get_height(item) + gap;
|
|
131
|
+
return sum;
|
|
132
|
+
});
|
|
133
|
+
}));
|
|
134
|
+
// Total height per column (for padding calculation)
|
|
135
|
+
let col_total_heights = $derived(prefix_heights.map((ph) => ph.at(-1) ?? 0));
|
|
136
|
+
// Scroll state with requestAnimationFrame throttling
|
|
137
|
+
let scroll_top = $state(0);
|
|
138
|
+
let ticking = false;
|
|
139
|
+
function on_scroll(event) {
|
|
140
|
+
if (ticking)
|
|
141
|
+
return;
|
|
142
|
+
ticking = true;
|
|
143
|
+
requestAnimationFrame(() => {
|
|
144
|
+
scroll_top = event.target.scrollTop;
|
|
145
|
+
ticking = false;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Container height for virtualization viewport
|
|
149
|
+
// For numeric height, use directly; for string (CSS units like "80vh"), use measured clientHeight
|
|
150
|
+
let container_height = $derived(typeof height === `number` ? height : masonryHeight || 400);
|
|
151
|
+
// Only enable virtualization once we have a valid container height measurement
|
|
152
|
+
// This prevents flicker when using CSS units like "80vh" that need DOM measurement
|
|
153
|
+
let can_virtualize = $derived(virtualize && (typeof height === `number` || masonryHeight > 0));
|
|
154
|
+
// Visible ranges per column: [start_idx, end_idx]
|
|
155
|
+
let visible_ranges = $derived(can_virtualize
|
|
156
|
+
? prefix_heights.map((ph) => {
|
|
157
|
+
const start = Math.max(0, binary_search_ge(ph, scroll_top) - 1 - overscan);
|
|
158
|
+
const end = Math.min(ph.length, binary_search_ge(ph, scroll_top + container_height) + overscan);
|
|
159
|
+
return [start, end];
|
|
160
|
+
})
|
|
161
|
+
: itemsToCols.map((col) => [0, col.length]));
|
|
162
|
+
// Padding to replace culled items (only when actively virtualizing)
|
|
163
|
+
const zero_padding = () => itemsToCols.map(() => 0);
|
|
164
|
+
let col_padding_top = $derived(can_virtualize
|
|
165
|
+
? visible_ranges.map(([start], idx) => (start > 0 ? prefix_heights[idx][start - 1] : 0))
|
|
166
|
+
: zero_padding());
|
|
167
|
+
let col_padding_bottom = $derived(can_virtualize
|
|
168
|
+
? visible_ranges.map(([, end], idx) => {
|
|
169
|
+
const visible_end = end > 0 ? (prefix_heights[idx][end - 1] ?? 0) : 0;
|
|
170
|
+
return Math.max(0, col_total_heights[idx] - visible_end);
|
|
171
|
+
})
|
|
172
|
+
: zero_padding());
|
|
173
|
+
// Auto-disable animations when actively virtualizing (FLIP doesn't work well)
|
|
174
|
+
let effective_animate = $derived(animate && !can_virtualize);
|
|
23
175
|
</script>
|
|
24
176
|
|
|
177
|
+
<!-- Dynamic container query styles for CLS-free SSR -->
|
|
178
|
+
<svelte:element this={`style`}>{container_query_css}</svelte:element>
|
|
179
|
+
|
|
25
180
|
<!-- deno-fmt-ignore -->
|
|
26
181
|
<div
|
|
27
|
-
class="masonry {className}"
|
|
28
182
|
bind:clientWidth={masonryWidth}
|
|
29
183
|
bind:clientHeight={masonryHeight}
|
|
30
184
|
bind:this={div}
|
|
31
|
-
|
|
185
|
+
onscroll={virtualize ? on_scroll : undefined}
|
|
186
|
+
style:gap="{gap}px"
|
|
187
|
+
style:overflow-y={virtualize ? `auto` : undefined}
|
|
188
|
+
style:height={virtualize ? (typeof height === `number` ? `${height}px` : height ?? `400px`) : undefined}
|
|
189
|
+
{...rest}
|
|
190
|
+
class="masonry {rest.class ?? ``}"
|
|
32
191
|
>
|
|
33
|
-
{#each itemsToCols as col,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
192
|
+
{#each itemsToCols as col, col_idx}
|
|
193
|
+
{@const [start, end] = visible_ranges[col_idx]}
|
|
194
|
+
{@const visible_items = col.slice(start, end)}
|
|
195
|
+
<div
|
|
196
|
+
class="col col-{col_idx} {columnClass}"
|
|
197
|
+
style="gap: {gap}px; max-width: {maxColWidth}px;{can_virtualize ? ` padding-top: ${col_padding_top[col_idx]}px; padding-bottom: ${col_padding_bottom[col_idx]}px;` : ``} {columnStyle}"
|
|
198
|
+
>
|
|
199
|
+
{#if effective_animate}
|
|
200
|
+
{#each visible_items as [item, item_idx] (getId(item))}
|
|
37
201
|
<div
|
|
202
|
+
use:measure_height={getId(item)}
|
|
38
203
|
in:fade={{ delay: 100, duration }}
|
|
39
204
|
out:fade={{ delay: 0, duration }}
|
|
40
205
|
animate:flip={{ duration }}
|
|
41
206
|
>
|
|
42
|
-
{#if children}{@render children({ idx, item })}{:else}
|
|
207
|
+
{#if children}{@render children({ idx: item_idx, item })}{:else}
|
|
43
208
|
<span>{item}</span>
|
|
44
209
|
{/if}
|
|
45
210
|
</div>
|
|
46
211
|
{/each}
|
|
47
212
|
{:else}
|
|
48
|
-
{#each
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
213
|
+
{#each visible_items as [item, item_idx] (getId(item))}
|
|
214
|
+
<div use:measure_height={getId(item)}>
|
|
215
|
+
{#if children}{@render children({ idx: item_idx, item })}{:else}
|
|
216
|
+
<span>{item}</span>
|
|
217
|
+
{/if}
|
|
218
|
+
</div>
|
|
52
219
|
{/each}
|
|
53
220
|
{/if}
|
|
54
221
|
</div>
|
|
@@ -57,6 +224,7 @@ let itemsToCols = $derived(items.reduce((cols, item, idx) => {
|
|
|
57
224
|
|
|
58
225
|
<style>
|
|
59
226
|
:where(div.masonry) {
|
|
227
|
+
container-type: inline-size;
|
|
60
228
|
display: flex;
|
|
61
229
|
justify-content: center;
|
|
62
230
|
overflow-wrap: anywhere;
|
package/dist/Masonry.svelte.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
declare function $$render<Item>(): {
|
|
4
|
+
props: Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
4
5
|
animate?: boolean;
|
|
6
|
+
balance?: boolean;
|
|
5
7
|
calcCols?: (masonryWidth: number, minColWidth: number, gap: number) => number;
|
|
6
|
-
columnClass?: string;
|
|
7
8
|
duration?: number;
|
|
8
9
|
gap?: number;
|
|
9
10
|
getId?: (item: Item) => string | number;
|
|
@@ -15,15 +16,28 @@ declare class __sveltets_Render<Item> {
|
|
|
15
16
|
minColWidth?: number;
|
|
16
17
|
style?: string;
|
|
17
18
|
class?: string;
|
|
19
|
+
columnStyle?: string;
|
|
20
|
+
columnClass?: string;
|
|
18
21
|
children?: Snippet<[{
|
|
19
22
|
idx: number;
|
|
20
23
|
item: Item;
|
|
21
24
|
}]>;
|
|
22
25
|
div?: HTMLDivElement;
|
|
26
|
+
virtualize?: boolean;
|
|
27
|
+
getEstimatedHeight?: (item: Item) => number;
|
|
28
|
+
overscan?: number;
|
|
29
|
+
height?: number | string;
|
|
23
30
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
exports: {};
|
|
32
|
+
bindings: "masonryHeight" | "masonryWidth" | "div";
|
|
33
|
+
slots: {};
|
|
34
|
+
events: {};
|
|
35
|
+
};
|
|
36
|
+
declare class __sveltets_Render<Item> {
|
|
37
|
+
props(): ReturnType<typeof $$render<Item>>['props'];
|
|
38
|
+
events(): ReturnType<typeof $$render<Item>>['events'];
|
|
39
|
+
slots(): ReturnType<typeof $$render<Item>>['slots'];
|
|
40
|
+
bindings(): "masonryHeight" | "masonryWidth" | "div";
|
|
27
41
|
exports(): {};
|
|
28
42
|
}
|
|
29
43
|
interface $$IsomorphicComponent {
|
package/package.json
CHANGED
|
@@ -1,40 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-bricks",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Svelte masonry component with SSR support and column balancing",
|
|
4
4
|
"author": "Janosh Riebesell <janosh.riebesell@gmail.com>",
|
|
5
5
|
"homepage": "https://janosh.github.io/svelte-bricks",
|
|
6
6
|
"repository": "https://github.com/janosh/svelte-bricks",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.4.0",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "./dist/index.js",
|
|
11
11
|
"bugs": "https://github.com/janosh/svelte-bricks/issues",
|
|
12
|
-
"scripts": {
|
|
13
|
-
"dev": "vite dev",
|
|
14
|
-
"build": "vite build",
|
|
15
|
-
"preview": "vite preview",
|
|
16
|
-
"package": "svelte-package",
|
|
17
|
-
"serve": "vite build && vite preview",
|
|
18
|
-
"check": "svelte-check --ignore dist",
|
|
19
|
-
"test": "vitest test",
|
|
20
|
-
"changelog": "npx auto-changelog --package --output changelog.md --hide-credit --commit-limit false"
|
|
21
|
-
},
|
|
22
12
|
"dependencies": {
|
|
23
|
-
"svelte": "^5.
|
|
13
|
+
"svelte": "^5.46.1"
|
|
24
14
|
},
|
|
25
15
|
"devDependencies": {
|
|
26
|
-
"@sveltejs/adapter-static": "^3.0.
|
|
27
|
-
"@sveltejs/kit": "^2.
|
|
28
|
-
"@sveltejs/package": "^2.
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
16
|
+
"@sveltejs/adapter-static": "^3.0.10",
|
|
17
|
+
"@sveltejs/kit": "^2.49.3",
|
|
18
|
+
"@sveltejs/package": "^2.5.7",
|
|
19
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
20
|
+
"jsdom": "^27.4.0",
|
|
21
|
+
"mdsvex": "^0.12.6",
|
|
22
|
+
"svelte-check": "^4.3.5",
|
|
23
|
+
"svelte-multiselect": "^11.5.1",
|
|
32
24
|
"svelte-preprocess": "^6.0.3",
|
|
33
|
-
"svelte-toc": "^0.
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"vitest": "^3.0.8"
|
|
25
|
+
"svelte-toc": "^0.6.2",
|
|
26
|
+
"svelte2tsx": "^0.7.46",
|
|
27
|
+
"vite": "^7.3.1",
|
|
28
|
+
"vitest": "^4.0.16"
|
|
38
29
|
},
|
|
39
30
|
"keywords": [
|
|
40
31
|
"svelte",
|
|
@@ -57,6 +48,11 @@
|
|
|
57
48
|
"default": "./dist/index.js"
|
|
58
49
|
}
|
|
59
50
|
},
|
|
51
|
+
"prettier": {
|
|
52
|
+
"semi": false,
|
|
53
|
+
"singleQuote": true,
|
|
54
|
+
"printWidth": 90
|
|
55
|
+
},
|
|
60
56
|
"types": "./dist/index.d.ts",
|
|
61
57
|
"files": [
|
|
62
58
|
"dist"
|
package/readme.md
CHANGED
|
@@ -10,19 +10,18 @@
|
|
|
10
10
|
[](https://github.com/janosh/svelte-bricks/actions/workflows/test.yml)
|
|
11
11
|
[](https://npmjs.com/package/svelte-bricks)
|
|
12
12
|
[](https://github.com/janosh/svelte-bricks/actions/workflows/gh-pages.yml)
|
|
13
|
-
[](https://results.pre-commit.ci/latest/github/janosh/svelte-bricks/main)
|
|
14
13
|
[](https://stackblitz.com/github/janosh/svelte-bricks)
|
|
15
14
|
|
|
16
15
|
</h4>
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
Svelte masonry component with SSR support (via CSS container queries) and automatic column balancing. **[Live demo](https://janosh.github.io/svelte-bricks)**
|
|
19
18
|
|
|
20
19
|
</div>
|
|
21
20
|
|
|
22
21
|
## Installation
|
|
23
22
|
|
|
24
23
|
```sh
|
|
25
|
-
|
|
24
|
+
pnpm add -D svelte-bricks
|
|
26
25
|
```
|
|
27
26
|
|
|
28
27
|
## Usage
|
|
@@ -47,7 +46,8 @@ Masonry size: <span>{width}px</span> × <span>{height}px</span> (w ×
|
|
|
47
46
|
{minColWidth}
|
|
48
47
|
{maxColWidth}
|
|
49
48
|
{gap}
|
|
50
|
-
|
|
49
|
+
style="padding: 20px;"
|
|
50
|
+
columnStyle="background-color: rgba(0, 0, 0, 0.1);"
|
|
51
51
|
bind:masonryWidth={width}
|
|
52
52
|
bind:masonryHeight={height}
|
|
53
53
|
>
|
|
@@ -57,9 +57,7 @@ Masonry size: <span>{width}px</span> × <span>{height}px</span> (w ×
|
|
|
57
57
|
</Masonry>
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
**Note**: If `items` is an array of objects, this component tries to access an `id` property on each item. This value is used to tell items apart in the keyed `{#each}` block that creates the masonry layout. Without it, Svelte could not avoid duplicates when new items are added or existing ones rearranged. Read the [Svelte docs](https://svelte.dev/
|
|
61
|
-
|
|
62
|
-
**Hint**: Balanced columns can be achieved even with this simple implementation if masonry items are allowed to stretch to the column height.
|
|
60
|
+
**Note**: If `items` is an array of objects, this component tries to access an `id` property on each item. This value is used to tell items apart in the keyed `{#each}` block that creates the masonry layout. Without it, Svelte could not avoid duplicates when new items are added or existing ones rearranged. Read the [Svelte docs](https://svelte.dev/docs/svelte/each#Keyed-each-blocks) for details. To change the name of the identifier key, pass `idKey="some-uniq-key`. Or pass a function `getId = (item: Item) => string | number` that maps items to unique IDs.
|
|
63
61
|
|
|
64
62
|
## Props
|
|
65
63
|
|
|
@@ -71,7 +69,13 @@ Additional optional props are:
|
|
|
71
69
|
animate: boolean = true
|
|
72
70
|
```
|
|
73
71
|
|
|
74
|
-
Whether to [FLIP-animate](https://svelte.dev/
|
|
72
|
+
Whether to [FLIP-animate](https://svelte.dev/docs/svelte/svelte-animate) masonry items when viewport resizing or other events cause `items` to rearrange.
|
|
73
|
+
|
|
74
|
+
1. ```ts
|
|
75
|
+
balance: boolean = true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Enable height-based column balancing. Items are distributed to the shortest column for a more even layout. Set to `false` for simple round-robin distribution.
|
|
75
79
|
|
|
76
80
|
1. ```ts
|
|
77
81
|
calcCols = (
|
|
@@ -132,7 +136,7 @@ Additional optional props are:
|
|
|
132
136
|
items: Item[]
|
|
133
137
|
```
|
|
134
138
|
|
|
135
|
-
The only required prop
|
|
139
|
+
The only required prop is the list of items to render where `Item` is a generic type (via `generics="Item"`) which usually will be `object` but can also be simple types `string` or `number`.
|
|
136
140
|
|
|
137
141
|
1. ```ts
|
|
138
142
|
masonryHeight: number = 0
|
|
@@ -164,6 +168,56 @@ Additional optional props are:
|
|
|
164
168
|
|
|
165
169
|
Inline styles that will be applied to the top-level `div.masonry`.
|
|
166
170
|
|
|
171
|
+
## Virtual Scrolling
|
|
172
|
+
|
|
173
|
+
For large lists (1000+ items), enable virtual scrolling to render only visible items:
|
|
174
|
+
|
|
175
|
+
```svelte
|
|
176
|
+
<Masonry
|
|
177
|
+
{items}
|
|
178
|
+
virtualize={true}
|
|
179
|
+
height={600}
|
|
180
|
+
getEstimatedHeight={(item) => item.height ?? 150}
|
|
181
|
+
overscan={5}
|
|
182
|
+
>
|
|
183
|
+
{#snippet children({ item })}
|
|
184
|
+
<Card {item} />
|
|
185
|
+
{/snippet}
|
|
186
|
+
</Masonry>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Virtualization Props
|
|
190
|
+
|
|
191
|
+
1. ```ts
|
|
192
|
+
virtualize: boolean = false
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Enable virtual scrolling. When `true`, only visible items are rendered. Requires the `height` prop.
|
|
196
|
+
|
|
197
|
+
1. ```ts
|
|
198
|
+
height: number | string
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Required when `virtualize=true`. Sets the scroll container height (e.g., `500` for pixels or `"80vh"`).
|
|
202
|
+
|
|
203
|
+
1. ```ts
|
|
204
|
+
getEstimatedHeight?: (item: Item) => number
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Optional function that returns an estimated height for items before they're measured. Defaults to 150px if not provided. Better estimates = less layout shift.
|
|
208
|
+
|
|
209
|
+
1. ```ts
|
|
210
|
+
overscan: number = 5
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Number of items to render above and below the visible area. Higher values reduce flicker during fast scrolling but render more items.
|
|
214
|
+
|
|
215
|
+
**Notes:**
|
|
216
|
+
|
|
217
|
+
- FLIP animations are automatically disabled when virtualizing
|
|
218
|
+
- Balance mode works with estimated heights until items are measured
|
|
219
|
+
- The masonry div becomes a scroll container (`overflow-y: auto`)
|
|
220
|
+
|
|
167
221
|
## Styling
|
|
168
222
|
|
|
169
223
|
Besides inline CSS which you can apply through the `style` prop, the following `:global()` CSS selectors can be used for fine-grained control of wrapper and column styles:
|