@wyxos/vibe 1.6.19 → 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/index.js +681 -647
- package/lib/vibe.css +1 -1
- package/package.json +1 -1
- package/src/Masonry.vue +140 -13
- package/src/components/MasonryItem.vue +431 -431
- package/src/useMasonryScroll.ts +60 -60
package/lib/vibe.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.masonry-container[data-v-
|
|
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
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
|
-
//
|
|
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:
|
|
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
|
|
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)"
|