@wyxos/vibe 1.6.10 → 1.6.12
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/README.md +29 -2
- package/lib/index.js +809 -645
- package/lib/vibe.css +1 -1
- package/package.json +1 -1
- package/src/Masonry.vue +447 -32
- package/src/masonryUtils.ts +3 -3
- package/src/views/Home.vue +50 -11
package/lib/vibe.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.masonry-container[data-v-
|
|
1
|
+
.masonry-container[data-v-919154a5]{overflow-anchor:none}.masonry-item[data-v-919154a5]{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-919154a5]{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-919154a5],.masonry-container:not(.force-motion) .masonry-move[data-v-919154a5]{transition-duration:1ms!important}}
|
package/package.json
CHANGED
package/src/Masonry.vue
CHANGED
|
@@ -97,6 +97,17 @@ const props = defineProps({
|
|
|
97
97
|
type: Boolean,
|
|
98
98
|
default: false
|
|
99
99
|
},
|
|
100
|
+
// Layout mode: 'auto' (detect from screen size), 'masonry', or 'swipe'
|
|
101
|
+
layoutMode: {
|
|
102
|
+
type: String,
|
|
103
|
+
default: 'auto',
|
|
104
|
+
validator: (v: string) => ['auto', 'masonry', 'swipe'].includes(v)
|
|
105
|
+
},
|
|
106
|
+
// Breakpoint for switching to swipe mode (in pixels or Tailwind breakpoint name)
|
|
107
|
+
mobileBreakpoint: {
|
|
108
|
+
type: [Number, String],
|
|
109
|
+
default: 768 // 'md' breakpoint
|
|
110
|
+
},
|
|
100
111
|
})
|
|
101
112
|
|
|
102
113
|
const defaultLayout = {
|
|
@@ -119,6 +130,57 @@ const layout = computed(() => ({
|
|
|
119
130
|
}
|
|
120
131
|
}))
|
|
121
132
|
|
|
133
|
+
const wrapper = ref<HTMLElement | null>(null)
|
|
134
|
+
const containerWidth = ref<number>(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
135
|
+
let resizeObserver: ResizeObserver | null = null
|
|
136
|
+
|
|
137
|
+
// Get breakpoint value from Tailwind breakpoint name
|
|
138
|
+
function getBreakpointValue(breakpoint: string): number {
|
|
139
|
+
const breakpoints: Record<string, number> = {
|
|
140
|
+
'sm': 640,
|
|
141
|
+
'md': 768,
|
|
142
|
+
'lg': 1024,
|
|
143
|
+
'xl': 1280,
|
|
144
|
+
'2xl': 1536
|
|
145
|
+
}
|
|
146
|
+
return breakpoints[breakpoint] || 768
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Determine if we should use swipe mode
|
|
150
|
+
const useSwipeMode = computed(() => {
|
|
151
|
+
if (props.layoutMode === 'masonry') return false
|
|
152
|
+
if (props.layoutMode === 'swipe') return true
|
|
153
|
+
|
|
154
|
+
// Auto mode: check container width
|
|
155
|
+
const breakpoint = typeof props.mobileBreakpoint === 'string'
|
|
156
|
+
? getBreakpointValue(props.mobileBreakpoint)
|
|
157
|
+
: props.mobileBreakpoint
|
|
158
|
+
|
|
159
|
+
return containerWidth.value < breakpoint
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Get current item index for swipe mode
|
|
163
|
+
const currentItem = computed(() => {
|
|
164
|
+
if (!useSwipeMode.value || masonry.value.length === 0) return null
|
|
165
|
+
const index = Math.max(0, Math.min(currentSwipeIndex.value, masonry.value.length - 1))
|
|
166
|
+
return (masonry.value as any[])[index] || null
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Get next/previous items for preloading in swipe mode
|
|
170
|
+
const nextItem = computed(() => {
|
|
171
|
+
if (!useSwipeMode.value || !currentItem.value) return null
|
|
172
|
+
const nextIndex = currentSwipeIndex.value + 1
|
|
173
|
+
if (nextIndex >= masonry.value.length) return null
|
|
174
|
+
return (masonry.value as any[])[nextIndex] || null
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const previousItem = computed(() => {
|
|
178
|
+
if (!useSwipeMode.value || !currentItem.value) return null
|
|
179
|
+
const prevIndex = currentSwipeIndex.value - 1
|
|
180
|
+
if (prevIndex < 0) return null
|
|
181
|
+
return (masonry.value as any[])[prevIndex] || null
|
|
182
|
+
})
|
|
183
|
+
|
|
122
184
|
const emits = defineEmits([
|
|
123
185
|
'update:items',
|
|
124
186
|
'backfill:start',
|
|
@@ -142,6 +204,14 @@ const currentPage = ref<any>(null) // Track the actual current page being displ
|
|
|
142
204
|
const isLoading = ref<boolean>(false)
|
|
143
205
|
const containerHeight = ref<number>(0)
|
|
144
206
|
|
|
207
|
+
// Swipe mode state
|
|
208
|
+
const currentSwipeIndex = ref<number>(0)
|
|
209
|
+
const swipeOffset = ref<number>(0)
|
|
210
|
+
const isDragging = ref<boolean>(false)
|
|
211
|
+
const dragStartY = ref<number>(0)
|
|
212
|
+
const dragStartOffset = ref<number>(0)
|
|
213
|
+
const swipeContainer = ref<HTMLElement | null>(null)
|
|
214
|
+
|
|
145
215
|
// Diagnostics: track items missing width/height to help developers
|
|
146
216
|
const invalidDimensionIds = ref<Set<number | string>>(new Set())
|
|
147
217
|
function isPositiveNumber(value: unknown): boolean {
|
|
@@ -320,6 +390,12 @@ function calculateHeight(content: any[]) {
|
|
|
320
390
|
}
|
|
321
391
|
|
|
322
392
|
function refreshLayout(items: any[]) {
|
|
393
|
+
if (useSwipeMode.value) {
|
|
394
|
+
// In swipe mode, no layout calculation needed - items are stacked vertically
|
|
395
|
+
masonry.value = items as any
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
323
399
|
if (!container.value) return
|
|
324
400
|
// Developer diagnostics: warn when dimensions are invalid
|
|
325
401
|
checkItemDimensions(items as any[], 'refreshLayout')
|
|
@@ -671,6 +747,8 @@ function reset() {
|
|
|
671
747
|
}
|
|
672
748
|
|
|
673
749
|
const debouncedScrollHandler = debounce(async () => {
|
|
750
|
+
if (useSwipeMode.value) return // Skip scroll handling in swipe mode
|
|
751
|
+
|
|
674
752
|
if (container.value) {
|
|
675
753
|
viewportTop.value = container.value.scrollTop
|
|
676
754
|
viewportHeight.value = container.value.clientHeight
|
|
@@ -688,34 +766,295 @@ const debouncedScrollHandler = debounce(async () => {
|
|
|
688
766
|
|
|
689
767
|
const debouncedResizeHandler = debounce(onResize, 200)
|
|
690
768
|
|
|
769
|
+
// Swipe gesture handlers
|
|
770
|
+
function handleTouchStart(e: TouchEvent) {
|
|
771
|
+
if (!useSwipeMode.value) return
|
|
772
|
+
isDragging.value = true
|
|
773
|
+
dragStartY.value = e.touches[0].clientY
|
|
774
|
+
dragStartOffset.value = swipeOffset.value
|
|
775
|
+
e.preventDefault()
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function handleTouchMove(e: TouchEvent) {
|
|
779
|
+
if (!useSwipeMode.value || !isDragging.value) return
|
|
780
|
+
const deltaY = e.touches[0].clientY - dragStartY.value
|
|
781
|
+
swipeOffset.value = dragStartOffset.value + deltaY
|
|
782
|
+
e.preventDefault()
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function handleTouchEnd(e: TouchEvent) {
|
|
786
|
+
if (!useSwipeMode.value || !isDragging.value) return
|
|
787
|
+
isDragging.value = false
|
|
788
|
+
|
|
789
|
+
const deltaY = swipeOffset.value - dragStartOffset.value
|
|
790
|
+
const threshold = 100 // Minimum swipe distance to trigger navigation
|
|
791
|
+
|
|
792
|
+
if (Math.abs(deltaY) > threshold) {
|
|
793
|
+
if (deltaY > 0 && previousItem.value) {
|
|
794
|
+
// Swipe down - go to previous
|
|
795
|
+
goToPreviousItem()
|
|
796
|
+
} else if (deltaY < 0 && nextItem.value) {
|
|
797
|
+
// Swipe up - go to next
|
|
798
|
+
goToNextItem()
|
|
799
|
+
} else {
|
|
800
|
+
// Snap back
|
|
801
|
+
snapToCurrentItem()
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
// Snap back if swipe wasn't far enough
|
|
805
|
+
snapToCurrentItem()
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
e.preventDefault()
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Mouse drag handlers for desktop testing
|
|
812
|
+
function handleMouseDown(e: MouseEvent) {
|
|
813
|
+
if (!useSwipeMode.value) return
|
|
814
|
+
isDragging.value = true
|
|
815
|
+
dragStartY.value = e.clientY
|
|
816
|
+
dragStartOffset.value = swipeOffset.value
|
|
817
|
+
e.preventDefault()
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function handleMouseMove(e: MouseEvent) {
|
|
821
|
+
if (!useSwipeMode.value || !isDragging.value) return
|
|
822
|
+
const deltaY = e.clientY - dragStartY.value
|
|
823
|
+
swipeOffset.value = dragStartOffset.value + deltaY
|
|
824
|
+
e.preventDefault()
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function handleMouseUp(e: MouseEvent) {
|
|
828
|
+
if (!useSwipeMode.value || !isDragging.value) return
|
|
829
|
+
isDragging.value = false
|
|
830
|
+
|
|
831
|
+
const deltaY = swipeOffset.value - dragStartOffset.value
|
|
832
|
+
const threshold = 100
|
|
833
|
+
|
|
834
|
+
if (Math.abs(deltaY) > threshold) {
|
|
835
|
+
if (deltaY > 0 && previousItem.value) {
|
|
836
|
+
goToPreviousItem()
|
|
837
|
+
} else if (deltaY < 0 && nextItem.value) {
|
|
838
|
+
goToNextItem()
|
|
839
|
+
} else {
|
|
840
|
+
snapToCurrentItem()
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
snapToCurrentItem()
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
e.preventDefault()
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function goToNextItem() {
|
|
850
|
+
if (!nextItem.value) {
|
|
851
|
+
// Try to load next page
|
|
852
|
+
loadNext()
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
currentSwipeIndex.value++
|
|
857
|
+
snapToCurrentItem()
|
|
858
|
+
|
|
859
|
+
// Preload next item if we're near the end
|
|
860
|
+
if (currentSwipeIndex.value >= masonry.value.length - 5) {
|
|
861
|
+
loadNext()
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function goToPreviousItem() {
|
|
866
|
+
if (!previousItem.value) return
|
|
867
|
+
|
|
868
|
+
currentSwipeIndex.value--
|
|
869
|
+
snapToCurrentItem()
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function snapToCurrentItem() {
|
|
873
|
+
if (!swipeContainer.value) return
|
|
874
|
+
|
|
875
|
+
// Use container height for swipe mode instead of window height
|
|
876
|
+
const viewportHeight = swipeContainer.value.clientHeight
|
|
877
|
+
swipeOffset.value = -currentSwipeIndex.value * viewportHeight
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Watch for container/window resize to update swipe mode
|
|
881
|
+
function handleWindowResize() {
|
|
882
|
+
// Update container width if wrapper is available
|
|
883
|
+
if (wrapper.value) {
|
|
884
|
+
containerWidth.value = wrapper.value.clientWidth
|
|
885
|
+
} else if (typeof window !== 'undefined') {
|
|
886
|
+
containerWidth.value = window.innerWidth
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// If switching from swipe to masonry, reset swipe state
|
|
890
|
+
if (!useSwipeMode.value && currentSwipeIndex.value > 0) {
|
|
891
|
+
currentSwipeIndex.value = 0
|
|
892
|
+
swipeOffset.value = 0
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// If switching to swipe mode, ensure we have items loaded
|
|
896
|
+
if (useSwipeMode.value && masonry.value.length === 0 && !isLoading.value) {
|
|
897
|
+
loadPage(paginationHistory.value[0] as any)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Re-snap to current item on resize to adjust offset
|
|
901
|
+
if (useSwipeMode.value) {
|
|
902
|
+
snapToCurrentItem()
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
691
906
|
function init(items: any[], page: any, next: any) {
|
|
692
907
|
currentPage.value = page // Track the initial current page
|
|
693
908
|
paginationHistory.value = [page]
|
|
694
909
|
paginationHistory.value.push(next)
|
|
695
910
|
// Diagnostics: check incoming initial items
|
|
696
911
|
checkItemDimensions(items as any[], 'init')
|
|
697
|
-
|
|
698
|
-
|
|
912
|
+
|
|
913
|
+
if (useSwipeMode.value) {
|
|
914
|
+
// In swipe mode, just add items without layout calculation
|
|
915
|
+
masonry.value = [...(masonry.value as any[]), ...items]
|
|
916
|
+
// Reset swipe index if we're at the start
|
|
917
|
+
if (currentSwipeIndex.value === 0 && masonry.value.length > 0) {
|
|
918
|
+
swipeOffset.value = 0
|
|
919
|
+
}
|
|
920
|
+
} else {
|
|
921
|
+
refreshLayout([...(masonry.value as any[]), ...items])
|
|
922
|
+
updateScrollProgress()
|
|
923
|
+
}
|
|
699
924
|
}
|
|
700
925
|
|
|
701
926
|
// Watch for layout changes and update columns + refresh layout dynamically
|
|
702
927
|
watch(
|
|
703
928
|
layout,
|
|
704
929
|
() => {
|
|
930
|
+
if (useSwipeMode.value) {
|
|
931
|
+
// In swipe mode, no layout recalculation needed
|
|
932
|
+
return
|
|
933
|
+
}
|
|
705
934
|
if (container.value) {
|
|
706
|
-
columns.value = getColumnCount(layout.value as any)
|
|
935
|
+
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
707
936
|
refreshLayout(masonry.value as any)
|
|
708
937
|
}
|
|
709
938
|
},
|
|
710
939
|
{ deep: true }
|
|
711
940
|
)
|
|
712
941
|
|
|
942
|
+
// Watch for swipe mode changes to refresh layout and setup/teardown handlers
|
|
943
|
+
watch(useSwipeMode, (newValue) => {
|
|
944
|
+
nextTick(() => {
|
|
945
|
+
if (newValue) {
|
|
946
|
+
// Switching to Swipe Mode
|
|
947
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
948
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
949
|
+
|
|
950
|
+
// Reset index if needed
|
|
951
|
+
currentSwipeIndex.value = 0
|
|
952
|
+
swipeOffset.value = 0
|
|
953
|
+
if (masonry.value.length > 0) {
|
|
954
|
+
snapToCurrentItem()
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
// Switching to Masonry Mode
|
|
958
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
959
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
960
|
+
|
|
961
|
+
if (container.value && wrapper.value) {
|
|
962
|
+
// Ensure containerWidth is up to date
|
|
963
|
+
containerWidth.value = wrapper.value.clientWidth
|
|
964
|
+
|
|
965
|
+
// Re-attach scroll listener since container was re-created
|
|
966
|
+
container.value.removeEventListener('scroll', debouncedScrollHandler) // Just in case
|
|
967
|
+
container.value.addEventListener('scroll', debouncedScrollHandler, { passive: true })
|
|
968
|
+
|
|
969
|
+
// Refresh layout with updated width
|
|
970
|
+
if (masonry.value.length > 0) {
|
|
971
|
+
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
972
|
+
refreshLayout(masonry.value as any)
|
|
973
|
+
|
|
974
|
+
// Update viewport state
|
|
975
|
+
viewportTop.value = container.value.scrollTop
|
|
976
|
+
viewportHeight.value = container.value.clientHeight
|
|
977
|
+
updateScrollProgress()
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
})
|
|
982
|
+
}, { immediate: true })
|
|
983
|
+
|
|
984
|
+
// Watch for swipe container element to attach touch listeners
|
|
985
|
+
watch(swipeContainer, (el) => {
|
|
986
|
+
if (el) {
|
|
987
|
+
el.addEventListener('touchstart', handleTouchStart, { passive: false })
|
|
988
|
+
el.addEventListener('touchmove', handleTouchMove, { passive: false })
|
|
989
|
+
el.addEventListener('touchend', handleTouchEnd)
|
|
990
|
+
el.addEventListener('mousedown', handleMouseDown)
|
|
991
|
+
}
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
// Watch for items changes in swipe mode to reset index if needed
|
|
995
|
+
watch(() => masonry.value.length, (newLength, oldLength) => {
|
|
996
|
+
if (useSwipeMode.value && newLength > 0 && oldLength === 0) {
|
|
997
|
+
// First items loaded, ensure we're at index 0
|
|
998
|
+
currentSwipeIndex.value = 0
|
|
999
|
+
nextTick(() => snapToCurrentItem())
|
|
1000
|
+
}
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
// Watch wrapper element to setup ResizeObserver for container width
|
|
1004
|
+
watch(wrapper, (el) => {
|
|
1005
|
+
if (resizeObserver) {
|
|
1006
|
+
resizeObserver.disconnect()
|
|
1007
|
+
resizeObserver = null
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (el && typeof ResizeObserver !== 'undefined') {
|
|
1011
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
1012
|
+
for (const entry of entries) {
|
|
1013
|
+
const newWidth = entry.contentRect.width
|
|
1014
|
+
if (containerWidth.value !== newWidth) {
|
|
1015
|
+
containerWidth.value = newWidth
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
})
|
|
1019
|
+
resizeObserver.observe(el)
|
|
1020
|
+
// Initial width
|
|
1021
|
+
containerWidth.value = el.clientWidth
|
|
1022
|
+
} else if (el) {
|
|
1023
|
+
// Fallback if ResizeObserver not available
|
|
1024
|
+
containerWidth.value = el.clientWidth
|
|
1025
|
+
}
|
|
1026
|
+
}, { immediate: true })
|
|
1027
|
+
|
|
1028
|
+
// Watch containerWidth changes to refresh layout in masonry mode
|
|
1029
|
+
watch(containerWidth, (newWidth, oldWidth) => {
|
|
1030
|
+
if (newWidth !== oldWidth && newWidth > 0 && !useSwipeMode.value && container.value && masonry.value.length > 0) {
|
|
1031
|
+
// Use nextTick to ensure DOM has updated
|
|
1032
|
+
nextTick(() => {
|
|
1033
|
+
columns.value = getColumnCount(layout.value as any, newWidth)
|
|
1034
|
+
refreshLayout(masonry.value as any)
|
|
1035
|
+
updateScrollProgress()
|
|
1036
|
+
})
|
|
1037
|
+
}
|
|
1038
|
+
})
|
|
1039
|
+
|
|
713
1040
|
onMounted(async () => {
|
|
714
1041
|
try {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1042
|
+
// Wait for next tick to ensure wrapper is mounted
|
|
1043
|
+
await nextTick()
|
|
1044
|
+
|
|
1045
|
+
// Initialize container width
|
|
1046
|
+
if (wrapper.value) {
|
|
1047
|
+
containerWidth.value = wrapper.value.clientWidth
|
|
1048
|
+
} else if (typeof window !== 'undefined') {
|
|
1049
|
+
containerWidth.value = window.innerWidth
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (!useSwipeMode.value) {
|
|
1053
|
+
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
1054
|
+
if (container.value) {
|
|
1055
|
+
viewportTop.value = container.value.scrollTop
|
|
1056
|
+
viewportHeight.value = container.value.clientHeight
|
|
1057
|
+
}
|
|
719
1058
|
}
|
|
720
1059
|
|
|
721
1060
|
const initialPage = props.loadAtPage as any
|
|
@@ -725,48 +1064,124 @@ onMounted(async () => {
|
|
|
725
1064
|
await loadPage(paginationHistory.value[0] as any)
|
|
726
1065
|
}
|
|
727
1066
|
|
|
728
|
-
|
|
1067
|
+
if (!useSwipeMode.value) {
|
|
1068
|
+
updateScrollProgress()
|
|
1069
|
+
} else {
|
|
1070
|
+
// In swipe mode, snap to first item
|
|
1071
|
+
nextTick(() => snapToCurrentItem())
|
|
1072
|
+
}
|
|
729
1073
|
|
|
730
1074
|
} catch (error) {
|
|
731
1075
|
console.error('Error during component initialization:', error)
|
|
732
1076
|
isLoading.value = false
|
|
733
1077
|
}
|
|
734
1078
|
|
|
735
|
-
|
|
1079
|
+
// Scroll listener is handled by watcher now for consistency
|
|
736
1080
|
window.addEventListener('resize', debouncedResizeHandler)
|
|
1081
|
+
window.addEventListener('resize', handleWindowResize)
|
|
737
1082
|
})
|
|
738
1083
|
|
|
739
1084
|
onUnmounted(() => {
|
|
1085
|
+
if (resizeObserver) {
|
|
1086
|
+
resizeObserver.disconnect()
|
|
1087
|
+
resizeObserver = null
|
|
1088
|
+
}
|
|
1089
|
+
|
|
740
1090
|
container.value?.removeEventListener('scroll', debouncedScrollHandler)
|
|
741
1091
|
window.removeEventListener('resize', debouncedResizeHandler)
|
|
1092
|
+
window.removeEventListener('resize', handleWindowResize)
|
|
1093
|
+
|
|
1094
|
+
if (swipeContainer.value) {
|
|
1095
|
+
swipeContainer.value.removeEventListener('touchstart', handleTouchStart)
|
|
1096
|
+
swipeContainer.value.removeEventListener('touchmove', handleTouchMove)
|
|
1097
|
+
swipeContainer.value.removeEventListener('touchend', handleTouchEnd)
|
|
1098
|
+
swipeContainer.value.removeEventListener('mousedown', handleMouseDown)
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Clean up mouse handlers
|
|
1102
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
1103
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
742
1104
|
})
|
|
743
1105
|
</script>
|
|
744
1106
|
|
|
745
1107
|
<template>
|
|
746
|
-
<div class="
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1108
|
+
<div ref="wrapper" class="w-full h-full flex flex-col relative">
|
|
1109
|
+
<!-- Swipe Feed Mode (Mobile/Tablet) -->
|
|
1110
|
+
<div v-if="useSwipeMode"
|
|
1111
|
+
class="overflow-hidden w-full flex-1 swipe-container touch-none select-none"
|
|
1112
|
+
:class="{ 'force-motion': props.forceMotion, 'cursor-grab': !isDragging, 'cursor-grabbing': isDragging }"
|
|
1113
|
+
ref="swipeContainer"
|
|
1114
|
+
style="height: 100%; max-height: 100%; position: relative;">
|
|
1115
|
+
<div
|
|
1116
|
+
class="relative w-full"
|
|
1117
|
+
:style="{
|
|
1118
|
+
transform: `translateY(${swipeOffset}px)`,
|
|
1119
|
+
transition: isDragging ? 'none' : `transform ${transitionDurationMs}ms ${transitionEasing}`,
|
|
1120
|
+
height: `${masonry.length * 100}%`
|
|
1121
|
+
}">
|
|
1122
|
+
<div
|
|
1123
|
+
v-for="(item, index) in masonry"
|
|
1124
|
+
:key="`${item.page}-${item.id}`"
|
|
1125
|
+
class="absolute top-0 left-0 w-full"
|
|
1126
|
+
:style="{
|
|
1127
|
+
top: `${index * (100 / masonry.length)}%`,
|
|
1128
|
+
height: `${100 / masonry.length}%`
|
|
1129
|
+
}">
|
|
1130
|
+
<div class="w-full h-full flex items-center justify-center p-4">
|
|
1131
|
+
<div class="w-full h-full max-w-full max-h-full">
|
|
1132
|
+
<slot :item="item" :remove="remove">
|
|
1133
|
+
<MasonryItem :item="item" :remove="remove" />
|
|
1134
|
+
</slot>
|
|
1135
|
+
</div>
|
|
1136
|
+
</div>
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
|
|
1140
|
+
<!-- Swipe indicator dots -->
|
|
1141
|
+
<div v-if="masonry.length > 1" class="fixed bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2 z-10 pointer-events-none">
|
|
1142
|
+
<div
|
|
1143
|
+
v-for="(item, index) in masonry.slice(0, Math.min(10, masonry.length))"
|
|
1144
|
+
:key="`dot-${item.id}`"
|
|
1145
|
+
class="w-1.5 h-1.5 rounded-full transition-all duration-300"
|
|
1146
|
+
:class="index === currentSwipeIndex ? 'bg-white w-4' : 'bg-white/40'"
|
|
1147
|
+
/>
|
|
1148
|
+
</div>
|
|
1149
|
+
|
|
1150
|
+
<!-- Item Counter -->
|
|
1151
|
+
<div class="fixed top-20 right-4 bg-black/50 backdrop-blur-sm text-white text-xs font-medium px-3 py-1.5 rounded-full z-20 pointer-events-none">
|
|
1152
|
+
{{ currentSwipeIndex + 1 }} / {{ masonry.length }}
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
|
|
1156
|
+
<!-- Masonry Grid Mode (Desktop) -->
|
|
1157
|
+
<div v-else
|
|
1158
|
+
class="overflow-auto w-full flex-1 masonry-container"
|
|
1159
|
+
:class="{ 'force-motion': props.forceMotion }"
|
|
1160
|
+
ref="container">
|
|
1161
|
+
<div class="relative"
|
|
1162
|
+
:style="{height: `${containerHeight}px`, '--masonry-duration': `${transitionDurationMs}ms`, '--masonry-leave-duration': `${leaveDurationMs}ms`, '--masonry-ease': transitionEasing}">
|
|
1163
|
+
<transition-group name="masonry" :css="false" @enter="enter" @before-enter="beforeEnter"
|
|
1164
|
+
@leave="leave"
|
|
1165
|
+
@before-leave="beforeLeave">
|
|
1166
|
+
<div v-for="(item, i) in visibleMasonry" :key="`${item.page}-${item.id}`"
|
|
1167
|
+
class="absolute masonry-item"
|
|
1168
|
+
v-bind="getItemAttributes(item, i)"
|
|
1169
|
+
:style="{ paddingTop: `${layout.header}px`, paddingBottom: `${layout.footer}px` }">
|
|
1170
|
+
<!-- Use default slot if provided, otherwise use MasonryItem -->
|
|
1171
|
+
<slot :item="item" :remove="remove">
|
|
1172
|
+
<MasonryItem :item="item" :remove="remove" />
|
|
1173
|
+
</slot>
|
|
1174
|
+
</div>
|
|
1175
|
+
</transition-group>
|
|
1176
|
+
|
|
1177
|
+
<!-- Scroll Progress Badge -->
|
|
1178
|
+
<div v-if="containerHeight > 0"
|
|
1179
|
+
class="fixed bottom-4 right-4 bg-gray-800 text-white text-xs rounded-full px-3 py-1.5 shadow-lg z-10 transition-opacity duration-300"
|
|
1180
|
+
:class="{'opacity-50 hover:opacity-100': !scrollProgress.isNearTrigger, 'opacity-100': scrollProgress.isNearTrigger}">
|
|
1181
|
+
<span>{{ masonry.length }} items</span>
|
|
1182
|
+
<span class="mx-2">|</span>
|
|
1183
|
+
<span>{{ scrollProgress.distanceToTrigger }}px to load</span>
|
|
760
1184
|
</div>
|
|
761
|
-
</transition-group>
|
|
762
|
-
|
|
763
|
-
<!-- Scroll Progress Badge -->
|
|
764
|
-
<div v-if="containerHeight > 0"
|
|
765
|
-
class="fixed bottom-4 right-4 bg-gray-800 text-white text-xs rounded-full px-3 py-1.5 shadow-lg z-10 transition-opacity duration-300"
|
|
766
|
-
:class="{'opacity-50 hover:opacity-100': !scrollProgress.isNearTrigger, 'opacity-100': scrollProgress.isNearTrigger}">
|
|
767
|
-
<span>{{ masonry.length }} items</span>
|
|
768
|
-
<span class="mx-2">|</span>
|
|
769
|
-
<span>{{ scrollProgress.distanceToTrigger }}px to load</span>
|
|
770
1185
|
</div>
|
|
771
1186
|
</div>
|
|
772
1187
|
</div>
|
package/src/masonryUtils.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { LayoutOptions, ProcessedMasonryItem } from './types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Get responsive column count based on
|
|
4
|
+
* Get responsive column count based on container width and layout sizes
|
|
5
5
|
*/
|
|
6
|
-
export function getColumnCount(layout: Pick<LayoutOptions, 'sizes'> & { sizes: Required<NonNullable<LayoutOptions['sizes']>> }): number {
|
|
7
|
-
const width = window.innerWidth
|
|
6
|
+
export function getColumnCount(layout: Pick<LayoutOptions, 'sizes'> & { sizes: Required<NonNullable<LayoutOptions['sizes']>> }, containerWidth?: number): number {
|
|
7
|
+
const width = containerWidth ?? (typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
8
8
|
const sizes = layout.sizes
|
|
9
9
|
|
|
10
10
|
if (width >= 1536 && sizes['2xl']) return sizes['2xl']
|