flowbite-svelte 1.24.1 → 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/index.d.ts CHANGED
@@ -84,3 +84,4 @@ export * from "./kanban";
84
84
  export * from "./split-pane";
85
85
  export * from "./tour";
86
86
  export * from "./scroll-spy";
87
+ export * from "./virtual-masonry";
package/dist/index.js CHANGED
@@ -90,3 +90,4 @@ export * from "./kanban";
90
90
  export * from "./split-pane";
91
91
  export * from "./tour";
92
92
  export * from "./scroll-spy";
93
+ export * from "./virtual-masonry";
@@ -3,6 +3,7 @@
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
8
  let { direction, index, onMouseDown, onTouchStart, onKeyDown, isDragging, currentSize, class: className = "" }: DividerProps = $props();
8
9
 
@@ -26,7 +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)}
29
- ontouchstart={(e) => onTouchStart(e, index)}
30
+ use:nonPassiveTouch={(e) => onTouchStart(e, index)}
30
31
  onkeydown={(e) => onKeyDown(e, index)}
31
32
  >
32
33
  <div class={dividerHitArea({ direction, class: clsx(themeDividerHitArea, className) })}></div>
@@ -311,97 +311,46 @@
311
311
  document.body.style.userSelect = "";
312
312
  }
313
313
 
314
- function resize(e: MouseEvent, index: number) {
315
- if (!isDragging || !container) return;
316
- if (index < 0 || index + 1 >= sizes.length) return;
317
-
318
- const currentPos = currentDirection === "horizontal" ? e.clientX : e.clientY;
319
- const delta = currentPos - startPos;
320
-
321
- if (Math.abs(delta) < MIN_DELTA) return; // Ignore very small movements
322
-
323
- const containerSize = currentDirection === "horizontal" ? container.offsetWidth : container.offsetHeight;
324
-
325
- // Bail out if container has zero or near-zero dimensions
326
- if (containerSize < 1) return;
327
-
328
- const deltaPercent = (delta / containerSize) * 100;
329
-
330
- // Calculate min as percentage based on current container size
331
- const minPercent = (minSize / containerSize) * 100;
332
-
333
- // Store original sizes
334
- const oldSize1 = sizes[index];
335
- const oldSize2 = sizes[index + 1];
336
- const totalSize = oldSize1 + oldSize2;
337
-
338
- // Calculate desired new sizes
339
- let newSize1 = oldSize1 + deltaPercent;
340
- 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;
341
316
 
342
- // Apply minimum constraints - clamp to valid range
343
- newSize1 = Math.max(minPercent, newSize1);
344
- newSize2 = totalSize - newSize1;
317
+ let newSize1 = Math.min(total - minPercent, Math.max(minPercent, targetSize));
318
+ let newSize2 = total - newSize1;
345
319
 
346
- // Check if second pane violates minimum constraint after first pane clamping
347
320
  if (newSize2 < minPercent) {
348
321
  newSize2 = minPercent;
349
- newSize1 = totalSize - newSize2;
322
+ newSize1 = total - newSize2;
350
323
  }
351
324
 
352
- // Only update if changed significantly
353
- if (Math.abs(newSize1 - oldSize1) > MIN_CHANGE_THRESHOLD) {
325
+ if (Math.abs(newSize1 - sizes[index]) > MIN_CHANGE_THRESHOLD) {
354
326
  sizes[index] = newSize1;
355
327
  sizes[index + 1] = newSize2;
356
- startPos = currentPos;
328
+ return true;
357
329
  }
330
+
331
+ return false;
358
332
  }
359
333
 
360
- function resizeTouch(e: TouchEvent, index: number) {
334
+ function applyResize(currentPos: number, index: number) {
361
335
  if (!isDragging || !container) return;
362
336
  if (index < 0 || index + 1 >= sizes.length) return;
363
337
 
364
- e.preventDefault(); // Prevent scrolling while dragging
365
-
366
- const touch = e.touches[0];
367
- const currentPos = currentDirection === "horizontal" ? touch.clientX : touch.clientY;
368
338
  const delta = currentPos - startPos;
339
+ if (Math.abs(delta) < MIN_DELTA) return;
369
340
 
370
- if (Math.abs(delta) < MIN_DELTA) return; // Ignore very small movements
371
-
372
- const containerSize = currentDirection === "horizontal" ? container.offsetWidth : container.offsetHeight;
373
-
374
- // Bail out if container has zero or near-zero dimensions
375
- if (containerSize < 1) return;
376
-
377
- const deltaPercent = (delta / containerSize) * 100;
341
+ const currentContainerSize = currentDirection === "horizontal" ? container.offsetWidth : container.offsetHeight;
342
+ if (currentContainerSize < 1) return;
378
343
 
379
- // Calculate min as percentage based on current container size
380
- const minPercent = (minSize / containerSize) * 100;
344
+ const deltaPercent = (delta / currentContainerSize) * 100;
345
+ const minPercent = (minSize / currentContainerSize) * 100;
381
346
 
382
- // Store original sizes
383
347
  const oldSize1 = sizes[index];
384
348
  const oldSize2 = sizes[index + 1];
385
349
  const totalSize = oldSize1 + oldSize2;
386
350
 
387
- // Calculate desired new sizes
388
- let newSize1 = oldSize1 + deltaPercent;
389
- let newSize2 = oldSize2 - deltaPercent;
390
-
391
- // Apply minimum constraints - clamp to valid range
392
- newSize1 = Math.max(minPercent, newSize1);
393
- newSize2 = totalSize - newSize1;
394
-
395
- // Check if second pane violates minimum constraint after first pane clamping
396
- if (newSize2 < minPercent) {
397
- newSize2 = minPercent;
398
- newSize1 = totalSize - newSize2;
399
- }
351
+ const targetSize = oldSize1 + deltaPercent;
400
352
 
401
- // Only update if changed significantly
402
- if (Math.abs(newSize1 - oldSize1) > MIN_CHANGE_THRESHOLD) {
403
- sizes[index] = newSize1;
404
- sizes[index + 1] = newSize2;
353
+ if (clampPaneSizes(index, targetSize, minPercent, totalSize)) {
405
354
  startPos = currentPos;
406
355
  }
407
356
  }
@@ -410,41 +359,24 @@
410
359
  if (!container) return;
411
360
  if (index < 0 || index + 1 >= sizes.length) return;
412
361
 
413
- const step = keyboardStep;
414
- let handled = false;
415
-
416
362
  const isHorizontal = currentDirection === "horizontal";
417
363
  const increaseKeys = isHorizontal ? ["ArrowRight"] : ["ArrowDown"];
418
364
  const decreaseKeys = isHorizontal ? ["ArrowLeft"] : ["ArrowUp"];
419
365
 
420
366
  const containerSize = isHorizontal ? container.offsetWidth : container.offsetHeight;
421
- // Bail out if container has zero or near-zero dimensions
422
367
  if (containerSize < 1) return;
423
368
 
424
369
  const minPercent = (minSize / containerSize) * 100;
425
-
426
370
  const total = sizes[index] + sizes[index + 1];
427
371
 
428
- const applyClamp = (target: number) => {
429
- let newSize1 = Math.min(total - minPercent, Math.max(minPercent, target));
430
- let newSize2 = total - newSize1;
431
-
432
- if (newSize2 < minPercent) {
433
- newSize2 = minPercent;
434
- newSize1 = total - newSize2;
435
- }
436
-
437
- if (Math.abs(newSize1 - sizes[index]) > MIN_CHANGE_THRESHOLD) {
438
- sizes[index] = newSize1;
439
- sizes[index + 1] = newSize2;
440
- handled = true;
441
- }
442
- };
372
+ let handled = false;
443
373
 
444
374
  if (increaseKeys.includes(e.key)) {
445
- applyClamp(sizes[index] + step);
375
+ const targetSize = sizes[index] + keyboardStep;
376
+ handled = clampPaneSizes(index, targetSize, minPercent, total);
446
377
  } else if (decreaseKeys.includes(e.key)) {
447
- applyClamp(sizes[index] - step);
378
+ const targetSize = sizes[index] - keyboardStep;
379
+ handled = clampPaneSizes(index, targetSize, minPercent, total);
448
380
  } else if (e.key === "Enter" || e.key === " ") {
449
381
  // Reset to equal sizes
450
382
  const equal = 100 / registeredPanes;
@@ -456,6 +388,18 @@
456
388
  e.preventDefault();
457
389
  }
458
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
+ }
459
403
  </script>
460
404
 
461
405
  <div bind:this={container} class={splitpane({ direction: currentDirection, class: clsx(theme, className) })}>
@@ -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";
@@ -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
@@ -1883,3 +1883,17 @@ export interface ScrollSpyProps extends ScrollSpyVariants, HTMLAttributes<HTMLEl
1883
1883
  /** Callback when navigation item is clicked */
1884
1884
  onNavigate?: (itemId: string) => void;
1885
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,3 @@
1
+ export declare function nonPassiveTouch(node: HTMLElement, handler: (event: TouchEvent) => void): {
2
+ destroy(): void;
3
+ };
@@ -0,0 +1,8 @@
1
+ export function nonPassiveTouch(node, handler) {
2
+ node.addEventListener("touchstart", handler, { passive: false });
3
+ return {
4
+ destroy() {
5
+ node.removeEventListener("touchstart", handler);
6
+ }
7
+ };
8
+ }
@@ -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,3 @@
1
+ export { default as VirtualMasonry } from "./VirtualMasonry.svelte";
2
+ export type { VirtualMasonryVariants } from "./theme";
3
+ export { virtualMasonry } from "./theme";
@@ -0,0 +1,2 @@
1
+ export { default as VirtualMasonry } from "./VirtualMasonry.svelte";
2
+ export { virtualMasonry } from "./theme";
@@ -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.24.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"