flowbite-svelte 1.24.0 → 1.25.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/command-palette/CommandPalette.svelte +1 -1
- package/dist/command-palette/CommandPalette.svelte.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/scroll-spy/ScrollSpy.svelte +1 -1
- package/dist/scroll-spy/ScrollSpy.svelte.d.ts +1 -1
- package/dist/split-pane/Divider.svelte +4 -1
- package/dist/split-pane/Divider.svelte.d.ts +1 -0
- package/dist/split-pane/Pane.svelte +9 -1
- package/dist/split-pane/SplitPane.svelte +72 -49
- package/dist/split-pane/SplitPane.svelte.d.ts +1 -0
- package/dist/theme/themes.d.ts +1 -0
- package/dist/theme/themes.js +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/utils/nonPassiveTouch.d.ts +3 -0
- package/dist/utils/nonPassiveTouch.js +8 -0
- package/dist/virtual-masonry/VirtualMasonry.svelte +185 -0
- package/dist/virtual-masonry/VirtualMasonry.svelte.d.ts +44 -0
- package/dist/virtual-masonry/index.d.ts +3 -0
- package/dist/virtual-masonry/index.js +2 -0
- package/dist/virtual-masonry/theme.d.ts +40 -0
- package/dist/virtual-masonry/theme.js +18 -0
- package/package.json +5 -1
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
@component
|
|
195
195
|
[Go to docs](https://flowbite-svelte.com/)
|
|
196
196
|
## Type
|
|
197
|
-
[CommandPaletteProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#
|
|
197
|
+
[CommandPaletteProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L2190)
|
|
198
198
|
## Props
|
|
199
199
|
@prop open = $bindable(false)
|
|
200
200
|
@prop items = []
|
|
@@ -2,7 +2,7 @@ import type { CommandPaletteProps } from "../types";
|
|
|
2
2
|
/**
|
|
3
3
|
* [Go to docs](https://flowbite-svelte.com/)
|
|
4
4
|
* ## Type
|
|
5
|
-
* [CommandPaletteProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#
|
|
5
|
+
* [CommandPaletteProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L2190)
|
|
6
6
|
* ## Props
|
|
7
7
|
* @prop open = $bindable(false)
|
|
8
8
|
* @prop items = []
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -255,7 +255,7 @@
|
|
|
255
255
|
@component
|
|
256
256
|
[Go to docs](https://flowbite-svelte.com/)
|
|
257
257
|
## Type
|
|
258
|
-
[ScrollSpyProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#
|
|
258
|
+
[ScrollSpyProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L2205)
|
|
259
259
|
## Props
|
|
260
260
|
@prop items
|
|
261
261
|
@prop position = "top"
|
|
@@ -2,7 +2,7 @@ import type { ScrollSpyProps } from "../types";
|
|
|
2
2
|
/**
|
|
3
3
|
* [Go to docs](https://flowbite-svelte.com/)
|
|
4
4
|
* ## Type
|
|
5
|
-
* [ScrollSpyProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#
|
|
5
|
+
* [ScrollSpyProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L2205)
|
|
6
6
|
* ## Props
|
|
7
7
|
* @prop items
|
|
8
8
|
* @prop position = "top"
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { divider, dividerHitArea } from "./theme";
|
|
4
4
|
import { getTheme } from "../theme/themeUtils";
|
|
5
5
|
import clsx from "clsx";
|
|
6
|
+
import { nonPassiveTouch } from "../utils/nonPassiveTouch";
|
|
6
7
|
|
|
7
|
-
let { direction, index, onMouseDown, onKeyDown, isDragging, currentSize, class: className = "" }: DividerProps = $props();
|
|
8
|
+
let { direction, index, onMouseDown, onTouchStart, onKeyDown, isDragging, currentSize, class: className = "" }: DividerProps = $props();
|
|
8
9
|
|
|
9
10
|
const themePane = getTheme("divider");
|
|
10
11
|
const themeDividerHitArea = getTheme("dividerHitArea");
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
aria-valuetext={`${roundedSize} percent`}
|
|
27
28
|
class={divider({ direction, isDragging, class: clsx(themePane, className) })}
|
|
28
29
|
onmousedown={(e) => onMouseDown(e, index)}
|
|
30
|
+
use:nonPassiveTouch={(e) => onTouchStart(e, index)}
|
|
29
31
|
onkeydown={(e) => onKeyDown(e, index)}
|
|
30
32
|
>
|
|
31
33
|
<div class={dividerHitArea({ direction, class: clsx(themeDividerHitArea, className) })}></div>
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
@prop direction
|
|
41
43
|
@prop index
|
|
42
44
|
@prop onMouseDown
|
|
45
|
+
@prop onTouchStart
|
|
43
46
|
@prop onKeyDown
|
|
44
47
|
@prop isDragging
|
|
45
48
|
@prop currentSize
|
|
@@ -32,7 +32,15 @@
|
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
{#if showDivider && context}
|
|
35
|
-
<Divider
|
|
35
|
+
<Divider
|
|
36
|
+
{direction}
|
|
37
|
+
index={paneIndex}
|
|
38
|
+
{isDragging}
|
|
39
|
+
currentSize={context.getPaneSize(paneIndex)}
|
|
40
|
+
onMouseDown={context.onMouseDown}
|
|
41
|
+
onTouchStart={context.onTouchStart}
|
|
42
|
+
onKeyDown={context.onKeyDown}
|
|
43
|
+
/>
|
|
36
44
|
{/if}
|
|
37
45
|
|
|
38
46
|
<!--
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
getDirection: () => "horizontal" | "vertical";
|
|
12
12
|
getIsDragging: () => boolean;
|
|
13
13
|
onMouseDown: (e: MouseEvent, index: number) => void;
|
|
14
|
+
onTouchStart: (e: TouchEvent, index: number) => void;
|
|
14
15
|
onKeyDown: (e: KeyboardEvent, index: number) => void;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -106,6 +107,7 @@
|
|
|
106
107
|
getDirection: () => currentDirection,
|
|
107
108
|
getIsDragging: () => isDragging,
|
|
108
109
|
onMouseDown: startResize,
|
|
110
|
+
onTouchStart: startTouchResize,
|
|
109
111
|
onKeyDown: handleKeyResize
|
|
110
112
|
});
|
|
111
113
|
|
|
@@ -281,48 +283,74 @@
|
|
|
281
283
|
document.body.style.userSelect = "";
|
|
282
284
|
}
|
|
283
285
|
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const currentPos = currentDirection === "horizontal" ? e.clientX : e.clientY;
|
|
289
|
-
const delta = currentPos - startPos;
|
|
286
|
+
function startTouchResize(e: TouchEvent, index: number) {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
isDragging = true;
|
|
289
|
+
transition = false;
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
const touch = e.touches[0];
|
|
292
|
+
startPos = currentDirection === "horizontal" ? touch.clientX : touch.clientY;
|
|
292
293
|
|
|
293
|
-
const
|
|
294
|
+
const moveHandler = (ev: TouchEvent) => resizeTouch(ev, index);
|
|
295
|
+
const endHandler = () => stopTouchResize(moveHandler, endHandler);
|
|
294
296
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
window.addEventListener("touchmove", moveHandler, { passive: false });
|
|
298
|
+
window.addEventListener("touchend", endHandler);
|
|
299
|
+
window.addEventListener("touchcancel", endHandler);
|
|
297
300
|
|
|
298
|
-
|
|
301
|
+
document.body.style.userSelect = "none";
|
|
302
|
+
}
|
|
299
303
|
|
|
300
|
-
|
|
301
|
-
|
|
304
|
+
function stopTouchResize(moveHandler: (e: TouchEvent) => void, endHandler: () => void) {
|
|
305
|
+
isDragging = false;
|
|
306
|
+
transition = transitionProp;
|
|
307
|
+
window.removeEventListener("touchmove", moveHandler);
|
|
308
|
+
window.removeEventListener("touchend", endHandler);
|
|
309
|
+
window.removeEventListener("touchcancel", endHandler);
|
|
302
310
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const oldSize2 = sizes[index + 1];
|
|
306
|
-
const totalSize = oldSize1 + oldSize2;
|
|
311
|
+
document.body.style.userSelect = "";
|
|
312
|
+
}
|
|
307
313
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
let newSize2 = oldSize2 - deltaPercent;
|
|
314
|
+
function clampPaneSizes(index: number, targetSize: number, minPercent: number, total: number): boolean {
|
|
315
|
+
if (index < 0 || index + 1 >= sizes.length) return false;
|
|
311
316
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
newSize2 = totalSize - newSize1;
|
|
317
|
+
let newSize1 = Math.min(total - minPercent, Math.max(minPercent, targetSize));
|
|
318
|
+
let newSize2 = total - newSize1;
|
|
315
319
|
|
|
316
|
-
// Check if second pane violates minimum constraint after first pane clamping
|
|
317
320
|
if (newSize2 < minPercent) {
|
|
318
321
|
newSize2 = minPercent;
|
|
319
|
-
newSize1 =
|
|
322
|
+
newSize1 = total - newSize2;
|
|
320
323
|
}
|
|
321
324
|
|
|
322
|
-
|
|
323
|
-
if (Math.abs(newSize1 - oldSize1) > MIN_CHANGE_THRESHOLD) {
|
|
325
|
+
if (Math.abs(newSize1 - sizes[index]) > MIN_CHANGE_THRESHOLD) {
|
|
324
326
|
sizes[index] = newSize1;
|
|
325
327
|
sizes[index + 1] = newSize2;
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function applyResize(currentPos: number, index: number) {
|
|
335
|
+
if (!isDragging || !container) return;
|
|
336
|
+
if (index < 0 || index + 1 >= sizes.length) return;
|
|
337
|
+
|
|
338
|
+
const delta = currentPos - startPos;
|
|
339
|
+
if (Math.abs(delta) < MIN_DELTA) return;
|
|
340
|
+
|
|
341
|
+
const currentContainerSize = currentDirection === "horizontal" ? container.offsetWidth : container.offsetHeight;
|
|
342
|
+
if (currentContainerSize < 1) return;
|
|
343
|
+
|
|
344
|
+
const deltaPercent = (delta / currentContainerSize) * 100;
|
|
345
|
+
const minPercent = (minSize / currentContainerSize) * 100;
|
|
346
|
+
|
|
347
|
+
const oldSize1 = sizes[index];
|
|
348
|
+
const oldSize2 = sizes[index + 1];
|
|
349
|
+
const totalSize = oldSize1 + oldSize2;
|
|
350
|
+
|
|
351
|
+
const targetSize = oldSize1 + deltaPercent;
|
|
352
|
+
|
|
353
|
+
if (clampPaneSizes(index, targetSize, minPercent, totalSize)) {
|
|
326
354
|
startPos = currentPos;
|
|
327
355
|
}
|
|
328
356
|
}
|
|
@@ -331,41 +359,24 @@
|
|
|
331
359
|
if (!container) return;
|
|
332
360
|
if (index < 0 || index + 1 >= sizes.length) return;
|
|
333
361
|
|
|
334
|
-
const step = keyboardStep;
|
|
335
|
-
let handled = false;
|
|
336
|
-
|
|
337
362
|
const isHorizontal = currentDirection === "horizontal";
|
|
338
363
|
const increaseKeys = isHorizontal ? ["ArrowRight"] : ["ArrowDown"];
|
|
339
364
|
const decreaseKeys = isHorizontal ? ["ArrowLeft"] : ["ArrowUp"];
|
|
340
365
|
|
|
341
366
|
const containerSize = isHorizontal ? container.offsetWidth : container.offsetHeight;
|
|
342
|
-
// Bail out if container has zero or near-zero dimensions
|
|
343
367
|
if (containerSize < 1) return;
|
|
344
368
|
|
|
345
369
|
const minPercent = (minSize / containerSize) * 100;
|
|
346
|
-
|
|
347
370
|
const total = sizes[index] + sizes[index + 1];
|
|
348
371
|
|
|
349
|
-
|
|
350
|
-
let newSize1 = Math.min(total - minPercent, Math.max(minPercent, target));
|
|
351
|
-
let newSize2 = total - newSize1;
|
|
352
|
-
|
|
353
|
-
if (newSize2 < minPercent) {
|
|
354
|
-
newSize2 = minPercent;
|
|
355
|
-
newSize1 = total - newSize2;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (Math.abs(newSize1 - sizes[index]) > MIN_CHANGE_THRESHOLD) {
|
|
359
|
-
sizes[index] = newSize1;
|
|
360
|
-
sizes[index + 1] = newSize2;
|
|
361
|
-
handled = true;
|
|
362
|
-
}
|
|
363
|
-
};
|
|
372
|
+
let handled = false;
|
|
364
373
|
|
|
365
374
|
if (increaseKeys.includes(e.key)) {
|
|
366
|
-
|
|
375
|
+
const targetSize = sizes[index] + keyboardStep;
|
|
376
|
+
handled = clampPaneSizes(index, targetSize, minPercent, total);
|
|
367
377
|
} else if (decreaseKeys.includes(e.key)) {
|
|
368
|
-
|
|
378
|
+
const targetSize = sizes[index] - keyboardStep;
|
|
379
|
+
handled = clampPaneSizes(index, targetSize, minPercent, total);
|
|
369
380
|
} else if (e.key === "Enter" || e.key === " ") {
|
|
370
381
|
// Reset to equal sizes
|
|
371
382
|
const equal = 100 / registeredPanes;
|
|
@@ -377,6 +388,18 @@
|
|
|
377
388
|
e.preventDefault();
|
|
378
389
|
}
|
|
379
390
|
}
|
|
391
|
+
|
|
392
|
+
function resize(e: MouseEvent, index: number) {
|
|
393
|
+
const currentPos = currentDirection === "horizontal" ? e.clientX : e.clientY;
|
|
394
|
+
applyResize(currentPos, index);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function resizeTouch(e: TouchEvent, index: number) {
|
|
398
|
+
e.preventDefault(); // Prevent scrolling while dragging
|
|
399
|
+
const touch = e.touches[0];
|
|
400
|
+
const currentPos = currentDirection === "horizontal" ? touch.clientX : touch.clientY;
|
|
401
|
+
applyResize(currentPos, index);
|
|
402
|
+
}
|
|
380
403
|
</script>
|
|
381
404
|
|
|
382
405
|
<div bind:this={container} class={splitpane({ direction: currentDirection, class: clsx(theme, className) })}>
|
|
@@ -6,6 +6,7 @@ interface SplitPaneContext {
|
|
|
6
6
|
getDirection: () => "horizontal" | "vertical";
|
|
7
7
|
getIsDragging: () => boolean;
|
|
8
8
|
onMouseDown: (e: MouseEvent, index: number) => void;
|
|
9
|
+
onTouchStart: (e: TouchEvent, index: number) => void;
|
|
9
10
|
onKeyDown: (e: KeyboardEvent, index: number) => void;
|
|
10
11
|
}
|
|
11
12
|
export declare function setSplitPaneContext(ctx: SplitPaneContext): void;
|
package/dist/theme/themes.d.ts
CHANGED
|
@@ -74,3 +74,4 @@ export { kanbanBoard, kanbanCard } from "../kanban/theme";
|
|
|
74
74
|
export { splitpane, pane, divider, dividerHitArea } from "../split-pane/theme";
|
|
75
75
|
export { tour } from "../tour/theme";
|
|
76
76
|
export { scrollspy } from "../scroll-spy/theme";
|
|
77
|
+
export { virtualMasonry } from "../virtual-masonry/theme";
|
package/dist/theme/themes.js
CHANGED
|
@@ -81,3 +81,4 @@ export { kanbanBoard, kanbanCard } from "../kanban/theme";
|
|
|
81
81
|
export { splitpane, pane, divider, dividerHitArea } from "../split-pane/theme";
|
|
82
82
|
export { tour } from "../tour/theme";
|
|
83
83
|
export { scrollspy } from "../scroll-spy/theme";
|
|
84
|
+
export { virtualMasonry } from "../virtual-masonry/theme";
|
package/dist/types.d.ts
CHANGED
|
@@ -1815,6 +1815,7 @@ export interface DividerProps {
|
|
|
1815
1815
|
direction: "horizontal" | "vertical";
|
|
1816
1816
|
index: number;
|
|
1817
1817
|
onMouseDown: (e: MouseEvent, index: number) => void;
|
|
1818
|
+
onTouchStart: (e: TouchEvent, index: number) => void;
|
|
1818
1819
|
onKeyDown: (e: KeyboardEvent, index: number) => void;
|
|
1819
1820
|
isDragging: boolean;
|
|
1820
1821
|
class?: string;
|
|
@@ -1882,3 +1883,17 @@ export interface ScrollSpyProps extends ScrollSpyVariants, HTMLAttributes<HTMLEl
|
|
|
1882
1883
|
/** Callback when navigation item is clicked */
|
|
1883
1884
|
onNavigate?: (itemId: string) => void;
|
|
1884
1885
|
}
|
|
1886
|
+
import type { VirtualMasonryVariants } from "./virtual-masonry/theme";
|
|
1887
|
+
export interface VirtualMasonryProps<T = unknown> extends VirtualMasonryVariants, Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
1888
|
+
children: Snippet<[item: T, index: number]>;
|
|
1889
|
+
items?: T[];
|
|
1890
|
+
columns?: number;
|
|
1891
|
+
gap?: number;
|
|
1892
|
+
height?: number;
|
|
1893
|
+
overscan?: number;
|
|
1894
|
+
getItemHeight?: (item: T, index: number) => number;
|
|
1895
|
+
scrollToIndex?: (fn: (index: number) => void) => void;
|
|
1896
|
+
contained?: boolean;
|
|
1897
|
+
ariaLabel?: string;
|
|
1898
|
+
class?: ClassValue | null;
|
|
1899
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import type { VirtualMasonryProps } from "../types";
|
|
3
|
+
import { virtualMasonry } from "./theme";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { getTheme } from "../theme/themeUtils";
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
items = [],
|
|
9
|
+
columns = 3,
|
|
10
|
+
gap = 16,
|
|
11
|
+
height = 600,
|
|
12
|
+
overscan = 200,
|
|
13
|
+
getItemHeight,
|
|
14
|
+
scrollToIndex,
|
|
15
|
+
children,
|
|
16
|
+
ariaLabel = "Virtual masonry grid",
|
|
17
|
+
class: className,
|
|
18
|
+
classes,
|
|
19
|
+
contained = false
|
|
20
|
+
}: VirtualMasonryProps<T> = $props();
|
|
21
|
+
|
|
22
|
+
const theme = getTheme("virtualMasonry");
|
|
23
|
+
|
|
24
|
+
let container: HTMLDivElement | undefined;
|
|
25
|
+
let containerWidth = $state(0);
|
|
26
|
+
let scrollTop = $state(0);
|
|
27
|
+
let rafId: number | undefined;
|
|
28
|
+
|
|
29
|
+
const styles = virtualMasonry({ contained });
|
|
30
|
+
|
|
31
|
+
const containStyle = $derived.by(() => {
|
|
32
|
+
if (!contained) return "";
|
|
33
|
+
const itemClasses = clsx(classes?.item);
|
|
34
|
+
const hasCustomContain = /\[contain:[^\]]+\]/.test(itemClasses);
|
|
35
|
+
return hasCustomContain ? "" : "contain: layout style paint;";
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Calculate column width based on container width
|
|
39
|
+
const columnWidth = $derived.by(() => {
|
|
40
|
+
if (containerWidth === 0) return 0;
|
|
41
|
+
return (containerWidth - gap * (columns - 1)) / columns;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Position items in columns
|
|
45
|
+
interface PositionedItem {
|
|
46
|
+
item: T;
|
|
47
|
+
index: number;
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
height: number;
|
|
51
|
+
column: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const positionedItems = $derived.by((): PositionedItem[] => {
|
|
55
|
+
if (columnWidth === 0) return [];
|
|
56
|
+
|
|
57
|
+
const columnHeights = new Array(columns).fill(0);
|
|
58
|
+
const positioned: PositionedItem[] = [];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < items.length; i++) {
|
|
61
|
+
// Find shortest column
|
|
62
|
+
const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
|
|
63
|
+
const itemHeight = getItemHeight ? getItemHeight(items[i], i) : 200;
|
|
64
|
+
|
|
65
|
+
positioned.push({
|
|
66
|
+
item: items[i],
|
|
67
|
+
index: i,
|
|
68
|
+
x: shortestColumn * (columnWidth + gap),
|
|
69
|
+
y: columnHeights[shortestColumn],
|
|
70
|
+
height: itemHeight,
|
|
71
|
+
column: shortestColumn
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
columnHeights[shortestColumn] += itemHeight + gap;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return positioned;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Total height is the tallest column
|
|
81
|
+
const totalHeight = $derived.by(() => {
|
|
82
|
+
if (positionedItems.length === 0) return 0;
|
|
83
|
+
return Math.max(...positionedItems.map((item) => item.y + item.height));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Visible items based on scroll position with overscan
|
|
87
|
+
const visibleItems = $derived.by(() => {
|
|
88
|
+
const viewportTop = scrollTop - overscan;
|
|
89
|
+
const viewportBottom = scrollTop + height + overscan;
|
|
90
|
+
|
|
91
|
+
return positionedItems.filter((item) => {
|
|
92
|
+
const itemTop = item.y;
|
|
93
|
+
const itemBottom = item.y + item.height;
|
|
94
|
+
return itemBottom >= viewportTop && itemTop <= viewportBottom;
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Performance optimized scroll handler using RAF
|
|
99
|
+
function handleScroll() {
|
|
100
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
101
|
+
rafId = requestAnimationFrame(() => {
|
|
102
|
+
if (container) scrollTop = container.scrollTop;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Scroll to specific index
|
|
107
|
+
function scrollToIndexImpl(index: number) {
|
|
108
|
+
if (!container || index < 0 || index >= items.length) return;
|
|
109
|
+
const item = positionedItems[index];
|
|
110
|
+
if (item) {
|
|
111
|
+
container.scrollTop = item.y;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Bind scrollToIndex function to parent component
|
|
116
|
+
$effect(() => {
|
|
117
|
+
if (scrollToIndex) {
|
|
118
|
+
scrollToIndex(scrollToIndexImpl);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Measure container width on mount and resize
|
|
123
|
+
$effect(() => {
|
|
124
|
+
if (!container) return;
|
|
125
|
+
|
|
126
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
containerWidth = entry.contentRect.width;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
resizeObserver.observe(container);
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
resizeObserver.disconnect();
|
|
136
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<div
|
|
142
|
+
bind:this={container}
|
|
143
|
+
onscroll={handleScroll}
|
|
144
|
+
role="list"
|
|
145
|
+
aria-label={ariaLabel}
|
|
146
|
+
class={styles.container({ class: clsx(theme?.container, className) })}
|
|
147
|
+
style={`height:${height}px; position:relative;`}
|
|
148
|
+
>
|
|
149
|
+
<div class={styles.spacer({ class: clsx(theme?.spacer, classes?.spacer) })} style={`height:${totalHeight}px;`}>
|
|
150
|
+
<div class={styles.content({ class: clsx(theme?.content, classes?.content) })}>
|
|
151
|
+
{#each visibleItems as { item, index, x, y, height: itemHeight } (index)}
|
|
152
|
+
<div
|
|
153
|
+
role="listitem"
|
|
154
|
+
aria-setsize={items.length}
|
|
155
|
+
aria-posinset={index + 1}
|
|
156
|
+
class={styles.item({ class: clsx(theme?.item, classes?.item) })}
|
|
157
|
+
style={`position:absolute; left:${x}px; top:${y}px; width:${columnWidth}px; height:${itemHeight}px; ${containStyle}`}
|
|
158
|
+
>
|
|
159
|
+
{@render children?.(item, index)}
|
|
160
|
+
</div>
|
|
161
|
+
{/each}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!--
|
|
167
|
+
@component
|
|
168
|
+
VirtualMasonry - Virtualized masonry/pinterest layout for efficient rendering of large image grids
|
|
169
|
+
[Go to docs](https://flowbite-svelte.com/)
|
|
170
|
+
## Type
|
|
171
|
+
[VirtualMasonryProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts)
|
|
172
|
+
## Props
|
|
173
|
+
@prop items = []
|
|
174
|
+
@prop columns = 3
|
|
175
|
+
@prop gap = 16
|
|
176
|
+
@prop height = 600
|
|
177
|
+
@prop overscan = 200
|
|
178
|
+
@prop getItemHeight
|
|
179
|
+
@prop scrollToIndex
|
|
180
|
+
@prop children
|
|
181
|
+
@prop ariaLabel = "Virtual masonry grid"
|
|
182
|
+
@prop contained = false
|
|
183
|
+
@prop class: className
|
|
184
|
+
@prop classes
|
|
185
|
+
-->
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { VirtualMasonryProps } from "../types";
|
|
2
|
+
declare function $$render<T>(): {
|
|
3
|
+
props: VirtualMasonryProps<T>;
|
|
4
|
+
exports: {};
|
|
5
|
+
bindings: "";
|
|
6
|
+
slots: {};
|
|
7
|
+
events: {};
|
|
8
|
+
};
|
|
9
|
+
declare class __sveltets_Render<T> {
|
|
10
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
11
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
12
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
13
|
+
bindings(): "";
|
|
14
|
+
exports(): {};
|
|
15
|
+
}
|
|
16
|
+
interface $$IsomorphicComponent {
|
|
17
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
18
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
19
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
20
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
21
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* VirtualMasonry - Virtualized masonry/pinterest layout for efficient rendering of large image grids
|
|
25
|
+
* [Go to docs](https://flowbite-svelte.com/)
|
|
26
|
+
* ## Type
|
|
27
|
+
* [VirtualMasonryProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts)
|
|
28
|
+
* ## Props
|
|
29
|
+
* @prop items = []
|
|
30
|
+
* @prop columns = 3
|
|
31
|
+
* @prop gap = 16
|
|
32
|
+
* @prop height = 600
|
|
33
|
+
* @prop overscan = 200
|
|
34
|
+
* @prop getItemHeight
|
|
35
|
+
* @prop scrollToIndex
|
|
36
|
+
* @prop children
|
|
37
|
+
* @prop ariaLabel = "Virtual masonry grid"
|
|
38
|
+
* @prop contained = false
|
|
39
|
+
* @prop class: className
|
|
40
|
+
* @prop classes
|
|
41
|
+
*/
|
|
42
|
+
declare const VirtualMasonry: $$IsomorphicComponent;
|
|
43
|
+
type VirtualMasonry<T> = InstanceType<typeof VirtualMasonry<T>>;
|
|
44
|
+
export default VirtualMasonry;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type VariantProps } from "tailwind-variants";
|
|
2
|
+
import type { Classes } from "../theme/themeUtils";
|
|
3
|
+
export declare const virtualMasonry: import("tailwind-variants").TVReturnType<{
|
|
4
|
+
contained: {
|
|
5
|
+
true: {
|
|
6
|
+
item: string;
|
|
7
|
+
};
|
|
8
|
+
false: {};
|
|
9
|
+
};
|
|
10
|
+
}, {
|
|
11
|
+
container: string;
|
|
12
|
+
spacer: string;
|
|
13
|
+
content: string;
|
|
14
|
+
item: string;
|
|
15
|
+
}, undefined, {
|
|
16
|
+
contained: {
|
|
17
|
+
true: {
|
|
18
|
+
item: string;
|
|
19
|
+
};
|
|
20
|
+
false: {};
|
|
21
|
+
};
|
|
22
|
+
}, {
|
|
23
|
+
container: string;
|
|
24
|
+
spacer: string;
|
|
25
|
+
content: string;
|
|
26
|
+
item: string;
|
|
27
|
+
}, import("tailwind-variants").TVReturnType<{
|
|
28
|
+
contained: {
|
|
29
|
+
true: {
|
|
30
|
+
item: string;
|
|
31
|
+
};
|
|
32
|
+
false: {};
|
|
33
|
+
};
|
|
34
|
+
}, {
|
|
35
|
+
container: string;
|
|
36
|
+
spacer: string;
|
|
37
|
+
content: string;
|
|
38
|
+
item: string;
|
|
39
|
+
}, undefined, unknown, unknown, undefined>>;
|
|
40
|
+
export type VirtualMasonryVariants = VariantProps<typeof virtualMasonry> & Classes<typeof virtualMasonry>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { tv } from "tailwind-variants";
|
|
2
|
+
export const virtualMasonry = tv({
|
|
3
|
+
slots: {
|
|
4
|
+
container: "overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent",
|
|
5
|
+
spacer: "relative",
|
|
6
|
+
content: "relative w-full",
|
|
7
|
+
item: ""
|
|
8
|
+
},
|
|
9
|
+
variants: {
|
|
10
|
+
contained: {
|
|
11
|
+
true: { item: "[contain:layout_style_paint]" },
|
|
12
|
+
false: {}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
contained: false
|
|
17
|
+
}
|
|
18
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowbite-svelte",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "Flowbite components for Svelte",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"author": {
|
|
@@ -861,6 +861,10 @@
|
|
|
861
861
|
"types": "./dist/video/Video.svelte.d.ts",
|
|
862
862
|
"svelte": "./dist/video/Video.svelte"
|
|
863
863
|
},
|
|
864
|
+
"./VirtualMasonry.svelte": {
|
|
865
|
+
"types": "./dist/virtual-masonry/VirtualMasonry.svelte.d.ts",
|
|
866
|
+
"svelte": "./dist/virtual-masonry/VirtualMasonry.svelte"
|
|
867
|
+
},
|
|
864
868
|
"./VirtualList.svelte": {
|
|
865
869
|
"types": "./dist/virtuallist/VirtualList.svelte.d.ts",
|
|
866
870
|
"svelte": "./dist/virtuallist/VirtualList.svelte"
|