@wyxos/vibe 1.6.18 → 1.6.20

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-01598521]{overflow-anchor:none}.masonry-item[data-v-01598521]{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-01598521]{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-01598521],.masonry-container:not(.force-motion) .masonry-move[data-v-01598521]{transition-duration:1ms!important}}
1
+ .masonry-container[data-v-2168600e]{overflow-anchor:none}.masonry-item[data-v-2168600e]{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-2168600e]{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-2168600e],.masonry-container:not(.force-motion) .masonry-move[data-v-2168600e]{transition-duration:1ms!important}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/vibe",
3
- "version": "1.6.18",
3
+ "version": "1.6.20",
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",
@@ -21,7 +21,7 @@
21
21
  "type": "git",
22
22
  "url": "https://github.com/wyxos/vibe.git"
23
23
  },
24
- "homepage": "https://wyxos.github.io/vibe/",
24
+ "homepage": "https://vibe.wyxos.com/",
25
25
  "bugs": {
26
26
  "url": "https://github.com/wyxos/vibe/issues"
27
27
  },
package/src/Masonry.vue CHANGED
@@ -416,6 +416,8 @@ defineExpose({
416
416
  remove,
417
417
  removeMany,
418
418
  removeAll,
419
+ restore,
420
+ restoreMany,
419
421
  loadNext,
420
422
  loadPage,
421
423
  refreshCurrentPage,
@@ -449,10 +451,11 @@ function refreshLayout(items: any[]) {
449
451
  if (!container.value) return
450
452
  // Developer diagnostics: warn when dimensions are invalid
451
453
  checkItemDimensions(items as any[], 'refreshLayout')
452
- // Preserve original index before layout reordering
454
+ // Update original index to reflect current position in array
455
+ // This ensures indices are correct after items are removed
453
456
  const itemsWithIndex = items.map((item, index) => ({
454
457
  ...item,
455
- originalIndex: item.originalIndex ?? index
458
+ originalIndex: index
456
459
  }))
457
460
 
458
461
  // When fixed dimensions are set, ensure container uses the fixed width for layout
@@ -732,6 +735,128 @@ async function removeMany(items: any[]) {
732
735
  })
733
736
  }
734
737
 
738
+ /**
739
+ * Restore a single item at its original index.
740
+ * This is useful for undo operations where an item needs to be restored to its exact position.
741
+ * Handles all index calculation and layout recalculation internally.
742
+ * @param item - Item to restore
743
+ * @param index - Original index of the item
744
+ */
745
+ async function restore(item: any, index: number) {
746
+ if (!item) return
747
+
748
+ const current = masonry.value as any[]
749
+ const existingIndex = current.findIndex(i => i.id === item.id)
750
+ if (existingIndex !== -1) return // Item already exists
751
+
752
+ // Insert at the original index (clamped to valid range)
753
+ const newItems = [...current]
754
+ const targetIndex = Math.min(index, newItems.length)
755
+ newItems.splice(targetIndex, 0, item)
756
+
757
+ // Update the masonry array
758
+ masonry.value = newItems
759
+ await nextTick()
760
+
761
+ // Trigger layout recalculation (same pattern as remove)
762
+ if (!useSwipeMode.value) {
763
+ // Commit DOM updates without forcing sync reflow
764
+ await new Promise<void>(r => requestAnimationFrame(() => r()))
765
+ // Start FLIP on next frame
766
+ requestAnimationFrame(() => {
767
+ refreshLayout(newItems)
768
+ })
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Restore multiple items at their original indices.
774
+ * This is useful for undo operations where items need to be restored to their exact positions.
775
+ * Handles all index calculation and layout recalculation internally.
776
+ * @param items - Array of items to restore
777
+ * @param indices - Array of original indices for each item (must match items array length)
778
+ */
779
+ async function restoreMany(items: any[], indices: number[]) {
780
+ if (!items || items.length === 0) return
781
+ if (!indices || indices.length !== items.length) {
782
+ console.warn('[Masonry] restoreMany: items and indices arrays must have the same length')
783
+ return
784
+ }
785
+
786
+ const current = masonry.value as any[]
787
+ const existingIds = new Set(current.map(i => i.id))
788
+
789
+ // Filter out items that already exist and pair with their indices
790
+ const itemsToRestore: Array<{ item: any; index: number }> = []
791
+ for (let i = 0; i < items.length; i++) {
792
+ if (!existingIds.has(items[i]?.id)) {
793
+ itemsToRestore.push({ item: items[i], index: indices[i] })
794
+ }
795
+ }
796
+
797
+ if (itemsToRestore.length === 0) return
798
+
799
+ // Build the final array by merging current items and restored items
800
+ // Strategy: Build position by position - for each position, decide if it should be
801
+ // a restored item (at its original index) or a current item (accounting for shifts)
802
+
803
+ // Create a map of restored items by their original index for O(1) lookup
804
+ const restoredByIndex = new Map<number, any>()
805
+ for (const { item, index } of itemsToRestore) {
806
+ restoredByIndex.set(index, item)
807
+ }
808
+
809
+ // Find the maximum position we need to consider
810
+ const maxRestoredIndex = itemsToRestore.length > 0
811
+ ? Math.max(...itemsToRestore.map(({ index }) => index))
812
+ : -1
813
+ const maxPosition = Math.max(current.length - 1, maxRestoredIndex)
814
+
815
+ // Build the final array position by position
816
+ // Key insight: Current array items are in "shifted" positions (missing the removed items).
817
+ // When we restore items at their original positions, current items naturally shift back.
818
+ // We can build the final array by iterating positions and using items sequentially.
819
+ const newItems: any[] = []
820
+ let currentArrayIndex = 0 // Track which current item we should use next
821
+
822
+ // Iterate through all positions up to the maximum we need
823
+ for (let position = 0; position <= maxPosition; position++) {
824
+ // If there's a restored item that belongs at this position, use it
825
+ if (restoredByIndex.has(position)) {
826
+ newItems.push(restoredByIndex.get(position)!)
827
+ } else {
828
+ // Otherwise, this position should be filled by the next current item
829
+ // Since current array is missing restored items, items are shifted left.
830
+ // By using them sequentially, they naturally end up in the correct positions.
831
+ if (currentArrayIndex < current.length) {
832
+ newItems.push(current[currentArrayIndex])
833
+ currentArrayIndex++
834
+ }
835
+ }
836
+ }
837
+
838
+ // Add any remaining current items that come after the last restored position
839
+ // (These are items that were originally after maxRestoredIndex)
840
+ while (currentArrayIndex < current.length) {
841
+ newItems.push(current[currentArrayIndex])
842
+ currentArrayIndex++
843
+ }
844
+
845
+ // Update the masonry array
846
+ masonry.value = newItems
847
+ await nextTick()
848
+
849
+ // Trigger layout recalculation (same pattern as removeMany)
850
+ if (!useSwipeMode.value) {
851
+ // Commit DOM updates without forcing sync reflow
852
+ await new Promise<void>(r => requestAnimationFrame(() => r()))
853
+ // Start FLIP on next frame
854
+ requestAnimationFrame(() => {
855
+ refreshLayout(newItems)
856
+ })
857
+ }
858
+ }
859
+
735
860
  function scrollToTop(options?: ScrollToOptions) {
736
861
  if (container.value) {
737
862
  container.value.scrollTo({
@@ -789,6 +914,8 @@ async function maybeBackfillToTarget(baselineCount: number, force = false) {
789
914
  if ((masonry.value as any[]).length >= targetCount) return
790
915
 
791
916
  backfillActive = true
917
+ // Set loading to true at the start of backfill and keep it true throughout
918
+ isLoading.value = true
792
919
  try {
793
920
  let calls = 0
794
921
  emits('backfill:start', { target: targetCount, fetched: (masonry.value as any[]).length, calls })
@@ -818,7 +945,7 @@ async function maybeBackfillToTarget(baselineCount: number, force = false) {
818
945
  break
819
946
  }
820
947
  try {
821
- isLoading.value = true
948
+ // Don't toggle isLoading here - keep it true throughout backfill
822
949
  const response = await getContent(currentPage)
823
950
  if (cancelRequested.value) break
824
951
  // Clear error on successful load
@@ -831,8 +958,6 @@ async function maybeBackfillToTarget(baselineCount: number, force = false) {
831
958
  } catch (error) {
832
959
  // Set load error but don't break the backfill loop
833
960
  loadError.value = error instanceof Error ? error : new Error(String(error))
834
- } finally {
835
- isLoading.value = false
836
961
  }
837
962
 
838
963
  calls++
@@ -841,6 +966,8 @@ async function maybeBackfillToTarget(baselineCount: number, force = false) {
841
966
  emits('backfill:stop', { fetched: (masonry.value as any[]).length, calls })
842
967
  } finally {
843
968
  backfillActive = false
969
+ // Only set loading to false when backfill completes
970
+ isLoading.value = false
844
971
  }
845
972
  }
846
973
 
@@ -877,7 +1004,7 @@ function reset() {
877
1004
  function destroy() {
878
1005
  // Cancel any ongoing loads
879
1006
  cancelLoad()
880
-
1007
+
881
1008
  // Reset all state
882
1009
  masonry.value = []
883
1010
  masonryContentHeight.value = 0
@@ -888,26 +1015,26 @@ function destroy() {
888
1015
  isLoading.value = false
889
1016
  backfillActive = false
890
1017
  cancelRequested.value = false
891
-
1018
+
892
1019
  // Reset swipe mode state
893
1020
  currentSwipeIndex.value = 0
894
1021
  swipeOffset.value = 0
895
1022
  isDragging.value = false
896
-
1023
+
897
1024
  // Reset viewport state
898
1025
  viewportTop.value = 0
899
1026
  viewportHeight.value = 0
900
1027
  virtualizing.value = false
901
-
1028
+
902
1029
  // Reset scroll progress
903
1030
  scrollProgress.value = {
904
1031
  distanceToTrigger: 0,
905
1032
  isNearTrigger: false
906
1033
  }
907
-
1034
+
908
1035
  // Reset invalid dimension tracking
909
1036
  invalidDimensionIds.value.clear()
910
-
1037
+
911
1038
  // Scroll to top if container exists
912
1039
  if (container.value) {
913
1040
  container.value.scrollTo({
@@ -1336,7 +1463,7 @@ onUnmounted(() => {
1336
1463
  }">
1337
1464
  <div class="w-full h-full flex items-center justify-center p-4">
1338
1465
  <div class="w-full h-full max-w-full max-h-full relative">
1339
- <slot :item="item" :remove="remove">
1466
+ <slot :item="item" :remove="remove" :index="item.originalIndex ?? props.items.indexOf(item)">
1340
1467
  <MasonryItem :item="item" :remove="remove" :header-height="layout.header" :footer-height="layout.footer"
1341
1468
  :in-swipe-mode="true" :is-active="index === currentSwipeIndex"
1342
1469
  @preload:success="(p) => emits('item:preload:success', p)"
@@ -1379,7 +1506,7 @@ onUnmounted(() => {
1379
1506
  <div v-for="(item, i) in visibleMasonry" :key="`${item.page}-${item.id}`" class="absolute masonry-item"
1380
1507
  v-bind="getItemAttributes(item, i)">
1381
1508
  <!-- Use default slot if provided, otherwise use MasonryItem -->
1382
- <slot :item="item" :remove="remove">
1509
+ <slot :item="item" :remove="remove" :index="item.originalIndex ?? items.indexOf(item)">
1383
1510
  <MasonryItem :item="item" :remove="remove" :header-height="layout.header" :footer-height="layout.footer"
1384
1511
  :in-swipe-mode="false" :is-active="false" @preload:success="(p) => emits('item:preload:success', p)"
1385
1512
  @preload:error="(p) => emits('item:preload:error', p)"