@wyxos/vibe 1.6.25 → 1.6.27

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/lib/vibe.css CHANGED
@@ -1 +1 @@
1
- .masonry-container[data-v-2c2a4c76]{overflow-anchor:none}.masonry-item[data-v-2c2a4c76]{will-change:transform,opacity;contain:layout paint;transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1)),opacity var(--masonry-leave-duration, .16s) ease-out var(--masonry-opacity-delay, 0ms);backface-visibility:hidden}.masonry-move[data-v-2c2a4c76]{transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1))}@media (prefers-reduced-motion: reduce){.masonry-container:not(.force-motion) .masonry-item[data-v-2c2a4c76],.masonry-container:not(.force-motion) .masonry-move[data-v-2c2a4c76]{transition-duration:1ms!important}}
1
+ .masonry-container[data-v-ce75570c]{overflow-anchor:none}.masonry-item[data-v-ce75570c]{will-change:transform,opacity;contain:layout paint;transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1)),opacity var(--masonry-leave-duration, .16s) ease-out var(--masonry-opacity-delay, 0ms);backface-visibility:hidden}.masonry-move[data-v-ce75570c]{transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1))}@media (prefers-reduced-motion: reduce){.masonry-container:not(.force-motion) .masonry-item[data-v-ce75570c],.masonry-container:not(.force-motion) .masonry-move[data-v-ce75570c]{transition-duration:1ms!important}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/vibe",
3
- "version": "1.6.25",
3
+ "version": "1.6.27",
4
4
  "description": "A high-performance, responsive masonry layout engine for Vue 3 with built-in infinite scrolling and virtualization.",
5
5
  "keywords": [
6
6
  "vue",
package/src/Masonry.vue CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  getItemAttributes,
10
10
  calculateColumnHeights
11
11
  } from './masonryUtils'
12
- import { useMasonryTransitions } from './useMasonryTransitions'
12
+ import { createMasonryTransitions } from './createMasonryTransitions'
13
13
  import { useMasonryScroll } from './useMasonryScroll'
14
14
  import { useSwipeMode as useSwipeModeComposable } from './useSwipeMode'
15
15
  import { useMasonryPagination } from './useMasonryPagination'
@@ -21,7 +21,7 @@ import MasonryItem from './components/MasonryItem.vue'
21
21
  import { normalizeError } from './utils/errorHandler'
22
22
 
23
23
  const props = defineProps({
24
- getNextPage: {
24
+ getPage: {
25
25
  type: Function,
26
26
  default: () => { }
27
27
  },
@@ -33,6 +33,12 @@ const props = defineProps({
33
33
  type: Array,
34
34
  default: () => []
35
35
  },
36
+ // Opaque caller-owned context passed through to getPage(page, context).
37
+ // Useful for including filters, service selection, tabId, etc.
38
+ context: {
39
+ type: Object,
40
+ default: null
41
+ },
36
42
  layout: {
37
43
  type: Object
38
44
  },
@@ -41,27 +47,20 @@ const props = defineProps({
41
47
  default: 'page', // or 'cursor'
42
48
  validator: (v: string) => ['page', 'cursor'].includes(v)
43
49
  },
44
- skipInitialLoad: {
45
- type: Boolean,
46
- default: false
47
- },
48
- // Initial pagination state when skipInitialLoad is true and items are provided
49
- initialPage: {
50
- type: [Number, String],
51
- default: null
52
- },
53
- initialNextPage: {
54
- type: [Number, String],
55
- default: null
50
+ init: {
51
+ type: String,
52
+ default: 'manual',
53
+ validator: (v: string) => ['auto', 'manual'].includes(v)
56
54
  },
57
55
  pageSize: {
58
56
  type: Number,
59
57
  default: 40
60
58
  },
61
59
  // Backfill configuration
62
- backfillEnabled: {
63
- type: Boolean,
64
- default: true
60
+ mode: {
61
+ type: String,
62
+ default: 'backfill',
63
+ validator: (value: string) => ['backfill', 'none', 'refresh'].includes(value)
65
64
  },
66
65
  backfillDelayMs: {
67
66
  type: Number,
@@ -110,10 +109,6 @@ const props = defineProps({
110
109
  type: Number,
111
110
  default: 200
112
111
  },
113
- autoRefreshOnEmpty: {
114
- type: Boolean,
115
- default: false
116
- },
117
112
  // Layout mode: 'auto' (detect from screen size), 'masonry', or 'swipe'
118
113
  layoutMode: {
119
114
  type: String,
@@ -181,19 +176,22 @@ const useSwipeMode = computed(() => {
181
176
 
182
177
  const emits = defineEmits([
183
178
  'update:items',
179
+ 'loading:start',
184
180
  'backfill:start',
185
181
  'backfill:tick',
186
182
  'backfill:stop',
187
183
  'retry:start',
188
184
  'retry:tick',
189
185
  'retry:stop',
186
+ 'loading:stop',
190
187
  'remove-all:complete',
191
188
  // Re-emit item-level preload events from the default MasonryItem
192
189
  'item:preload:success',
193
190
  'item:preload:error',
194
191
  // Mouse events from MasonryItem content
195
192
  'item:mouse-enter',
196
- 'item:mouse-leave'
193
+ 'item:mouse-leave',
194
+ 'update:context'
197
195
  ])
198
196
 
199
197
  const masonry = computed<any>({
@@ -201,6 +199,20 @@ const masonry = computed<any>({
201
199
  set: (val) => emits('update:items', val)
202
200
  })
203
201
 
202
+ const context = computed<any>({
203
+ get: () => props.context,
204
+ set: (val) => emits('update:context', val)
205
+ })
206
+
207
+ function setContext(val: any) {
208
+ context.value = val
209
+ }
210
+
211
+ const masonryLength = computed((): number => {
212
+ const items = masonry.value as any[]
213
+ return items?.length ?? 0
214
+ })
215
+
204
216
  const columns = ref<number>(7)
205
217
  const container = ref<HTMLElement | null>(null)
206
218
  const paginationHistory = ref<any[]>([])
@@ -209,6 +221,10 @@ const isLoading = ref<boolean>(false)
209
221
  const masonryContentHeight = ref<number>(0)
210
222
  const hasReachedEnd = ref<boolean>(false) // Track when we've reached the last page
211
223
  const loadError = ref<Error | null>(null) // Track load errors
224
+ // Track when first content has loaded
225
+ // For 'manual' init, show masonry immediately since we're about to load
226
+ // For 'auto' init, wait for items to be provided or loaded
227
+ const isInitialized = ref<boolean>(false)
212
228
 
213
229
  // Current breakpoint
214
230
  const currentBreakpoint = computed(() => getBreakpointName(containerWidth.value))
@@ -251,8 +267,8 @@ const virtualization = useMasonryVirtualization({
251
267
  // Extract virtualization state and functions
252
268
  const { viewportTop, viewportHeight, virtualizing, scrollProgress, visibleMasonry, updateScrollProgress, updateViewport: updateViewportVirtualization, reset: resetVirtualization } = virtualization
253
269
 
254
- // Initialize transitions composable with virtualization support
255
- const { onEnter, onBeforeEnter, onBeforeLeave, onLeave } = useMasonryTransitions(
270
+ // Initialize transitions factory with virtualization support
271
+ const { onEnter, onBeforeEnter, onBeforeLeave, onLeave } = createMasonryTransitions(
256
272
  { container, masonry: masonry as any },
257
273
  { leaveDurationMs: props.leaveDurationMs, virtualizing }
258
274
  )
@@ -265,7 +281,8 @@ const leave = onLeave
265
281
 
266
282
  // Initialize pagination composable
267
283
  const pagination = useMasonryPagination({
268
- getNextPage: props.getNextPage as (page: any) => Promise<{ items: any[]; nextPage: any }>,
284
+ getPage: props.getPage as (page: any, ctx?: any) => Promise<{ items: any[]; nextPage: any }>,
285
+ context,
269
286
  masonry: masonry as any,
270
287
  isLoading,
271
288
  hasReachedEnd,
@@ -276,11 +293,10 @@ const pagination = useMasonryPagination({
276
293
  retryMaxAttempts: props.retryMaxAttempts,
277
294
  retryInitialDelayMs: props.retryInitialDelayMs,
278
295
  retryBackoffStepMs: props.retryBackoffStepMs,
279
- backfillEnabled: props.backfillEnabled,
296
+ mode: props.mode,
280
297
  backfillDelayMs: props.backfillDelayMs,
281
298
  backfillMaxCalls: props.backfillMaxCalls,
282
299
  pageSize: props.pageSize,
283
- autoRefreshOnEmpty: props.autoRefreshOnEmpty,
284
300
  emits
285
301
  })
286
302
 
@@ -324,12 +340,11 @@ const items = useMasonryItems({
324
340
  refreshCurrentPage,
325
341
  loadNext,
326
342
  maybeBackfillToTarget,
327
- autoRefreshOnEmpty: props.autoRefreshOnEmpty,
328
343
  paginationHistory
329
344
  })
330
345
 
331
346
  // Extract item management functions
332
- const { remove, removeMany, restore, restoreMany, removeAll: removeAllItems } = items
347
+ const { remove, removeMany, restore, restoreMany, removeAll } = items
333
348
 
334
349
  // setFixedDimensions is now in useMasonryLayout composable
335
350
  // Wrapper function to maintain API compatibility and handle wrapper restoration
@@ -343,39 +358,62 @@ function setFixedDimensions(dimensions: { width?: number; height?: number } | nu
343
358
  }
344
359
 
345
360
  defineExpose({
346
- isLoading,
347
- refreshLayout,
348
- // Container dimensions (wrapper element)
349
- containerWidth,
361
+ // Cancels any ongoing load operations (page loads, backfills, etc.)
362
+ cancelLoad,
363
+ // Opaque caller context passed through to getPage(page, context). Useful for including filters, service selection, tabId, etc.
364
+ context,
365
+ // Container height (wrapper element) in pixels
350
366
  containerHeight,
351
- // Masonry content height (for backward compatibility, old containerHeight)
352
- contentHeight: masonryContentHeight,
353
- // Current page
367
+ // Container width (wrapper element) in pixels
368
+ containerWidth,
369
+ // Current Tailwind breakpoint name (base, sm, md, lg, xl, 2xl) based on containerWidth
370
+ currentBreakpoint,
371
+ // Current page number or cursor being displayed
354
372
  currentPage,
355
- // End of list tracking
373
+ // Completely destroys the component, clearing all state and resetting to initial state
374
+ destroy,
375
+ // Boolean indicating if the end of the list has been reached (no more pages to load)
356
376
  hasReachedEnd,
357
- // Load error tracking
377
+ // Initializes the component with items, page, and next page cursor. Use this for manual init mode.
378
+ initialize,
379
+ // Boolean indicating if the component has been initialized (first content has loaded)
380
+ isInitialized,
381
+ // Boolean indicating if a page load or backfill operation is currently in progress
382
+ isLoading,
383
+ // Error object if the last load operation failed, null otherwise
358
384
  loadError,
359
- // Set fixed dimensions (overrides ResizeObserver)
360
- setFixedDimensions,
361
- remove,
362
- removeMany,
363
- removeAll: removeAllItems,
364
- restore,
365
- restoreMany,
385
+ // Loads the next page of items asynchronously
366
386
  loadNext,
387
+ // Loads a specific page number or cursor asynchronously
367
388
  loadPage,
389
+ // Array tracking pagination history (pages/cursors that have been loaded)
390
+ paginationHistory,
391
+ // Refreshes the current page by clearing items and reloading from the current page
368
392
  refreshCurrentPage,
393
+ // Recalculates the layout positions for all items. Call this after manually modifying items.
394
+ refreshLayout,
395
+ // Removes a single item from the masonry
396
+ remove,
397
+ // Removes all items from the masonry
398
+ removeAll,
399
+ // Removes multiple items from the masonry in a single operation
400
+ removeMany,
401
+ // Resets the component to initial state (clears items, resets pagination, scrolls to top)
369
402
  reset,
370
- destroy,
371
- init,
372
- restoreItems,
373
- paginationHistory,
374
- cancelLoad,
375
- scrollToTop,
403
+ // Restores a single item at its original index (useful for undo operations)
404
+ restore,
405
+ // Restores multiple items at their original indices (useful for undo operations)
406
+ restoreMany,
407
+ // Scrolls the container to a specific position
376
408
  scrollTo,
377
- totalItems: computed(() => (masonry.value as any[]).length),
378
- currentBreakpoint
409
+ // Scrolls the container to the top
410
+ scrollToTop,
411
+ // Sets the opaque caller context (alternative to v-model:context)
412
+ setContext,
413
+ // Sets fixed dimensions for the container, overriding ResizeObserver. Pass null to restore automatic sizing.
414
+ setFixedDimensions,
415
+ // Computed property returning the total number of items currently in the masonry
416
+ totalItems: computed(() => (masonry.value as any[]).length)
379
417
  })
380
418
 
381
419
  // Layout functions are now in useMasonryLayout composable
@@ -401,6 +439,18 @@ const goToNextItem = swipeMode.goToNextItem
401
439
  const goToPreviousItem = swipeMode.goToPreviousItem
402
440
  const snapToCurrentItem = swipeMode.snapToCurrentItem
403
441
 
442
+ // Helper functions for swipe mode percentage calculations
443
+ function getSwipeItemTop(index: string | number): string {
444
+ const length = masonryLength.value
445
+ const numIndex = typeof index === 'string' ? parseInt(index, 10) : index
446
+ return length > 0 ? `${numIndex * (100 / length)}%` : '0%'
447
+ }
448
+
449
+ function getSwipeItemHeight(): string {
450
+ const length = masonryLength.value
451
+ return length > 0 ? `${100 / length}%` : '0%'
452
+ }
453
+
404
454
  // refreshCurrentPage is now in useMasonryPagination composable
405
455
 
406
456
  // Item management functions (remove, removeMany, restore, restoreMany, removeAll) are now in useMasonryItems composable
@@ -430,7 +480,7 @@ function scrollTo(options: { top?: number; left?: number; behavior?: ScrollBehav
430
480
  }
431
481
  }
432
482
 
433
- // removeAll is now in useMasonryItems composable (removeAllItems)
483
+ // removeAll is now in useMasonryItems composable
434
484
 
435
485
  // onResize is now in useMasonryLayout composable (onResizeLayout)
436
486
  function onResize() {
@@ -461,12 +511,10 @@ function reset() {
461
511
  paginationHistory.value = [props.loadAtPage]
462
512
  hasReachedEnd.value = false // Reset end flag
463
513
  loadError.value = null // Reset error flag
514
+ isInitialized.value = false // Reset initialization flag
464
515
 
465
516
  // Reset virtualization state
466
517
  resetVirtualization()
467
-
468
- // Reset auto-initialization flag so watcher can work again if needed
469
- hasInitializedWithItems = false
470
518
  }
471
519
 
472
520
  function destroy() {
@@ -481,6 +529,7 @@ function destroy() {
481
529
  hasReachedEnd.value = false
482
530
  loadError.value = null
483
531
  isLoading.value = false
532
+ isInitialized.value = false
484
533
 
485
534
  // Reset swipe mode state
486
535
  currentSwipeIndex.value = 0
@@ -519,24 +568,32 @@ function handleWindowResize() {
519
568
  // Note: containerWidth is updated by ResizeObserver
520
569
  }
521
570
 
522
- function init(items: any[], page: any, next: any) {
571
+ function initialize(items: any[], page: any, next: any) {
523
572
  currentPage.value = page // Track the initial current page
524
573
  paginationHistory.value = [page]
525
- paginationHistory.value.push(next)
526
- // Update hasReachedEnd if next is null
527
- hasReachedEnd.value = next == null
574
+ if (next !== null && next !== undefined) {
575
+ paginationHistory.value.push(next)
576
+ }
577
+ // Only treat explicit null as end-of-list. Undefined means "unknown".
578
+ hasReachedEnd.value = next === null
528
579
  // Diagnostics: check incoming initial items
529
- checkItemDimensions(items as any[], 'init')
580
+ checkItemDimensions(items as any[], 'initialize')
581
+
582
+ // If masonry is empty, replace items; otherwise add them
583
+ const currentItems = masonry.value as any[]
584
+ const newItems = currentItems.length === 0 ? items : [...currentItems, ...items]
585
+
586
+ // Set items first (this updates the v-model)
587
+ masonry.value = newItems
530
588
 
531
589
  if (useSwipeMode.value) {
532
- // In swipe mode, just add items without layout calculation
533
- masonry.value = [...(masonry.value as any[]), ...items]
590
+ // In swipe mode, just set items without layout calculation
534
591
  // Reset swipe index if we're at the start
535
592
  if (currentSwipeIndex.value === 0 && masonry.value.length > 0) {
536
593
  swipeOffset.value = 0
537
594
  }
538
595
  } else {
539
- refreshLayout([...(masonry.value as any[]), ...items])
596
+ refreshLayout(newItems)
540
597
 
541
598
  // Update viewport state from container's scroll position
542
599
  // Critical after refresh when browser may restore scroll position
@@ -555,89 +612,14 @@ function init(items: any[], page: any, next: any) {
555
612
  }
556
613
  })
557
614
  }
558
- }
559
-
560
- /**
561
- * Restore items when skipInitialLoad is true.
562
- * This method should be called instead of directly assigning to v-model:items
563
- * when restoring items from saved state.
564
- * @param items - Items to restore
565
- * @param page - Current page number/cursor
566
- * @param next - Next page cursor (or null if at end)
567
- */
568
- async function restoreItems(items: any[], page: any, next: any) {
569
- // If skipInitialLoad is false, fall back to init behavior
570
- if (!props.skipInitialLoad) {
571
- init(items, page, next)
572
- return
573
- }
574
-
575
- // When skipInitialLoad is true, we need to restore items without triggering initial load
576
- currentPage.value = page
577
- paginationHistory.value = [page]
578
- if (next !== null && next !== undefined) {
579
- paginationHistory.value.push(next)
580
- }
581
- // Only set hasReachedEnd to true if next is explicitly null (end of list)
582
- // undefined means "unknown" - don't assume end of list
583
- hasReachedEnd.value = next === null
584
- loadError.value = null
585
-
586
- // Diagnostics: check incoming items
587
- checkItemDimensions(items as any[], 'restoreItems')
588
-
589
- // Set items directly (v-model will sync) and refresh layout
590
- // Follow the same pattern as init() and getContent()
591
- if (useSwipeMode.value) {
592
- // In swipe mode, just set items without layout calculation
593
- masonry.value = items
594
- // Reset swipe index if we're at the start
595
- if (currentSwipeIndex.value === 0 && masonry.value.length > 0) {
596
- swipeOffset.value = 0
597
- }
598
- } else {
599
- // In masonry mode, refresh layout with the restored items
600
- refreshLayout(items)
601
-
602
- // Update viewport state from container's scroll position
603
- if (container.value) {
604
- viewportTop.value = container.value.scrollTop
605
- viewportHeight.value = container.value.clientHeight || window.innerHeight
606
- }
607
615
 
608
- // Update again after DOM updates to catch browser scroll restoration
609
- await nextTick()
610
- if (container.value) {
611
- viewportTop.value = container.value.scrollTop
612
- viewportHeight.value = container.value.clientHeight || window.innerHeight
613
- updateScrollProgress()
614
-
615
- // Check if user is already at the bottom after restoration
616
- // If so, trigger loading to restore scroll-to-bottom functionality
617
- // Wait for layout to be fully calculated before checking
618
- await nextTick()
619
- const columnHeights = calculateColumnHeights(masonry.value as any, columns.value)
620
- const tallest = columnHeights.length ? Math.max(...columnHeights) : 0
621
- const scrollerBottom = container.value.scrollTop + container.value.clientHeight
622
- const threshold = typeof props.loadThresholdPx === 'number' ? props.loadThresholdPx : 200
623
- const triggerPoint = threshold >= 0
624
- ? Math.max(0, tallest - threshold)
625
- : Math.max(0, tallest + threshold)
626
- const nearBottom = scrollerBottom >= triggerPoint
627
-
628
- // If user is at bottom and there's a next page, trigger loading
629
- // This restores scroll-to-bottom functionality after tab restoration
630
- if (nearBottom && !hasReachedEnd.value && !isLoading.value && paginationHistory.value.length > 0) {
631
- const nextPage = paginationHistory.value[paginationHistory.value.length - 1]
632
- if (nextPage != null) {
633
- // Use handleScroll with forceCheck=true to bypass isScrollingDown check
634
- await handleScroll(columnHeights, true)
635
- }
636
- }
637
- }
616
+ // Mark as initialized when items are provided
617
+ if (items && items.length > 0) {
618
+ isInitialized.value = true
638
619
  }
639
620
  }
640
621
 
622
+
641
623
  // Watch for layout changes and update columns + refresh layout dynamically
642
624
  watch(
643
625
  layout,
@@ -676,31 +658,14 @@ watch(container, (el) => {
676
658
  }
677
659
  }, { immediate: true })
678
660
 
679
- // Watch for items when skipInitialLoad is true to auto-initialize pagination state
680
- // This handles cases where items are provided after mount or updated externally
681
- let hasInitializedWithItems = false
661
+ // Watch for when items are first loaded (for init='manual' when items are loaded via initialize)
682
662
  watch(
683
- () => [props.items, props.skipInitialLoad, props.initialPage, props.initialNextPage] as const,
684
- ([items, skipInitialLoad, initialPage, initialNextPage]) => {
685
- // Only auto-initialize if:
686
- // 1. skipInitialLoad is true
687
- // 2. Items exist
688
- // 3. We haven't already initialized with items (to avoid re-initializing on every update)
689
- if (
690
- skipInitialLoad &&
691
- items &&
692
- items.length > 0 &&
693
- !hasInitializedWithItems
694
- ) {
695
- hasInitializedWithItems = true
696
- const page = initialPage !== null && initialPage !== undefined
697
- ? initialPage
698
- : (props.loadAtPage as any)
699
- const next = initialNextPage !== undefined
700
- ? initialNextPage
701
- : undefined // undefined means "unknown", null means "end of list"
702
-
703
- restoreItems(items as any[], page, next)
663
+ () => masonry.value.length,
664
+ (newLength, oldLength) => {
665
+ // For manual mode, mark as initialized when items first appear
666
+ // This handles the case where items are loaded via initialize after mount
667
+ if (props.init === 'manual' && !isInitialized.value && newLength > 0 && oldLength === 0) {
668
+ isInitialized.value = true
704
669
  }
705
670
  },
706
671
  { immediate: false }
@@ -852,23 +817,21 @@ onMounted(async () => {
852
817
  const initialPage = props.loadAtPage as any
853
818
  paginationHistory.value = [initialPage]
854
819
 
855
- if (!props.skipInitialLoad) {
856
- await loadPage(paginationHistory.value[0] as any)
857
- } else if (props.items && props.items.length > 0) {
858
- // When skipInitialLoad is true and items are provided, initialize pagination state
859
- // Use initialPage/initialNextPage props if provided, otherwise use loadAtPage
860
- // Only set next to null if initialNextPage is explicitly null (not undefined)
861
- const page = props.initialPage !== null && props.initialPage !== undefined
862
- ? props.initialPage
863
- : (props.loadAtPage as any)
864
- const next = props.initialNextPage !== undefined
865
- ? props.initialNextPage
866
- : undefined // undefined means "unknown", null means "end of list"
867
-
868
- await restoreItems(props.items as any[], page, next)
869
- // Mark as initialized to prevent watcher from running again
870
- hasInitializedWithItems = true
820
+ if (props.init === 'auto') {
821
+ // Auto mode: automatically call loadPage on mount
822
+ // Set initialized BEFORE loading so the masonry container renders
823
+ // This allows refreshLayout to access the container element for measurements
824
+ isInitialized.value = true
825
+ await nextTick() // Ensure container is rendered before loading
826
+
827
+ try {
828
+ await loadPage(initialPage)
829
+ } catch (error) {
830
+ // Error is already handled by loadPage via loadError
831
+ // Continue - component is already initialized
832
+ }
871
833
  }
834
+ // Manual mode: do nothing, user will manually call restore()
872
835
 
873
836
  if (!useSwipeMode.value) {
874
837
  updateScrollProgress()
@@ -886,6 +849,7 @@ onMounted(async () => {
886
849
  loadError.value = normalizeError(error)
887
850
  }
888
851
  isLoading.value = false
852
+ // isInitialized is already set to true before loadPage for 'auto' mode
889
853
  }
890
854
 
891
855
  // Scroll listener is handled by watcher now for consistency
@@ -918,19 +882,26 @@ onUnmounted(() => {
918
882
 
919
883
  <template>
920
884
  <div ref="wrapper" class="w-full h-full flex flex-col relative">
885
+ <!-- Loading message while waiting for initial content -->
886
+ <div v-if="!isInitialized" class="w-full h-full flex items-center justify-center">
887
+ <slot name="loading-message">
888
+ <p class="text-gray-500 dark:text-gray-400">Waiting for content to load...</p>
889
+ </slot>
890
+ </div>
891
+
921
892
  <!-- Swipe Feed Mode (Mobile/Tablet) -->
922
- <div v-if="useSwipeMode" class="overflow-hidden w-full flex-1 swipe-container touch-none select-none"
893
+ <div v-else-if="useSwipeMode" class="overflow-hidden w-full flex-1 swipe-container touch-none select-none"
923
894
  :class="{ 'force-motion': props.forceMotion, 'cursor-grab': !isDragging, 'cursor-grabbing': isDragging }"
924
895
  ref="swipeContainer" style="height: 100%; max-height: 100%; position: relative;">
925
896
  <div class="relative w-full" :style="{
926
897
  transform: `translateY(${swipeOffset}px)`,
927
898
  transition: isDragging ? 'none' : `transform ${transitionDurationMs}ms ${transitionEasing}`,
928
- height: `${masonry.length * 100}%`
899
+ height: `${masonryLength * 100}%`
929
900
  }">
930
901
  <div v-for="(item, index) in masonry" :key="`${item.page}-${item.id}`" class="absolute top-0 left-0 w-full"
931
902
  :style="{
932
- top: `${index * (100 / masonry.length)}%`,
933
- height: `${100 / masonry.length}%`
903
+ top: getSwipeItemTop(index),
904
+ height: getSwipeItemHeight()
934
905
  }">
935
906
  <div class="w-full h-full flex items-center justify-center p-4">
936
907
  <div class="w-full h-full max-w-full max-h-full relative">